Linux用户态程序 Inject & Hook
文章目录
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 原理
- 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下调用共享库非导出函数
本文来自博客园,作者:pwl999,转载请注明原文链接:https://www.cnblogs.com/pwl999/p/15535002.html