两种linux系统下常见的HOOK方法

嵌入式技术

1372人已加入

描述

本文作者/focus

HOOK通过在系统调用或函数调用前以替换的方式改变程序中原有的函数功能,实现更改原有函数的功能。

利用LD_PRELOAD进行HOOK

Linux提供了一个名为LD_PRELOAD的环境变量。这个环境变量允许用户指定一个或多个共享链接库文件的路径。当程序启动时,动态加载器会在加载C语言运行库之前,首先加载LD_PRELOAD所指定的共享链接库。这种加载方式被称为预装载。

预装载机制使得用户可以在程序执行前插入自定义的共享链接库,从而改变或扩展程序的行为。这些自定义的共享链接库可以包含重写的函数定义,当程序尝试调用这些函数时,动态加载器会优先加载并执行预装载的库中的函数定义,而不是默认的库中的定义。结合LD_PRELOAD和预装载机制,我们可以实现对函数的HOOK。

首先我们编写一个目标程序代码,这个程序会等待用户的输入,从而处于阻塞状态。

 

#include 

int main()
{
  printf("please input a number:
");
  int val = 0;
  scanf("%d", &val);
  printf("already recv your number!
");
  return 0;
}

 

然后编写HOOK函数,该函数重写了scanf函数,打印出一句话,从而使目标程序便能够无需等待用户输入而继续执行。

 

#include 

int main()
{
  printf("please input a number:
");
  int val = 0;
  scanf("%d", &val);
  printf("already recv your number!
");
  return 0;
}

 

通过以下命令分别编译目标程序和用于HOOK的so文件。

 

gcc ./target.c -o target
gcc --shared hook.c -o hook.so -fPIC

 

执行下述命令实现HOOK。可以看到scanf函数由原先的等待用户输入,变成了输出一句话。

 

LD_PRELOAD=./hook.so ./target

 

Linux

利用ptrace进行HOOK

然而上述方法只能对未运行的程序进行HOOK,对于已经运行的程序可以利用ptrace系统调用进行HOOK。

ptrace允许一个进程监控和控制另一个进程的执行,是GDB等调试器实现的基础。利用ptrace进行HOOK的步骤如下所示:

 

1.HOOK程序利用ptrace附加到已经运行的目标程序,获取目标进程运行的上下文,保存原寄存器数据;
2.查找目标程序的link_map链表的指针,根据函数名称遍历查找函数的真实地址。link_map地址存在于.got.plt区节中,该区节的加载地址可以从DYNAMIC段DT_PLTGOT区域得到。在此我们主要查找dlopen函数地址;
3.通过修改目标程序的寄存器和堆栈使目标程序调用dlopen函数,从而将hook.so加载到目标程序中;
4.将要HOOK的原函数地址,替换为hook.so中重写后的新函数地址。因为hook.so在上一步被dlopen加载到目标内存空间中,所以重写后的新函数地址可以通过步骤2得到;
5.恢复目标程序原寄存器的内容,传入PTRACE_DETACH参数结束对目标程序的附加。

 

接下来详细介绍具体的实现:

利用ptrace附加目标程序,传入user_regs_struct结构体用于保存目标程序的寄存器信息。

 

void ptrace_attach(pid_t pid, struct user_regs_struct *regs)
{
    if(ptrace(PTRACE_ATTACH, pid, NULL, NULL) < 0)
    {
        printf("ptrace_attach error
");
    }
    
    waitpid(pid, NULL, WUNTRACED);
    
    if(ptrace(PTRACE_GETREGS, pid, NULL, regs)) 
    {
        printf("ptrace_getregs error!
");
    }
}

 

这里省略了查找link_map链表指针,该链表可以通过解析ELF文件结构获取到。主要看如何遍历link_map链表查找指定函数的地址。

在find_symbol函数中通过link_map链表指针获取link_map结构体内容,根据link_map中l_name字段判断该动态链接库是否有效。

在find_symbol_in_linkmap函数中,通过解析link_map中l_ld字段,获取动态链接库的符号表等信息,与指定函数名称进行对比,获取该函数的地址。

 

Elf_Addr find_symbol(int pid, Elf_Addr lm_addr, char *sym_name)
{
    struct link_map lmap;//存储lmap的内容
    unsigned int nlen = 0;

    while (lm_addr) 
    {
        // 有了link_map指针后,根据指针获取link_map结构体的内容
        ptrace_getdata(pid, lm_addr, &lmap, sizeof(struct link_map));
    // 获取下一个link_map结构体的指针
        lm_addr = (Elf_Addr)(lmap.l_next);
        // 判断动态链接库是有否有效
        if (0 == lmap.l_name) 
        {
            continue;
        }
        Elf_Addr sym_addr = find_symbol_in_linkmap(pid, &lmap, sym_name);
        if (sym_addr) 
        {
            return sym_addr;
        }
    }
    return 0;
}

 

通过上一步的find_symbol函数,可以得到dlopen的函数地址,模拟调用dlopen函数,设置栈空间将要加载的so库绝对路径写入栈地址,调用dlopen加载so库到目标地址中。

 

int inject_code(pid_t pid, unsigned long dlopen_addr, char *libc_path)
{
    char sbuf1[STRLEN], sbuf2[STRLEN];
    struct user_regs_struct regs, saved_regs;
    int status;

    ptrace_getregs(pid, ®s);//获取所有寄存器值
    ptrace_getdata(pid, regs.rsp + STRLEN, sbuf1, sizeof(sbuf1));
    ptrace_getdata(pid, regs.rsp, sbuf2, sizeof(sbuf2));

    /*用于引发SIGSEGV信号的ret内容*/
    unsigned long ret_addr = 0x666;

    ptrace_setdata(pid, regs.rsp, (char *)&ret_addr, sizeof(ret_addr));
    ptrace_setdata(pid, regs.rsp + STRLEN, libc_path, strlen(libc_path) + 1); 

    memcpy(&saved_regs, ®s, sizeof(regs));

    regs.rdi = regs.rsp + STRLEN;
    regs.rsi = RTLD_NOW|RTLD_GLOBAL|RTLD_NODELETE;
    regs.rip = dlopen_addr+2;

    if (ptrace(PTRACE_SETREGS, pid, NULL, ®s) < 0)
    {
        printf("inject_code:PTRACE_SETREGS 1 failed!");
    }
    if (ptrace(PTRACE_CONT, pid, NULL, NULL) < 0)
    {
        printf("inject_code:PTRACE_CONT failed!");
    }
    waitpid(pid, &status, 0);
    if (ptrace(PTRACE_SETREGS, pid, 0, &saved_regs) < 0)
    {
        printf("inject_code:PTRACE_SETREGS 2 failed!");;
    }
    ptrace_setdata(pid, saved_regs.rsp + STRLEN, sbuf1, sizeof(sbuf1));
    ptrace_setdata(pid, saved_regs.rsp, sbuf2, sizeof(sbuf2));
    return 0;
}

 

接下来查找目标程序的got表,修改目标函数的got表内容为新函数的地址。

 

Elf_Addr find_sym_in_rel(int pid, char *sym_name)
{
    Elf_Rel *rel = (Elf_Rel *) malloc(sizeof(Elf_Rel));
    Elf_Sym *sym = (Elf_Sym *) malloc(sizeof(Elf_Sym));
    int i;
    char str[STRLEN] = {0};
    unsigned long ret;
    struct lmap_result *lmret = get_dyn_info(pid);

    for (i = 0; inrelplts; i++) 
    {
        ptrace_getdata(pid, lmret->jmprel + i*sizeof(Elf_Rela), rel, sizeof(Elf_Rela));
        ptrace_getdata(pid, lmret->symtab + ELF64_R_SYM(rel->r_info) * sizeof(Elf_Sym), sym, sizeof(Elf_Sym));
        int n = ptrace_getstr(pid, lmret->strtab + sym->st_name, str, STRLEN);
        if (strcmp(str, sym_name) == 0) 
            break;
    }
    if (i == lmret->nrelplts)
        ret = 0;
    else
        ret = rel->r_offset;
    free(rel);
    return ret;
}

 

在这里我们修改一下目标程序,循环10次,每次接收控制台输入,并打印出来。

 

#include 
#include 
int main()
{
  int val = 10;
  while (val--)
  {
        sleep(2);
    printf("please input a number:
");
    int val = 0;
    scanf("%d", &val);
    printf("your val is %d
", val); 
  }

  return 0;
}

 

在HOOK代码中,自动为val变量赋值。

 

#include 
#include 
int num = 1;
int hookscanf(const char *format,...)
{
    va_list ap;
    int retval;
    va_start(ap, format);
    int* pval = va_arg(ap, int*);
  printf("自动输入:%d
", num);
    *pval = num++;
    return 0;
}

 

编译运行,效果如下,不需要在控制台进行输入,自动为val赋值。

Linux

小结

本文介绍了两种linux系统下常见的HOOK方法,第一种方法比较简单但无法对已经运行的程序进行HOOK,第二种方法相较于第一种会更加复杂,需要对ELF文件格式以及相关结构有更深入的了解。

审核编辑:黄飞

 

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分