1. 背景介绍

1.1 Inject方式

我们要hook一个应用,首先我们需要有手段往目标程序添加代码。这种动作一般称之为inject注入,常见的注入手段分为两种:

  • 静态注入 (elf的DT_NEED)
  • 动态注入 (ptrace)

1.2 Hook方式

注入了以后还需要替换某些处理路径,才有作用。关于linux用户态程序的hook,已经在andriod中做了大量的研究称之为native hook。

根据hook的原理主要分为两大类:

  • PLT/GOT hook
  • inline hook

接下来我们就来详细解析inject和hook的原理。

2 Inject方式

2.1 静态注入

静态注入一般都是通过修改elf依赖so文件的方式进行注入的。

  • 1、修改elf的依赖so文件

首先安装patchelf工具,patchelf命令可以很方便的修改elf文件的一些配置。

/* (1) 修改前main依赖的so文件 */
$ ldd main
        linux-vdso.so.1 =>  (0x00007ffcf8dfa000)
        libtest.so => /home/ipu/sohook/libtest.so (0x00007f3aedc70000)
        libc.so.6 => /lib64/libc.so.6 (0x00007f3aed8a2000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f3aede72000)
$ 
/* (2) 将libinject.so加入到main文件的依赖列表中 */
$ patchelf --add-needed /home/ipu/sohook/libinject.so
patchelf: missing filename
$ patchelf --add-needed /home/ipu/sohook/libinject.so main
/* (3) 查看结果,修改成功 */
$ ldd main
        linux-vdso.so.1 =>  (0x00007ffef96d1000)
        /home/ipu/sohook/libinject.so (0x00007f87c7fb7000)
        libtest.so => /home/ipu/sohook/libtest.so (0x00007f87c7db5000)
        libc.so.6 => /lib64/libc.so.6 (0x00007f87c79e7000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f87c81b9000)
  • 2、定义库函数的构造和析构函数

在libinject.so的代码实现中,使用属性__attribute__ ((constructor))来定义库的初始化函数,这些函数在库加载时会被自动执行。__attribute__ ((destructor))用来定义库的退出函数。

$ gcc inject.c -shared -fPIC -o libinject.so
$ cat inject.c 
#include <stdio.h>
#include <stdlib.h>

void __attribute__ ((constructor)) inject_init()
{
        printf("libinject.so init.\n");
}

void __attribute__ ((destructor)) inject_exit()
{
        printf("libinject.so exit.\n");
}

我们可以在初始化函数中执行hook动作。

  • 3、验证结果
$ ./main 
libinject.so init.  // libinject.so被成功加载,且inject_init()被自动执行

2.2 动态注入

动态注入一般使用ptrace来实现的,一般的流程如下:

  • 1、attach上目标进程。
  • 2、查找到目标进程的dlopen函数,调用dlopen加载到注入的so到目标进程空间。
  • 3、查找到目标进程的dlsym函数,调用dlsym查找到新so中的目标函数地址,并调用目标函数。
  • 4、在目标函数中,可以实现很多功能,比如hook掉某个函数。

具体例子可以参考:

3. PLT/GOT hook

3.1 原理

理解本节需要一些ELF编译链接加载的基础知识。

  • 1、编译链接

在编译.c文件成.o文件时,把需要引用的符号放在一张专门的重定位表.rela.text中。这张表主要有两个元素:引用符号的位置,符号的名称。

$ gcc main.c -c -o main.o
[ipu@localhost sohook]$ readelf -S main.o 
There are 13 section headers, starting at offset 0x938:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align

  [ 2] .rela.text        RELA             0000000000000000  00000630
       0000000000000258  0000000000000018   I      10     1     8

$ readelf -r main.o 

Relocation section '.rela.text' at offset 0x630 contains 25 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000014  00050000000a R_X86_64_32       0000000000000000 .rodata + 0
00000000001e  000a00000002 R_X86_64_PC32     0000000000000000 printf - 4
00000000002a  000b00000002 R_X86_64_PC32     0000000000000000 malloc - 4
...

这时引用符号的位置上填的是0。例如’.rela.text’表的第2个表项offset=0x1e name=prinf,可以查看到.o文件的offset=0x1e处填的偏移还是00 00 00 00

$ objdump -d main.o 

  1d:   e8 00 00 00 00          callq  22 <my_malloc+0x22>

在这里插入图片描述

在链接多个.o文件成exe文件时,外部符号地址被确定以后,根据重定位表.rela.text修复引用外部符号位置上的0为确定后的地址。这种操作就称为重定位操作。

  • 2、运行加载时动态链接

exe文件运行的时候,还需要进行重定位操作。因为exe还有部分的外部符号没有解析,这些符号在外部的.so库中,需要在加载时进行动态链接。

同样exe为了重定位定义了两张表:.rela.dyn用来做外部数据的重定位,.rela.plt用来做外部程序的重定位(和.rela.text类似)。

不同的是为了提高定位效率,exe程序中并不是直接访问外部数据和程序,而是加入了间接访问的中间层表:GOT(Global Offset Table)负责间接访问外部数据,PLT(Procedure Linkage Table)负责间接访问外部程序。
如果exe程序中有多处引用外部符号,我们只需要修改GOT/PLT中一处即可,大大加快了效率。另外如果运行时外部符号和引用处的offset过大,引用中的offset空间可能不够用,而GOT/PLT表项直接使用了typeof(long)长度,无后顾之忧又不浪费空间。

$ gcc main.c -o main
$ objdump -d main 

  4008ca:       e8 41 fe ff ff          callq  400710 <printf@plt>

可以看到exe文件引用printf的同样位置已经不是0了,而是41 fe ff ff,当前pc计算出来的位置为0x400710。这个位置并不是printf函数在libc.so中的地址,而是本文件内的间接访问表.plt

$ objdump -d main 

Disassembly of section .plt:

0000000000400710 <printf@plt>:
  400710:       ff 25 0a 09 20 00       jmpq   *0x20090a(%rip)        # 601020 <printf@GLIBC_2.2.5>
  400716:       68 01 00 00 00          pushq  $0x1
  40071b:       e9 d0 ff ff ff          jmpq   4006f0 <.plt>

.plt中的jmpq *0x20090a(%rip) # 601020 <printf@GLIBC_2.2.又跳转到了.got.plt段中:

$ objdump -D main 

Disassembly of section .got.plt:

0000000000601000 <_GLOBAL_OFFSET_TABLE_>:

  60101f:       00 16                   add    %dl,(%rsi)
  601021:       07                      (bad)  
  601022:       40 00 00                add    %al,(%rax)
  601025:       00 00                   add    %al,(%rax)
  601027:       00 26                   add    %ah,(%rsi)

在动态链接时,.rela.dyn用来重定位修复GOT(.got),.rela.plt用来重定位修复PLT(.got.plt)。

$ readelf -r main

Relocation section '.rela.plt' at offset 0x5b0 contains 12 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend

000000601020  000200000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0

下面的图很好的阐释了上述的关系和原理:
在这里插入图片描述

这样实现了动态链接的特性,即使用的时候才链接,不使用时可以不用链接。第一次是动态解析code → .plt → .got.plt → prepare_resolver解析完以后把外部符号的绝对地址写入到.got.plt中,后续直接通过code → .plt → .got.plt就可以直接访问了。

  • 3、结论

我们看到elf程序引用外部.so中函数,运行时动态链接的原理:就是把.so中的函数的地址填写到.got.plt表项中即可。

既然如此,我们改写.got.plt表项中的地址即可hook掉原有的函数。

但是这种hook也有限制,它只能hookelf引用的外部符号(在.plt.got.plt中有表项的),对于elf内部的符号则不能hook。

3.2 hook so

在这里插入图片描述

首先我们使用一个例子演示hook一个.so引用的其他so的外部符号:

  • 1、构造需要hook的实例:

构造库文件libtest.so

$ cat test.h 
#ifndef TEST_H
#define TEST_H 1

#ifdef __cplusplus
extern "C" {
#endif


void say_hello();

#ifdef __cplusplus
}
#endif
#endif

$ cat test.c 
#include <stdio.h>
#include <stdlib.h>

void say_hello()
{
        char *buf = malloc(1024);

        if (NULL != buf){
                snprintf(buf, 1024, "hello.\n");
                printf("%s", buf);
        }

}

$ gcc test.c -shared -fPIC -o libtest.so

构造可执行程序main

$ cat main.c 

#include "test.h"


int main()
{
        say_hello();
        return 0;
}

$ gcc main.c -L. -ltest -o main
$ export LD_LIBRARY_PATH=`pwd`:$LD_LIBRARY_PATH
$ ./main 
hello.

我们尝试hook libtest.so中say_hello()调用的malloc()函数,替换成自己的my_malloc()函数。

  • 2、查询被hook函数的.got.plt表项地址
$ readelf -r libtest.so 

Relocation section '.rela.plt' at offset 0x538 contains 4 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend

000000201028  000400000007 R_X86_64_JUMP_SLO 0000000000000000 malloc@GLIBC_2.2.5 + 0

可以看到malloc()函数的.got.plt表项地址为0x000000201028

当然这个地址是相对地址,实际地址还需要加上libtest.so在进程空间vma中的基地址。

$ ./main &
$ ps -ef | grep main
ipu       10718   6740  0 17:48 pts/0    00:00:00 ./main
$ 
$ cat /proc/10718/maps 

7f31cb3fc000-7f31cb3fd000 r-xp 00000000 fd:00 36168917                   /home/ipu/sohook/libtest.so
7f31cb3fd000-7f31cb5fc000 ---p 00001000 fd:00 36168917                   /home/ipu/sohook/libtest.so
7f31cb5fc000-7f31cb5fd000 r--p 00000000 fd:00 36168917                   /home/ipu/sohook/libtest.so
7f31cb5fd000-7f31cb5fe000 rw-p 00001000 fd:00 36168917                   /home/ipu/sohook/libtest.so

可以看到libtest.so的基地址为0x7f31cb3fc000。接下来我们修改 0x000000201028 + 0x7f31cb3fc000 地址中的内容,就可以hooklibtest.so中的malloc函数了。

  • 3、修改.got.plt表项中的内容

我们这里通过程序的方式去修改:

$ cat main.c 

#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>
#include <unistd.h>
#include <sys/mman.h>
#include <string.h>
#include "test.h"

#define PAGE_SIZE       0x1000
#define PAGE_MASK       (~(0x1000-1))
#define PAGE_START(addr) ((addr) & PAGE_MASK)
#define PAGE_END(addr)   (PAGE_START(addr) + PAGE_SIZE)

/*  hook 函数
    把`libtest.so`中的malloc()函数替换成my_malloc()
 */
void *my_malloc(size_t size)
{
    printf("%zu bytes memory are allocated by libtest.so\n", size);
    return malloc(size);
}

void hook()
{
    char       line[512];
    FILE      *fp;
    unsigned long  base_addr = 0;
    unsigned long  base_addr1 = 0;
    unsigned long  addr;
    int inode_num = 0;
    int  ret=0;

    /* (1.1) 获取`libtest.so`在进程空间中的基地址 */
    //find base address of libtest.so
    if(NULL == (fp = fopen("/proc/self/maps", "r"))) return;
    while(fgets(line, sizeof(line), fp))
    {
        if(NULL != strstr(line, "libtest.so") &&
           (ret=sscanf(line, "%"PRIxPTR"-%*lx %*4s 00000000 %*s %d", &base_addr, &inode_num)) == 2){
            printf("base_addr = 0x%lx, ret = %d \n", base_addr, ret);
            //break;
        }
        if(NULL != strstr(line, "libtest.so") &&
           sscanf(line, "%"PRIxPTR"-%*lx %*4s 00001000 %*s %d", &base_addr1, &inode_num) == 2){
            break;
        }
    }
    fclose(fp);
    printf("base_addr = 0x%lx base_addr1 = 0x%lx\n", base_addr, base_addr1);
    if(0 == base_addr) return;

    /* (1.2) 计算malloc对应的`.got.plt`表项地址 */
    //the absolute address
    addr = base_addr + 0x201028;
    addr = base_addr1 - 0x1000 + 0x201028;
    
    /* (1.3) 解除对应内存的写保护 */
    //add write permission
    mprotect((void *)PAGE_START(addr), PAGE_SIZE, PROT_READ | PROT_WRITE);

    /* (1.4) 替换`.got.plt`表项中的内容
        替换成新函数`my_malloc()`的地址
     */
    //replace the function address
    *(void **)addr = my_malloc;

    /* (1.5) 刷新cache */
    //clear instruction cache
    __builtin___clear_cache((void *)PAGE_START(addr), (void *)PAGE_END(addr));
}

int main()
{
        /* (1) hook`libtest.so`中的malloc */
        hook();

        /* (2) 调用`libtest.so`中的函数 */
        say_hello();

        while(1){
                sleep(1);
        }

        return 0;
}

执行main来验证hook效果:

$ ./main 
1024 bytes memory are allocated by libtest.so   // `libtest.so`中的malloc已经被替换成my_malloc
hello.

3.3 hook exe

在这里插入图片描述

hook.so引用的外部符号有很大的局限性,因为exe通常是直接引用.so中的函数,而不是再通过一个间接的.so来来间接访问。

不如我们直接hook掉exe文件中的.got.plt表项,这样至少我们能hookexe中引用的所有外部函数。

本节我们直接使用gdb来操作,转换成代码可以使用ptrace()函数来操作即可。

  • 1、选定hook目标

我们复用上一节的例子,这里我们hookexe文件中main()函数的sleep()函数,将其替换成my_malloc()函数

int main()
{
        ...

        while(1){
                sleep(1);   // 替换成my_malloc()
        }

        return 0;
}
  • 2、计算被hook函数对应的.got.plt表项地址
$ readelf -r main

Relocation section '.rela.plt' at offset 0x5b0 contains 12 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend

000000601068  000b00000007 R_X86_64_JUMP_SLO 0000000000000000 sleep@GLIBC_2.2.5 + 0

可以看到sleep()函数的.got.plt表项地址为0x000000601068。因为exe文件地址和进程空间是对应的,所以这里不需要再加上偏移地址。

  • 3、计算新函数的地址
$ readelf -s main

Symbol table '.symtab' contains 74 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name

    47: 00000000004008ad    48 FUNC    GLOBAL DEFAULT   13 my_malloc

my_malloc()函数的符号地址为0x00000000004008ad,因为在exe文件中所以也不需要加偏移。

  • 4、修改.got.plt表项中的内容
$ ps -ef | grep main
ipu       11318   6740  0 18:26 pts/0    00:00:00 ./main
$ gdb -p 11318
gdb-peda$ x/1xg 0x000000601068      // `.got.plt`表项中的值为0x00007f5ed5c6d5d0,对应sleep()
0x601068:       0x00007f5ed5c6d5d0
gdb-peda$ set {long}0x000000601068=0x00000000004008ad  // 修改成0x00000000004008ad,对应my_malloc()
gdb-peda$ x/1xg 0x000000601068
0x601068:       0x00000000004008ad

验证:

gdb-peda$ c     // gdb continue命令

1 bytes memory are allocated by libtest.so   // sleep()函数已经被替换成my_malloc()
1 bytes memory are allocated by libtest.so
1 bytes memory are allocated by libtest.so
1 bytes memory are allocated by libtest.so
1 bytes memory are allocated by libtest.so
1 bytes memory are allocated by libtest.so

具体例子可以参考:

4. inline hook

上述plt/got的hook还是有很多限制,只能hook.got.plt中引用的外部函数。如果需要hook其他位置的函数,或者在任意位置插入hook,这种就需要inline hook。

本节借鉴别人arm上的实现,重点说明原理。x86上还需要重新实现和调试。

4.1 hook 某个函数

  • 1、原函数:
void sevenWeapons(int number)
{
    char* str = "Hello,LiBieGou!";
    printf("%s %d\n",str,number);
}
  • 2、Hook:
    在这里插入图片描述
int hook_direct(struct hook_t *h, unsigned int addr, void *hookf)
{
    int i;

    printf("addr  = %x\n", addr);
    printf("hookf = %x\n", (unsigned int)hookf);

//mprotect
    /* (1) 解除被hook位置的写保护 */
    mprotect((void*)0x8000, 0xa000-0x8000, PROT_READ|PROT_WRITE|PROT_EXEC);

//modify function entry 
    /* (2) hook */
    h->patch = (unsigned int)hookf;
    h->orig = addr;
    /* (2.1) 构造跳转指令 */
    h->jump[0] = 0xe59ff000; // LDR pc, [pc, #0]
    h->jump[1] = h->patch;
    h->jump[2] = h->patch;
    /* (2.2) 备份hook点的原指令 */
    for (i = 0; i < 3; i++)
        h->store[i] = ((int*)h->orig)[i];
    /* (2.3) 替换hook点为新的跳转指令 */
    for (i = 0; i < 3; i++)
        ((int*)h->orig)[i] = h->jump[i];

//cacheflush    
    /* (3) 刷新cache */
    hook_cacheflush((unsigned int)h->orig, (unsigned int)h->orig+sizeof(h->jump));
    return 1;
}
  • 3、新函数:
    在这里插入图片描述
void  __attribute__ ((noinline)) my_sevenWeapons(int number)
{
    /* (1) hook函数的自定义操作 */
    printf("sevenWeapons() called, number = %d\n", number);
    number++;

    void (*orig_sevenWeapons)(int number);
    orig_sevenWeapons = (void*)eph.orig;

    /* (2) 处理完hook上的新操作
          继续兼任的处理旧的操作
     */
    /* (2.1) 把hook点恢复成原指令 */
    hook_precall(&eph);
    /* (2.2) 调用原函数 */
    orig_sevenWeapons(number);
    /* (2.3) 如果需要下一次hook,继续把hook点替换成跳转到新函数
            和hook_direct()函数一样
     */
    hook_postcall(&eph);

}

↓

void hook_precall(struct hook_t *h)
{
    int i;
    /* (2.1.1) 把hook点恢复成原指令 */
    for (i = 0; i < 3; i++)
        ((int*)h->orig)[i] = h->store[i];

    hook_cacheflush((unsigned int)h->orig, (unsigned int)h->orig+sizeof(h->jump)*10);
}
  • 4、风险和问题

这种方法一个最大的问题,还是hook操作需要替换多条指令,在多进程使用下存在互斥风险。

具体例子可以参考:

4.2 hook 任意位置

上一节现实了hook任意的内部函数,但是人类是永不满足的,如果我想hook任意位置怎么处理?

arm上已经有了一个现成的方案:
在这里插入图片描述

  • 第1步

根据/proc/self/map中目标so库的内存加载地址与目标Hook地址的偏移计算出实际需要Hook的内存地址。将目标地址处的2条ARM32汇编代码(8 Bytes)进行备份,然后用一条LDR PC指令和一个地址(共计8 Bytes)替换它们。这样就能(以arm模式)将PC指向图中第二部分stub代码所在的位置。由于使用的是LDR而不是BLX,所以lr寄存器不受影响。关键代码如下:

//LDR PC, [PC, #-4]对应的机器码为:0xE51FF004

BYTE szLdrPCOpcodes[8] = {0x04, 0xF0, 0x1F, 0xE5};
//将目的地址拷贝到跳转指令下方的4 Bytes中

memcpy(szLdrPCOpcodes + 4, &pJumpAddress, 4);
  • 第2步

构造stub代码。构造思路是先保存当前全部的寄存器状态到栈中。然后用BLX命令(以arm模式)跳转去执行用户自定义的Hook后的函数。执行完成后,从栈恢复所有的寄存器状态。最后(以arm模式)跳转至第三部分备份代码处。关键代码如下:

_shellcode_start_s:
    push    {r0, r1, r2, r3}
    mrs     r0, cpsr
    str     r0, [sp, #0xC]
    str     r14, [sp, #8]   
    add     r14, sp, #0x10
    str     r14, [sp, #4]    
    pop     {r0}               
    push    {r0-r12}           
    mov     r0, sp
    ldr     r3, _hookstub_function_addr_s
    blx     r3
    ldr     r0, [sp, #0x3C]
    msr     cpsr, r0
    ldmfd   sp!, {r0-r12}       
    ldr     r14, [sp, #4]
    ldr     sp, [r13]
    ldr     pc, _old_function_addr_s
  • 第3步

构造备份代码。构造思路是先执行之前备份的2条arm32代码(共计8 Btyes),然后用LDR指令跳转回Hook地址+8bytes的地址处继续执行。此处先不考虑PC修复,下文会说明。构造出来的汇编代码如下:

备份代码1
备份代码2
LDR PC, [PC, #-4]
HOOK_ADDR+8
  • 指令修复

从上述步骤可以看到,我们是备份了hook点的指令,然后腾出位置来放新的跳转指令。但是有两个问题:

1、备份指令的位置已经发生改变,如果涉及到使用PC计算的指令,需要重新修正。

针对这种情况,需要根据指令的新位置重新计算PC值,更新指令偏移。

2、其他位置的指令可能跳转到hook点,但是hook点的指令已经发生改变,这样会逻辑出错。

这种情况基本没有修复的手段,只能根据ida的分析,找一个被调用风险较小的位置进行hook

  • 风险和问题

这种方法一个最大的问题,还是hook操作需要替换多条指令,在多进程使用下存在互斥风险。

具体例子可以参考:

参考文档:

1.Android Native Hook
2.盘点Android常用Hook技术
3.Android Arm Inline Hook
4.Android Native Hook技术路线概述
5.Android Native Hook工具实践
6.Android Hook(上)
7.Android Hook(下)
8.Android Hook 框架(Cydia篇)
9.Android Hook 框架(XPosed篇)
10.Android平台下hook框架adbi的研究(上)
11.Android平台下hook框架adbi的研究(下)
12.Android hacking: hooking system functions used by Dalvik
13.Android中so文件的Hook
14.Android的so注入( inject)和函数Hook(基于got表) - 支持arm和x86
15.ELF文件装载链接过程及hook原理
16.Executable and Linking Format (ELF) Specification
17.链接加载原理及elf文件格式
18.Android中的so注入(inject)和挂钩(hook) - For both x86 and arm
19.linux-inject:动态注入替换进程调用函数
20.linux-inject, 将共享对象注入到Linux进程中的工具
21.linux 修改 elf 文件的dynamic linker 和 rpath
22.一个静态注入动态库的工具: luject
23.linux下调用共享库非导出函数

Logo

更多推荐