上一篇分析ELF格式的文件中忽然发现ELF文件的导入表、重定位没有涉及到;
还发现在ida在显示ELF的导入表时没有显示导入函数所在的库文件
并没有显示_libc_start_main( )、puts( )函数所在的库文件,感觉ELF文件的重定位好神秘。
在linux系统中可以使用ldd [目标文件]命令显示目标文件依赖的共享库文件,用上次文章中的test为例:
显示依赖3个文件:
linux-gate.so.1文件 可以看到加载地址属于linux内核空间,整个文件系统中也搜索不到这个文件(慢慢搞明白这个文件作用吧)
/lib/ld-linux.so.2文件 是一个软连接,指向动态连链接器,动态链接器在操作系统加载完可执行文件后,接过控制权从自身的入口地址(动态链接器也是一个共享库)开始一系列自身的初始化工作,并对可执行文件进行动态链接,完成之后把控制权交给可执行文件的入口地址,ELF程序正式执行。这个动态链接器其实也是就是.interp节区的那个字符串:
/lib/i386-linux-gnu/libc.so.6文件 也是一个软连接,连接到同目录下的libc-2.23.so文件,用ida打开可以看到里面导出了_libc_start_main( )、puts( )函数
导入函数的位置找到了,那么ELF文件中是如何定义导入表的呢?
ELF文件规定:
.got节区用来保存全局变量的地址值
.got.plt用来保存导入函数的地址值
.rel.dyn存放对全局变量的修正信息
.rel.plt存放对导入函数的修正信息
先看一下.rel.dyn、.rel.plt节区的内容:
对于R_386_JUMP_SLOT类型,只需要把函数地址直接填入对应内存即可(其他的重定位类型有不同的地址计算方法)
可以看到在内存偏移0x0804a00c存放着puts( )函数地址、偏移0x0804a010存放着_libc_start_main( )函数地址
再看一下ida中显示的内存地址(前12个字节用于存放其他结构),这2个内存地址处确实是存放着对应函数的地址值:
我们再看一下文件中.got.plt节区的内容:
有没有注意到内容不一样,为什么???
因为ELF文件引用的外部函数使用了延迟绑定,具体代码流程如下:
.text 0x0804832c: call 0x80482f0 <__libc_start_main@plt>
.plt 0x080482f0: jmp DWORD PTR ds:0x804a010
.plt 0x080482f6: push 0x8 序号
.plt 0x080482fb: jmp 0x80482d0
gdb中无法反汇编的代码
.plt 0x80482d0: push DWORD PTR ds:0x804a004 模块ID
.plt 0x80482d6: jmp DWORD PTR ds:0x804a008 _dl_runtime_resolve()函数地址
因为使用了延迟绑定第一次调用_libc_start_main( )函数时,会转到.plt 0x080482f0: jmp DWORD PTR ds:0x804a010指令处,由于是第一次调用0x0x804a010内存处应该是文件中的原始值0x080482F6(这样就一致了);
后面4条指令在ida中不显示(这4条指令出的值应该是动态链接器接过控制权时修改的),最后2条在gdb中无法反汇编,不过还好有大神解释:
调用 _dl_runtime_resolve( )函数,结合入栈的模块ID和序号中找到_libc_start_main( )函数地址,调用并把这个地址填入0x804a010内存处(覆盖掉源文件中的初始值0x080482F6,和IDA显示的值就对应了),这样第二次调用_libc_start_main( )函数时,直接就跳到函数处,不用再用_dl_runtime_resolve( )函数寻找了。
贴上gdb调试的代码:
0x080482d0处内存反汇编失败,只能在od中修改二进制值查看代码了(唉!能用的办法都用上了):
最后,如果要在linux下显式调用外部函数、变量,需要使用以下4个函数:
void * dlopen( const char * pathname, int mode);
void * dlsym(void * handle,const char * symbol);
char * dlerror(void); // 返回错误信息,成功返回NULL
int dlclose (void *handle);