(转载兼整理)Linux 2.6 下通过 ptrace 和 plt 实现用户态 API Hook
这厮此文写的相当实用,不知道为啥不好好整理一下,得,我代劳了吧。作者:l04m33@gmail.com,原文。去看一眼就知道我干嘛干这个脏活儿了... 感觉这篇文章有上首页的素质,可惜不是我自己写的,那就算了吧。
本来我自己想用类似这篇文章说的方法,仔细琢磨了一下,似乎我的事儿还是用别的方法干比较好。另外感觉和这篇文章需求相似的话,只要不是偷着摸着干事,也还是LD_PRELOAD来的简单直接。LD_PRELOAD的方法见:http://www.linuxjournal.com/article/7795,和这里。
以下是转载的那篇的正文。
最近写的,晒一下—_—正文里面出了点比较严重的问题,是某L在某处神志不清地把这篇东西贴出去之后才囧然发现的,所以就干脆不改了……看出来的当看笑话吧—_—
目录
1、背景
2、ELF文件格式和库函数动态解析过程
3、ptrace和Linux下调试器工作原理
4、实现过程
5、程序代码
6、参考资料
1、背景
API Hook,传说中的API钩子,是指神不知鬼不觉地替换掉标准系统API的方法和研究这种方法的艺术学科……好吧,学科的确谈不上,不过这些技术貌似在Windows下面已经发展得登峰造极了,大家回头看看坐在后排为数众多的病毒+木马就不难明白……当然大家不要理解错了,我没有教大家干坏事的意思……比方说我们还可以用API钩子让glut支持全屏抗锯齿之类的……
写这个之前在网上看过一阵,发现似乎Linux下的API钩子大家研究得比较少……可能主要是因为,第一,有LD_PRELOAD这种方便易用的好东东……第二,Linux的运行库不像Windows,没有提供专用的挂钩工具,而且由于两者之间进程模型的区别,要在Linux下面实现API钩子是比较麻烦的(忽略 LD_PRELOAD —_—),特别是全局Hook。
这篇文章旨在研究怎样比LD_PRELOAD更有技术含量地钩上系统API,当然啦,还是局部挂钩,扩展成全局的话还没试过,不清楚效率 怎么样……另外我们这里用的是ptrace,和调试器原理相似,可以在目标程序运行时将钩子挂上,有牛人已经知道的就直接无视俺吧……
然后这篇文章是基于 Linux 2.6 和 x86-64 机器的,例程在32位机器上跑不起来,不过原理不变……这篇东西假设你了解AT&T格式的x86汇编、c语言和 x86-64 cpu的一些特性,当然基本的Linux操作知识是必不可少的……
2、ELF文件格式和库函数动态解析过程
ELF,Executable and Linking Format,是Linux使用的可执行文件和链接库格式,一般分为几部分(废话),行话叫“sections”,就是我们运行objdump -h /bin/bash之后左边第二列的内容,名字有个点,都叫“.text”、“.data”之类的,大部分顾名思义没问题(详情见参考资料[ELFstd])……像 .text section 就是ELF中专放代码的部分。
这里我们关心的是“.got”(Global Offset Table)和“.plt”(Procedure Linkage Table)两个区域,因为它们和ELF的动态链接有非常暧昧的关系—_—:当程序代码在 .text 区里 call 一个库函数的时候,call 指令的操作数实际上不是对应库函数映射到内存中的入口位置,而是 .plt 区域中的一个地址,这个地址对应类似下面的一条 jmp 指令。比方说我们在代码区见到这样一条指令
45d23f: e8 04 b3 fb ff callq 418548
这个用 objdump -d 可以看到(详情见 man objdump),最后尖括号里的东西是 objdump 自己加上去的,说明被 call 的这个地址在 plt 里 chdir 对应的部分。下面就是 0x418548 附近的景象
0000000000418548 :
418548: ff 25 b2 1c 2a 00 jmpq *0x2a1cb2(%rip) # 6ba200
41853e: 68 00 00 00 00 pushq $0x0
418543: e9 e0 ff ff ff jmpq 418528 <_init+0x18>
第一个jmp指令里的 %rip 是下面 push 指令的地址,所以是jump到了 0x41854e+0x2a1cb2=0x6ba200 这个内存单元的内容所指向的位置。不要被 objdump 加上去的注释骗了—_—,那个地址不是代码区内的,和compgen_doc函数也一点关系都没有。
实际上那是 .got 区内的地址,而那个地址里放的就是真正的chdir入口,是由 ld-linux 在将可执行文件扯进内存的时候根据文件内的符号填上的,这样无论库函数被映射到内存中的什么偏僻地方我们的程序都能马上找到,从而实现“位置无关”和“动态链接”这样的东西。
总结一下,控制从用户程序转移到库函数的过程就是call xxxx -> xxxx函数的plt表项 -> xxxx函数的got表项 -> 真正的xxxx库函数入口其实蛮简单的嘛……
接下来用gdb验证一下上面的说法,因为got表项只有在程序运行时才能查到(又废话—_—)
[l_amee@localhost linux]$ gdb /bin/bash # 我们的目标是 bash :目
GNU gdb Fedora (6.8-17.fc9)(gdb的版权信息,省略)
This GDB was configured as "x86_64-redhat-linux-gnu"...(no debugging symbols found)
Missing separate debuginfos, use: debuginfo-install bash.x86_64
(gdb) startBreakpoint 1 at 0x41a2c0
Starting program: /bin/bash (no debugging symbols found)……0x000000000041a2c0 in main ()
(gdb) x/20xg 0x6ba1d8 # 这是用 objdump -h 看到的got起始地址
0x6ba1d8 <__mbrlen+2756720>: 0x0000000000000000 0x00000000006ba028
0x6ba1e8 <__mbrlen+2756736>: 0x000000000041853e 0x0000000000000000
0x6ba1f8 <__mbrlen+2756752>: 0x000000321eedb670 0x000000321eed6ec0
0x6ba208 <__mbrlen+2756768>: 0x000000321ee70a70 0x000000321eed6de0
0x6ba218 <__mbrlen+2756784>: 0x000000321ee35ee0 0x000000321ee809f0
0x6ba228 <__mbrlen+2756800>: 0x000000321ee37560 0x000000321ee82460
0x6ba238 <__mbrlen+2756816>: 0x000000321ee89410 0x000000321eea7260
0x6ba248 <__mbrlen+2756832>: 0x000000321ee887b0 0x000000321eef9d60
0x6ba258 <__mbrlen+2756848>: 0x000000321eed6510 0x000000321eedc180
0x6ba268 <__mbrlen+2756864>: 0x000000321ee33c00 0x000000321eed7370
(gdb) x/i 0x000000321eed6ec0
0x321eed6ec0 : mov $0x50,%eax # 我们找到 chdir() 了……
(gdb)
可以看到上面列出了N多 0x321 打头的地址,那是我的机器上库函数的映射地址。头4条记录貌似是链接器干完活之后留下的,包括一个回指指针指向第一条plt条目,我们忽略……
关于ELF就到这里,因为……那个……我懂的也不多—_—……有兴趣的请移步到参考资料[ELFstd]
3、ptrace和Linux下调试器工作原理
呃,这个就涉及到调试器和strace、ltrace(详见它们的manpage)等东西的工作原理了……以前我一直对ltrace和gdb这种东西的强大迷惑不已——究竟它们是什么怪物,能在用户态用跟踪每个库函数调用这种方式来蹂躏别的程序呢……直到我看到[PTRACE]这篇文章—_—,原因是它用了ptrace。
具体来说呢,ptrace是Linux的系统调用之一,由于调试器这种特殊工具需要追踪别的程序甚至是查看和修改别人的内存空间(据我所知这是非常非常不礼貌的=_=),所以ptrace就有了存在滴理由……为了几个调试器提供一个系统调用,可见调试器多么重要……而用调试器的都是程序员,可见程序员多么重要……不好意思扯远了……
这里举个gdb里设置断点的例子。当你给出地址让gdb弄个断点的时候,它名义上是被调试进程的父进程,能够通过ptrace往那个进程的地址空间(包括一般情况下只读的代码区)写入数据,这时它往断点位置写入一个 int3 断点指令,然后让子程序继续运行,而自己进入wait()。
Linux的信号系统在子程序不幸遭遇到gdb填入的 int3 指令后会用 SIGCHLD 将gdb唤醒,然后gdb检查断点位置,用ptrace将自己改过的指令复原,将子进程的 %rip 退回到断点前。如果这时我们输入c让被调试程序继续运行,那个程序就会好像什么事都没发生一样继续运行了……
ltrace这边原理据说也差不多,是通过设置断点来侦测每个函数的调用情况。源码我没看过,有人看过而实现方法和这里描述的有出入的话请务必指出。我的猜测是,一个程序调用库函数的地方有那么多,不可能每处都设了断点,但是每次调用都必须经过plt和got(见上面ELF文件一节),而过了got就已经进入我们要钩的函数了。
ltrace这类工具是不可能搞代码注入的,改got 没用,所以就只剩下在plt内设置断点这个方法了(在我看来—_—)。具体请看下一节的实验……
4、实现过程
这里我们用gdb来模拟它自己背地里干的东西……而我们的目标是 bash 里的chdir调用,也就是我们要hook的东西。首先请打开一个bash会话,这里假定它的pid是10854。然后在另外一个bash会话里:
[l_amee@localhost linux]$ gdbGNU gdb Fedora (6.8-17.fc9)(版权信息……)
This GDB was configured as "x86_64-redhat-linux-gnu".
(gdb) attach 10854 # 附着到10854也就是另一个bash上面去
Attaching to process 10854
Reading symbols from /bin/bash...(no debugging symbols found)...done.
(N多其他读取symbol的提示……)
(no debugging symbols found)
0x000000321eed6590 in __read_nocancel () from /lib64/libc.so.6
Missing separate debuginfos, use: debuginfo-install bash.
x86_64(gdb) x/xg 0x418548 # 这个是上面 objdump -d 找出来的地址(见第二节),plt里chdir对应的条目
0x418548 : 0x0168002a1cb225ff # 这个记下,下面恢复用到(gdb) set *(0x418548) = 0xcc
# 低半字改为0x000000CC,即是int3断点(这里“字长”指64位)
(gdb) cContinuing.
这时被调试的 bash 会话恢复响应,进去打个 cd,cd是bash的内置命令,会调用chdir函数,然后由于碰到断点它会看起来像死掉,这证明断点设置成功了。如果是在程序里我们可以马上对函数的参数进行处理,达到hook的目的ohoho……
回到gdb:
Program received signal SIGTRAP, Trace/breakpoint trap. # gdb收到子进程停止的信号
0x0000000000418549 in chdir@plt ()(gdb) x/10xg $rsp-72 # 通过 %rsp 可以找到堆栈内容
0x7fff8f0635f0: 0x2f00000000000000 0x000000321f167a00
# 下面这一行两个地址是chdir的参数,但是chdir的manpage里面写明参数只有一个,不明白怎么回事……
0x7fff8f063600: 0x0000000002647dd0 0x000000000262a550
0x7fff8f063610: 0x0000000000000000 0x000000000000000c
0x7fff8f063620: 0x0000000000000000 0x000000321ee7a866
0x7fff8f063630: 0x0000000000000000 0x000000000045d2dd
(gdb) x/s 0x0000000002647dd00x2647dd0: "/home/l_amee" # 绝对路径,看来bash在调用chdir前就已经把路径解析了一边哦……
(gdb) x/s 0x000000000262a5500x262a550: "/home/l_amee" # 相对路径,就是你输入bash的cd参数
(gdb) set *(0x418548) = 0x0168002a1cb225ff # 恢复plt条目原来的内容
(gdb) set $pc = $pc-1
# 倒回触发断点的地址重新执行,int3的长度是一字节
(gdb) cContinuing.
这时被调试的 bash 继续照常运行……
上面整个过程就是第三节描述的截获API调用的方法,只不过素“全手工打造”滴……步骤都有了写个程序还不简单……例程代码见下节。另外貌似gdb对追踪“追踪别人的程序”不大在行—_—,所以例程如果用gdb调试的话可能工作不正常……这可以理解,毕竟没多少人会用gdb来调试gdb……
5、程序代码
这个程序用于钩住bash里的chdir调用,我的环境是Fedora9 64位版本+bash 3.2.33+gcc 4.3.0,新装的系统,发现堆栈竟然是默认向高地址增长的(因为函数参数在%rsp位置之前),如果你的机子不是这个情况可能程序要小小改一下……原理是利用上面的技巧将chdir的调用钩住,修改其参数为字符串“/hooked”,那么每次chdir调用除了这个目录外无处可去……
ps:本来想弄个给gtk界面加点“特效”的hook的,无奈有关函数太多,只好对bash下手了……
用法:ptrace_hook pid addr
其中pid是要钩住的bash会话的pid,addr是chdir的plt条目地址,可以用objdump找到(见ELF文件那一节),钩住后对应bash shell里的cd命令无法使用,而是显示类似下面的东西:
bash: cd: hooked: No such file or directory
证明钩子生效。
#define STKALN 8 /* We use this to extract info from words read from the victim */
union pltval {
unsigned long val;
unsigned char chars[ sizeof (unsigned long )];
};
void usage( char ** argv){ printf( " Usage: %s plt_posn " , argv[ 0 ]);}
void peekerror(){ printf( " Status: %sn " , strerror(errno));}
/* function to modify the two parameters used by chdir */
void mod_test(pid_t traced, void * addr1, void * addr2) {
union pltval buf;
buf.val = ptrace(PTRACE_PEEKDATA, traced, addr1, NULL);
printf( " --- mod_test: " );
peekerror();
memcpy(buf.chars, " hooked " , 6 );
buf.chars[ 6 ] = 0 ;
ptrace(PTRACE_POKEDATA, traced, addr1, buf.val);
printf( " --- mod_test: " );
peekerror();
buf.val = ptrace(PTRACE_PEEKDATA, traced, addr2, NULL);
printf( " --- mod_test: " );
peekerror();
memcpy(buf.chars, " /hooked " , 7 );
buf.chars[ 7 ] = 0 ;
ptrace(PTRACE_POKEDATA, traced, addr2, buf.val);
printf( " --- mod_test: " );
peekerror();
}
int main( int argc, char ** argv) {
pid_t traced;
struct user_regs_struct regs;
int status, trigd = 0 ;
unsigned long ppos;
union pltval buf;
unsigned long backup;
siginfo_t si;
long flag = 0 , args[ 2 ];
if (argc < 2 ) {
usage(argv);
exit( 1 );
}
traced = atoi(argv[ 1 ]);
ppos = atoi(argv[ 2 ]);
ptrace(PTRACE_ATTACH, traced, NULL, NULL);
printf( " Attach: " );
peekerror();
wait( & status);
buf.val = ptrace(PTRACE_PEEKDATA, traced, ppos, NULL);
backup = buf.val;
buf.chars[ 0 ] = 0xcc ;
ptrace(PTRACE_POKEDATA, traced, ppos, buf.val);
ptrace(PTRACE_CONT, traced, NULL, NULL);
while ( 1 ){
printf( " I'm going to wait.n " );
wait( & status);
printf( " Done waitingn " );
if (WIFEXITED(status))
break ;
ptrace(PTRACE_GETSIGINFO, traced, NULL, & si);
ptrace(PTRACE_GETREGS, traced, NULL, & regs);
if ((si.si_signo != SIGTRAP) || (regs.rip != ( long )ppos + 1 )) {
ptrace(PTRACE_GETREGS, traced, NULL, & regs);
ptrace(PTRACE_CONT, traced, NULL, NULL);
continue ;
}
printf( " Hook trigered: %ld timesn " , ++ flag);
printf( " RSP: %lxn " , regs.rsp);
int i;
for (i = 0 ; i < 2 ; i ++ ) {
args[i] = ptrace(PTRACE_PEEKDATA, traced, regs.rsp - STKALN * (i + 6 ), NULL);
printf( " Argument #%d: %lxn " , i, args[i]);
}
mod_test(traced, ( void * )args[ 0 ], ( void * )args[ 1 ]);
buf.val = backup;
ptrace(PTRACE_POKEDATA, traced, ppos, buf.val);
regs.rip = regs.rip - 1 ;
ptrace(PTRACE_SETREGS, traced, NULL, & regs);
ptrace(PTRACE_SINGLESTEP, traced, NULL, NULL);// We have to wait after each call of ptrace(),
wait(NULL);
ptrace(PTRACE_GETREGS, traced, NULL, & regs);
buf.chars[ 0 ] = 0xcc ;
ptrace(PTRACE_POKEDATA, traced, ppos, buf.val);
ptrace(PTRACE_CONT, traced, NULL, NULL);
}
return 0 ;
}
6、参考资料
1、[ELFstd]: The ELF standard, http://www.muppetlabs.com/~breadbox/software/ELF.txt
2、[PTRACE]: Playing with ptrace, 作者 Pradeep Padala, http://www.linuxjournal.com/article/6100, http://www.linuxjournal.com/article/6210
3、man ptrace
4、man objdump