0x08048371
0x08048373
0x08048376
0x08048379
0x0804837e
0x08048380
0x08048383
0x08048388
0x0804838d
0x08048390
0x08048395
0x08048396
0x08048397
End of assembler dump.
显然唯一的一条函数调用“call 0x80482b0 <_init+56>”应该就是printf,看看0x80482b0处有什么?
(gdb) x/10i 0x80482b0
0x80482b0 <_init+56>: jmp *0x8049570
0x80482b6 <_init+62>: push $0x8
0x80482bb <_init+67>: jmp 0x8048290 <_init+24>
0x80482c0 <_start>: xor %ebp,%ebp
0x80482c2 <_start+2>: pop %esi
0x80482c3 <_start+3>: mov %esp,%ecx
0x80482c5 <_start+5>: and $0xfffffff0,%esp
0x80482c8 <_start+8>: push %eax
0x80482c9 <_start+9>: push %esp
0x80482ca <_start+10>: push %edx
原 来是一条跳转指令,要跳到的地址被保存在0x8049570处,按照我们对PE文件的经验0x8049570处一定存放着printf的真正地址。系统加 载hello时从重定位表和动态表中知道我们引用了libc.so.6中的printf,然后它就加载libc.so.6,然后它就从libc.so.6 的符号表中找到了printf,然后它就把hello的0x8049570处的四个字节改成printf的地址!一定是这样!那么这一条跳转就是去执行 printf了。看看再说。
(gdb) x/4xw 0x8049570
0x8049570 <_GLOBAL_OFFSET_TABLE_+16>: 0x080482b6 0x00000000 0x00000000 0x0804948c
“0x080482b6” 这不是紧挨着跳转指令的下一条指令地址吗?怎么不是printf!Linux与Windows是不同的,《ELF规范》把0x80482b0处的这段东西 叫做过程连接表(PLT)又把0x8049570处的东西称为全局偏移表(GOT),这两个概念我们不过多引入。请大家注意0x080482b6开始的那 条push指令,它把0x8压入堆栈。0x8就我们的关键!我们退出GDB使用我们自己前面写下的工具。先打印符号表,由于内容太多我只把动态符号表中的 情况摘抄在下面,动态符号表是类型为SHT_DYNSYM节。
.dynsym(type=SHT_DYNSYM)
offset=0x174, vaddr=0x8048174, size=0x60
Sym:
type=0(STT_NOTYPE) attrib=0
st_shndx=0, st_value=0x0(0), st_size=0
Sym:__libc_start_main
type=2(STT_FUNC) attrib=1
st_shndx=0, st_value=0x0(0), st_size=239
Sym:printf
type=2(STT_FUNC) attrib=1
st_shndx=0, st_value=0x0(0), st_size=57
Sym:_IO_stdin_used
type=1(STT_OBJECT) attrib=1
st_shndx=14, st_value=0x8048468(134513768), st_size=4
Sym:_Jv_RegisterClasses
type=0(STT_NOTYPE) attrib=2
st_shndx=0, st_value=0x0(0), st_size=0
Sym:__gmon_start__
type=0(STT_NOTYPE) attrib=2
st_shndx=0, st_value=0x0(0), st_size=0
可以看到动态符号表共有六项,除第1项保留不用,共引入5个外部符号,其中两个是函数,printf位于第3项。再打印重定位表:
.rel.dyn(type=SHT_REL)
offset=0x260, vaddr=0x8048260, size=0x8
type=6(R_386_GLOB_DAT) SYM=5 offset=0x804955c
.rel.plt(type=SHT_REL)
offset=0x268, vaddr=0x8048268, size=0x10
type=7(R_386_JMP_SLOT) SYM=1 offset=0x804956c
type=7(R_386_JMP_SLOT) SYM=2 offset=0x8049570
在 重定位表中请注意名为“.rel.plt”的重定位节,这个重定位表中只有两项,它们的重定位类型都是R_386_JMP_SLOT,它们与动态符号表中 仅有的两个函数一一对应。第一个的符号表索引是1,对应着__libc_start_main,我们不关心;第二个符号表索引是2,恰与printf对 应,我们看到它的r_offset字段的值是0x8049570,这个地址正好保存着printf要跳转到的地址。重定位信息告诉操作系统要修改这个地方 可是系统并没修改,修改任务于是就必须由0x80482b6处的指令来完成。0x8——这个压入栈中的数值就我们的关键。重定位表中每个结构的大小恰是8 个字节,于是你大胆猜测这个0x8就是外部函数的重定位信息在重定位表中的偏移量。这个猜测可以通过引入多个不同的函数加以验证。push指令后的跳转可 认为是去调用一个函数,而push本身仅是向那个函数传递这个参数罢了,而那个函数一定会找到printf并调用它。那么为什么要如此大费周折呢,我想有 些人一定猜到了结果:那个用重定位表偏移做参数的函数一定在得到printf地址后随后修改了0x8049570处保存的值,下一次再调用printf时 就会直接由0x80482b0处跳到真正的函数体内了——怎么有点像Win9x中“VXD CALL”。通过引入多个外部函数我们又发现:多个外部函数在“.rel.plt”中的排列顺序与它们对应内容在GOT中的排列顺序完全一致,不同的是它 们不是从GOT的偏移0开始的。
继续我们的实验,重新用GDB打开hello,在0x804838d处下一个断点,它将使程序在调用printf的语句之后停止不动。然后用r命令执行hello。
(gdb) b *0x804838d
Breakpoint 1 at 0x804838d
(gdb) r
经过几行输出,GDB最后打印了一条信息“Breakpoint 1, 0x0804838d in main ()”等待我们的命令。我们再来看一下0x8049570处的内容,它果然变了,由0x080482b6变成了0x005d62a0。这恰是printf 的真正地址,我们还能看到printf还调用了vfprintf。
(gdb) x/4x 0x8049570
0x8049570 <_GLOBAL_OFFSET_TABLE_+16>: 0x005d62a0 0x00000000 0x00000000 0x0804948c
(gdb) disass printf
Dump of assembler code for function printf:
0x005d62a0
0x005d62a1
0x005d62a3
0x005d62a6
0x005d62a9
0x005d62ac
0x005d62af
0x005d62b4
0x005d62ba
0x005d62be
0x005d62c4
0x005d62c8
0x005d62ca
0x005d62cd
0x005d62d2
0x005d62d5
0x005d62d7
0x005d62d8
0x005d62d9
0x005d62da
0x005d62db
0x005d62dc
0x005d62dd
0x005d62de
0x005d62df
End of assembler dump.
(gdb)
实验内容就这么多,更进一步的细节我宁愿当它是一个黑盒,根据参数实现功能。
有 一点我需要再三重复。在ELF文件中没有信息把printf与libc.so.6联系在一起,也就是说加载程序不知道printf的函数体在 libc.so.6中。所以加载程序只能根据DT_NEEDED类型的动态结构加载所有程序需要的so模块,然后在所有so模块中寻找printf。这也 是符号结构要带有“绑定类型”信息的原因。这样的连接方式有些好处。例如PE中没有使用的“懒模式”,不管程序执行过程中是否会用到,PE文件执行之前它 引入的所有外部函数都必须被系统解析出来,而Linux下正如刚才看到的用到时才会解析。“懒模式”有它的危害,如果用到时才加载so模块,必然使程序的 运行不够流畅,所以Linux一次性加载所有模块而用时解析函数应该是对此问题的解决方案。没有把模块与函数绑定在一起也使Linux的驱动开发者受益, 可加载模块能够引用内核符号并能导出符号供其它模块使用。
希望大家看完文章后会下载我写的小程序,然后帮我发现BUG(它需要大量的测试而我又不能满足它):hdasm64
本身是一个Win32程序(PE32格式)在下载的包里有64位的PE和ELF文件各一个,32位ELF文件一个,供大家研究!
支持实模式、保护模式、64位模式三种模式指令集的动态反汇编. 支持DOS系统COM和EXE可执行文件格式,支持32位和64位PE文件格式(windows可执行文件),支持32位和64位ELF文件格式(Linux可执行文件),共计六种文件格式。支持部分指令的虚拟执行调试。
下载页面:
作者邮箱:hangj@zhongsou.com