一步一步走进Linux HOOK API(五)
这一讲中,我们不在继续进入研究动态符号表的获取了,今天咱们来点实战的话题,对于前面我们研究的那么多知识,做一次总结.来看看这些节表之间是怎么联系起来的.最后我们将经过一个简单的拦截printf函数,修改它的参数,来输出我指定的字符串,这样就达到简单的hook api的目的.好了废话不多说.先看看main.c里面的代码.
这段代码很简单,敲一个回车就输出一个hello,我们的目的就是实现,敲一个hello让他输出我们指定的字符串.
首先先运行这个main程序.在另一个终端里面输入ps -a 查看main 的进程ID.一会我们用GDB手动修改的时候要attach 连接他的进程 ID.
$ ps -a
PID TTY TIME CMD
2068 pts/0 00:00:00 sudo
2069 pts/0 00:00:00 su
2077 pts/0 00:00:00 bash
2174 pts/0 00:00:00 dbus-launch
4803 pts/2 00:00:00 sudo
4805 pts/2 00:00:00 su
4813 pts/2 00:00:00 bash
5282 pts/1 00:00:00 linux_server
5774 pts/2 00:00:00 man
5784 pts/2 00:00:00 pager
6256 pts/4 00:00:00 sudo
6257 pts/4 00:00:00 su
6265 pts/4 00:00:00 bash
6275 pts/4 00:00:00 main
6276 pts/3 00:00:00 ps
这里是获取main 程序的进程空间的映射,找到未被使用的空间来写入我们指定的字符串,这里使用的指令是:
cat /proc/6275/maps
00258000-00274000 r-xp 00000000 08:01 918417 /lib/i386-linux-gnu/ld-2.13.so
00274000-00275000 r--p 0001b000 08:01 918417 /lib/i386-linux-gnu/ld-2.13.so
00275000-00276000 rw-p 0001c000 08:01 918417 /lib/i386-linux-gnu/ld-2.13.so
0058f000-00590000 r-xp 00000000 00:00 0 [vdso]
00cf6000-00e50000 r-xp 00000000 08:01 918430 /lib/i386-linux-gnu/libc-2.13.so
00e50000-00e51000 ---p 0015a000 08:01 918430 /lib/i386-linux-gnu/libc-2.13.so
00e51000-00e53000 r--p 0015a000 08:01 918430 /lib/i386-linux-gnu/libc-2.13.so
00e53000-00e54000 rw-p 0015c000 08:01 918430 /lib/i386-linux-gnu/libc-2.13.so
00e54000-00e57000 rw-p 00000000 00:00 0
08048000-08049000r-xp00000000 08:01 1158586 /home/Linux_Project/ELF/Hello/main
08049000-0804a000rw-p0000000008:01 1158586 /home/Linux_Project/ELF/Hello/main
b7723000-b7724000 rw-p 00000000 00:00 0
b7733000-b7736000 rw-p 00000000 00:00 0
bfa4c000-bfa6d000 rw-p 00000000 00:00 0 [stack]
这里我们选用00e54000-00e57000 rw-p 00000000 00:00 0 在下面我们会向这个地址空间.写入我们需要的字符串.
这里我们上面只是做了个提前预报会使用这个来获取未使用空间,下面我们开始通过手动方式来拦截printf这个函数的参数.
启动我们的调试器GDB
# gdb
GNU gdb (Ubuntu/Linaro 7.2-1ubuntu11) 7.2
Copyright (C) 2010 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "i686-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
通过attach之类依附到已经运行的进程中去,这里我们依附到main程序中去.
(gdb) attach 6275
Attaching to process 6275
Reading symbols from /home/Linux_Project/ELF/Hello/main...(no debugging symbols found)...done.
Reading symbols from /lib/i386-linux-gnu/libc.so.6...(no debugging symbols found)...done.
Loaded symbols for /lib/i386-linux-gnu/libc.so.6
Reading symbols from /lib/ld-linux.so.2...(no debugging symbols found)...done.
Loaded symbols for /lib/ld-linux.so.2
0x0058f416 in __kernel_vsyscall ()
首先万事都是从main开始,所以先查看main函数处的代码,距离我们的目标又近了一步,看到printf函数的调用的地方了.根据源代码的分析,这里是唯一一个使用printf的函数调用的地方.
(gdb) disassemble main
Dump of assembler code for function main:
0x08048404 <+0>: lea 0x4(%esp),%ecx
0x08048408 <+4>: and $0xfffffff0,%esp
0x0804840b <+7>: pushl -0x4(%ecx)
0x0804840e <+10>: push %ebp
0x0804840f <+11>: mov %esp,%ebp
0x08048411 <+13>: push %ecx
0x08048412 <+14>: sub $0x4,%esp
0x08048415 <+17>: call 0x80482e0 <getchar@plt>
0x0804841a <+22>: movl $0x8048510,(%esp)
0x08048421 <+29>: call 0x8048320 <printf@plt>
0x08048426 <+34>: mov 0x8049620,%eax
0x0804842b <+39>: mov %eax,(%esp)
0x0804842e <+42>: call 0x8048310 <fflush@plt>
0x08048433 <+47>: jmp 0x8048415 <main+17>
End of assembler dump.
根据ELF文件格式,printf跳转的地址是PLT表内,所以查看PLT内的printf地址处的代码,可见这里jmp跳转的地址就是根据前面介绍的GOT表了.那么理论上,就应该跳转到printf函数内去执行了.但是其实不是的.linux采用了一个叫"懒模式"的加载方式,当某个函数第一次被调用,它才会把真正的地址放到GOT表内去.
(gdb) x/10i 0x8048320
0x8048320 <printf@plt>: jmp *0x8049618
0x8048326 <printf@plt+6>: push $0x20
0x804832b <printf@plt+11>: jmp 0x80482d0
0x8048330 <_start>: xor %ebp,%ebp
0x8048332 <_start+2>: pop %esi
0x8048333 <_start+3>: mov %esp,%ecx
0x8048335 <_start+5>: and $0xfffffff0,%esp
0x8048338 <_start+8>: push %eax
0x8048339 <_start+9>: push %esp
0x804833a <_start+10>: push %edx
所以我们查看下GOT表内的数据,发现这个地址很熟悉,这个0x08048326不正是plt表内的JMP下面的地址嘛.由此可见,当第一次加载函数的时候,其实GOT表内还没有得到真正的函数地址,而是返回去支持PLT表内下面的之类,push跟jmp 然后push会把printf函数的相关偏移数据送入栈,jmp去寻找这个函数的地址,然后修改got表对应的地址处的值,这样,当下次再调用printf的时候,那么GOT表内才是printf真正的函数地址
(gdb) x/10xw 0x8049618
0x8049618 <_GLOBAL_OFFSET_TABLE_+28>: 0x08048326 0x00000000 0x00e534e0 0x00000000
首先我们对plt表内的0x8048320地址下一个断点
(gdb) x/10b 0x8048320
0x8048320 <printf@plt>: 0xcc 0x00 0x00 0x00 0x04 0x08 0x68 0x20
0x8048328 <printf@plt+8>: 0x00 0x00
然后让程序继续执行.
(gdb) c
Continuing.
当你在main程序内输入一个回车的时候,GDB就会收到信号通知,表示我们下的断点起作用了.程序调用了printf函数.
Program received signal SIGTRAP, Trace/breakpoint trap.
0x08048321 in printf@plt ()
这里我们先恢复被修改的值,改回原来的指令
(gdb) set *(0x8048320)=0x961825ff
这里查看下eip的值,把eip指向0x8048320 <printf@plt>: jmp *0x8049618
这条指令
(gdb) info register eip
eip 0x8048321 0x8048321 <printf@plt+1>
(gdb) set $eip=0x8048320
(gdb) info register eip
eip 0x8048320 0x8048320 <printf@plt>
(gdb) x/10b 0x8048320
0x8048320 <printf@plt>: 0xff 0x25 0x18 0x96 0x04 0x08 0x68 0x20
0x8048328 <printf@plt+8>: 0x00 0x00
接着.我们重新说明下我们此行的目的,就是为了修改printf的参数地址,使其志向我们给定的字符串地址,用于替换它原本的输出字符串.那么首先得查看下他的栈内的参数情况.
(gdb) info register esp
esp 0xbfa6b0ac 0xbfa6b0ac
我们发现当前的esp指向的是0xbfa6b0ac 继续查看0xbfa6b0ac 处的数据
(gdb) x/10xw 0xbfa6b0ac
0xbfa6b0ac: 0x08048426 0x08048510 0xbfa6b0d0 0xbfa6b148
0xbfa6b0bc: 0x00d0ce37 0x08048450 0x00000000 0xbfa6b148
0xbfa6b0cc: 0x00d0ce37 0x00000001
0xbfa6b0ac = 0x08048426
0x08048426 熟悉不????这不就是我们main函数里面的printf下面的一条指令嘛,那么这应该就是call调用的时候留下的call指令反弹地址.调用完printf之后返回的指令地址,那么再查看0xbfa6b0b0处的值0x08048510也刚好就是我们printf参数的地址.
这样一来,我们的目的就很明确了修改0xbfa6b0ac 地址处的值,指向我们新的字符串地址,那么任务就完成了.
首先找到被调试程序的内存内是否有空余的空间,这个在前面已经讲过了.这里我们选用0x08048510这个内存空间.如下,添加了字符串(我不太熟悉gdb只好用笨方法添加了)
先修改参数地址.
(gdb) set *(0xbfa6b0b0)=0x00e54000
(gdb) x/10xw 0xbfa6b0ac
0xbfa6b0ac: 0x08048426 0x00e54000 0xbfa6b0d0 0xbfa6b148
0xbfa6b0bc: 0x00d0ce37 0x08048450 0x00000000 0xbfa6b148
0xbfa6b0cc: 0x00d0ce37 0x00000001
写入字符串
(gdb) set *(0x00e54000)=0x62696c2f
(gdb) set *(0x00e54000)=0x2d646c2f
(gdb) set *(0x00e54000)=0x62696c2f
(gdb) set *(0x00e54004)=0x2d646c2f
(gdb) set *(0x00e54008)=0x756e696c
(gdb) set *(0x00e5400c)=0x6f732e78
(gdb) set *(0x00e54010)=0x0000322e
(gdb) x/s 0xe54000
0xe54000: "/lib/ld-linux.so.2"
(gdb) c
Continuing.
# ./main
/lib/ld-linux.so.2注意这里,已经输出了我们指定的字符串,初步已经达到了我们的目的.
设想.如果我们写入一段修改esp内printf函数参数的汇编代码嵌入到对方进程内,然后修改PLT的第一条指令,使其跳至我们的代码里,然后再修改完之后跳回GOT表内,那么main这个程序就永远不会打印hello字符串了.
好了,就说到这了.教程写的不是太好.我已经尽量将我能表达的都表达到了.要是有那里不太清楚了,希望大家可以留言,我会第一时间给你答复的.
我也是菜鸟.大家一起努力...
谢谢