Linux逆向---ELF动态链接

ELF动态链接

静态链接通过将整个库都编译到可执行文件的方式来生成可执行文件,而动态链接则利用共享库来实现可执行文件对共享库中函数的调用,在执行时将共享库加载并绑定到该进程的地址空间中。

1.事前准备

由于要探究的是共享库,所以我们需要实现一个共享库文件:

首先是头文件:

add.h:

#ifndef ADD_H
#define ADD_H
int add(int a,int b);
#endif

然后是实现文件:

add.c:

#include "add.h"
int add(int a,int b)
{
    return a+b;
}

最后编译一波生成.so文件:

gcc -shared -fPIC add.c -o libadd.so

这里shared参数说明要生成一个共享库。

PIC意为position independent code,意思是说生成的代码中没有绝对地址,全部是相对地址,这也是为了共享库的通用性而加的。

这样就生成了一个只有函数的共享库。

可以用readelf -h libadd.so查看一下ELF头:

ELF 头:
.......
  类型:                              DYN (共享目标文件)

可以看到类型是共享目标文件,使用readelf -l 查看它的段的话也会发现没有INTERP段,因为它不需要程序解释器。

接下来编写一个简单的a+b程序调用一下这个共享库,为了防止其他共享库造成影响,这里并没有IO过程:

test.c:

#include "add.h"
int main(){

    int a=1,b=2;
    int c=add(a,b);
    return 0;
}

然后需要进行编译,这里需要干两件事,第一件是强制程序到当前的目录去找库文件,第二件事就是编译,命令如下:

export LD_LIBRARY_PATH=.
gcc test.c -L. -l add

然后当前目录就可以生成一个a.out。

当一个共享库被加载到一个进程的地址空间中时,动态链接器会修改可执行文件中的全局偏移表GOT,从而达到让可执行文件访问库函数的目的,而由于GOT会被修改,所以它位于数据段中,也就是.got.plt节,如下所示:

0000000000400420 <.plt.got>:
  400420:	ff 25 d2 0b 20 00    	jmpq   *0x200bd2(%rip)        # 600ff8 <_DYNAMIC+0x1d0>
  400426:	66 90                	xchg   %ax,%ax

这个0x600ff8也处于数据段中。

2.辅助向量

这一节读完之后也不是很懂这个辅助向量是干嘛的,需要读完PLT/GOT这一节,就能够理解这个辅助向量的用处了。

通过系统调用sys_execve()将程序加载到内存中时,对应的可执行文件会被映射到内存的地址空间,并为该进程的地址空间分配一个栈。这个栈会用特定的方式向动态链接器传递信息。这种特定的对信息的设置和安排即为辅助向量。栈底存放了如下信息:

Auxilary
environ
argv
Stack

辅助向量的项目满足如下结构:

typedef struct{
    uint64_t a_type;
    union{
        uint64_t a_val;
    } a_un;
}Elf64_auxv_t;

a_type指定了辅助向量的条目类型,a_val为辅助向量的值。

辅助向量是由内核函数create_elf_tables()设定的,该内核函数在Linux的源码/usr/src/linux/fs/binfmt_elf.c中

  1. sys_execve()
  2. 调用do_execve_common()
  3. 调用search_binary_handler()
  4. 调用load_elf_binary()
  5. 调用create_elf_tables()

程序被加载进内存,辅助向量被填充好以后,控制权就交给了动态链接器,它会解析要链接到进程地址空间的用于共享库的符号和重定位。

使用ldd命令可以查看一个可执行文件所依赖的共享库列表。

$ ldd a.out 
	linux-vdso.so.1 =>  (0x00007ffdb7354000)
	libadd.so => ./libadd.so (0x00007f78f0f4a000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f78f0b80000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f78f114c000)

3.PLT/GOT

当一个程序调用共享库中的函数时,需要到程序运行时才能解析这些函数调用。这里我实验的时候和书上的例子不太一样。。大概是因为书上的是32位而我的是64位,不过表达的意思都差不多。

来看我们之前准备好的例子:

使用objdump -d a.out,看main函数中的内容:

  4006a2:	89 d6                	mov    %edx,%esi
  4006a4:	89 c7                	mov    %eax,%edi
  4006a6:	e8 b5 fe ff ff       	callq  400560 <add@plt>

可以看到这里调用了地址为0x400560的一个函数add,也就是我们库中实现的函数,然后来看0x400560对应的内容:

0000000000400560 <add@plt>:
  400560:	ff 25 b2 0a 20 00    	jmpq   *0x200ab2(%rip)        # 601018 <_GLOBAL_OFFSET_TABLE_+0x18>
  400566:	68 00 00 00 00       	pushq  $0x0
  40056b:	e9 e0 ff ff ff       	jmpq   400550 <_init+0x28>

可以看到这里有一个跳转到0x601018中的地址的指令,这个地址就是GOT条目,存储着共享库中函数add的实际地址。

`动态链接器使用默认的延迟链接方式时,不会在函数第一次调用时就对地址进行解析。延迟链接意味着动态链接器不会在程序加载时解析每一个函数,而是在调用时通过.plt和.got.plt节来对函数进行解析。可以通过修改LD_BIND_NOW环境变量将链接方式修改为严格加载,以便在程序加载的同时进行动态链接。但是延迟链接能够提高性能。

这里先看一下add函数的重定位条目:

$ readelf -r a.out 
......
000000601018  000200000007 R_X86_64_JUMP_SLO 0000000000000000 add + 0
......

可以看到,重定位的偏移地址为0x601018,跟add函数PLT的跳转地址相同。动态链接器需要对add的地址进行解析,并把值存入add的GOT条目中,看一下测试程序的GOT==(这个0x18应该对应的就是0x601018,也就是说这个GOT应该为0x601000)==:

_GLOBAL_OFFSET_TABLE_+0x18>
  400566:	68 00 00 00 00       	pushq  $0x0
  40056b:	e9 e0 ff ff ff       	jmpq   400550 <_init+0x28>

这个0x0实际上是第4个GOT条目,即GOT[3],共享库中的地址并不是从GOT[0]开始的,而是从GOT[3]开始的,前三个条目有其他的作用:

  • GOT[0] 存放了指向可执行文件动态段的地址,动态链接器利用该地址提取动态链接相关的信息。
  • GOT[1] 存放link_map结构的地址,动态链接器利用该地址来对符号进行解析。
  • GOT[2] 存放了指向动态链接器_dl_runtime_resolve()函数的地址,该函数用来解析共享函数的实际符号地址。

这里如果把_GLOBAL_OFFSET_TABLE+0x18当做这个0x0(GOT[3])会好理解很多

它的最后一条指令是jmpq 0x400550,那么我们来看一下这个地址的指令:

0000000000400550 <add@plt-0x10>:
  400550:	ff 35 b2 0a 20 00    	pushq  0x200ab2(%rip)        # 601008 <_GLOBAL_OFFSET_TABLE_+0x8>
  400556:	ff 25 b4 0a 20 00    	jmpq   *0x200ab4(%rip)        # 601010 <_GLOBAL_OFFSET_TABLE_+0x10>
  40055c:	0f 1f 40 00          	nopl   0x0(%rax)

由于64位系统一个单位为8个字节,则0x10/8=2,第一条指令将GOT[1]的地址压入栈中,jmpq 0x601010则跳转到第3个GOT条目,即GOT[2],在GOT[2]中存放了动态链接器函数的地址,对函数add进行解析后,后续所有对PLT条目add的调用都会跳转到add的代码本身,而不是重新指向PLT。

这个GOT[1]的地址相当于_GLOBAL_OFFSET_TABLE+0x08,也就是GOT[3-2+1]=GOT[1],GOT[2]则为_GLOBAL_OFFSET_TABLE+0x10,也就是GOT[3-1+1]=GOT[2],其为程序解释器的地址。这里压入的栈我觉得可以联系到前面提到的辅助向量。

4.动态段

动态链接器需要在程序运行时引用段,动态段需要相关的程序头。

动态段保存了一个如下结构体组成的数组:

这里有必要对下列的结构体成员类型进行一下解释,以下信息来自ELF手册:

ElfN_Addr       Unsigned program address, uintN_t
ElfN_Off        Unsigned file offset, uintN_t
ElfN_Section    Unsigned section index, uint16_t
ElfN_Versym     Unsigned version symbol information, uint16_t
Elf_Byte        unsigned char
ElfN_Half       uint16_t
ElfN_Sword      int32_t
ElfN_Word       uint32_t
ElfN_Sxword     int64_t
ElfN_Xword      uint64_t

数组如下:

typedef struct {
	Elf64_Sxword    d_tag;
	union {
		Elf64_Xword d_val;
		Elf64_Addr  d_ptr;
	} d_un;
} Elf64_Dyn;

d_tag字段保存了类型的定义参数

  • DT_NEEDED 保存了所需的共享库名的字符串偏移表
  • DT_SYMTAB 动态表的地址,对应的节名.dynsym
  • DT_HASH 符号散列表的地址,对应的节名.gnu,hash

也就是说,可以通过对这一段的解读,找到.dynsym等节,这样不用节头只用程序头也可以找出这些节。于是在缺少节头表的情况下也可以通过这一段重建部分节头表。

d_val成员保存了一个整型值,可以存放各种不同的数据,如条目大小。

p_ptr保存了一个内存虚址,可以指向链接器需要的各种类型的地址,如d_tag DT_SYMTAB符号表的地址。

p_val与p_ptr位于一个联合体,也就是说这个结构体的第二个成员有可能是一个地址也可能是一个数值。

动态链接器利用d_tag来定位动态段的不同部分,每一部分都通过d_tag保存了指向某部分可执行文件的引用,对应的d_prt给出了指向该符号表的虚址。

动态链接器映射到内存中时,首先会处理自身的重定位,因为链接器本身就是一个共享库。接着会查看可执行程序的动态段并查找DT_NEEDED参数,该参数保存了指向所需要的共享库的字符串或者路径名。当一个共享库被映射到内存之后,链接器会获取到共享库的动态段,并将共享库的符号表添加到符号链中,符号链存储了所有映射到内存中的共享库的符号表。

链接器为每个共享库生成一个link_map结构的条目,并将其存到一个链表中:

struct link_map{
    ElfW(Addr) l_addr;	//共享对象的基址
    char *l_name;	//对象的绝对文件名
    ElfW(Dyn) *l_ld;	//共享对象的动态节
    struct link_map *l_next,*l_prev;
}

这个link_map应该就是GOT[1]中存储的内容

链接器构建完依赖列表之后,会挨个处理每个库的重定位,同时会补充每个共享库的GOT。延迟链接对共享库的PLT/GOT仍然适用,因此,只有当一个函数真正被调用时,才会进行GOT重定位。

为了理解这一段,我进行了一些小实验。

4.1.可执行文件

这里假设链接器已经完成了自身的重定位,首先我们关注可执行文件:

我们可以用readelf -d命令查看动态段的项:

$readelf -d a.out
Dynamic section at offset 0xe18 contains 25 entries:
  标记        类型                         名称/值
 0x0000000000000001 (NEEDED)             共享库:[libadd.so]
 0x0000000000000001 (NEEDED)             共享库:[libc.so.6]
...
 0x0000000000000003 (PLTGOT)             0x601000
...
 0x0000000000000000 (NULL)               0x0

这里我们看到了PLTGOT偏移表的地址为0x601000,也与我们之前的猜想相同,我们同样可以看到第一条就写出了共享库libadd.so,那么这个值是如何得来的呢?

我们先查看一下a.out程序头的情况:

  DYNAMIC        0x0000000000000e18 0x0000000000600e18 0x0000000000600e18
                 0x00000000000001e0 0x00000000000001e0  RW     8

首先我们可计算出Elf64_Dyn结构体的大小为8*2=16字节,DYNAMIC中共有0x1e0个字节,也就是0x1e0/16=30项,这与之前的25项似乎有点出入,不过可以看之前打印出来的最后一条是NULL,说明之后的项目不再有意义,也就是说一共只有24项有实际含义,第25项表示结束,往后的都再无意义了。

接下来我们需要搞清楚这个libadd.so是如何得到的,为了搞清楚这个,我们来查看一波从0xe18开始的前两条的情况:

									 01 00 00 00  00 00 00 00  ................
00000E20   01 00 00 00  00 00 00 00  01 00 00 00  00 00 00 00  ................
00000E30   74 00 00 00  00 00 00 00

那么第一个结构体的d_tag值为1,d_val的值为1,第二个结构体d_tag值也为1,d_val的值为0x74,接下来我们查看一下源码中的.dynstr节所在的地址:

$ readelf -S a.out 
  [ 6] .dynstr           STRTAB           00000000004003f0  000003f0
       00000000000000b4  0000000000000000   A       0     0     1

知道了是0x3f0这个地址之后,使用hexedit a.out去找这个地址对应的内容:

000003F0   00 6C 69 62  61 64 64 2E  73 6F 00 5F  49 54 4D 5F  .libadd.so._ITM_
...
00000460   69 6E 69 00  6C 69 62 63  2E 73 6F 2E  36 00 5F 5F  ini.libc.so.6.__
...

到这里我们就能够明白libadd.so和libc.so.6的来历了,因为这个d_val代表的是偏移量,第一个结构体的偏移量是1,也就指向了这里的libadd.so,第二个结构体的偏移量是0x74,0x3F0+0x74=0x464,也就指向了这个libc.so.6。

那么现在链接器成功读取了共享库的名称,并成功将其映射到内存中了,下一步就是获取共享库的动态段了。

4.2.共享库文件

首先我们观察共享库的动态段:

$readelf -l libadd.so
Dynamic section at offset 0xe48 contains 21 entries:
  标记        类型                         名称/值
 0x0000000000000005 (STRTAB)             0x368
 0x0000000000000006 (SYMTAB)             0x230

可以知道符号表的起始地址为0x230,(通过对共享库的节头表各项地址的查看,可以知道这里的符号表指的是动态符号表)

接下来我们用readelf直接看一下符号表中的各项内容:

$ readelf -s libadd.so 
Symbol table '.dynsym' contains 13 entries:
......
     8: 0000000000201028     0 NOTYPE  GLOBAL DEFAULT   22 _end
     9: 0000000000000650    20 FUNC    GLOBAL DEFAULT   11 add
    10: 0000000000201020     0 NOTYPE  GLOBAL DEFAULT   22 __bss_start
......

这里第8项就是我们一直想要找的add函数的实际地址,这里表达的意思是在偏移量650的位置,于是我们再用objdump查看一下650是不是add函数的位置:

$ objdump -d libadd.so  
.......
0000000000000650 <add>:
 650:	55                   	push   %rbp
 651:	48 89 e5             	mov    %rsp,%rbp
.......

可以看出,这里的确是真正的add函数的位置,这也是最终调用函数的地址,到这里,可执行文件能够获得真正的函数地址,也可以进一步调用这个函数了。

所以总结一下整个过程,就是可执行文件加载时,首先把GOT之类的东西压入辅助向量,然后将控制权交给动态链接器,它把真实地址从共享库中找出来,然后替换GOT中的值,这样可执行文件调用共享库函数的时候,就可以访问真实的函数入口了。

  • 4
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
提供的源码资源涵盖了Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 适合毕业设计、课程设计作业。这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。 所有源码均经过严格测试,可以直接运行,可以放心下载使用。有任何使用问题欢迎随时与博主沟通,第一时间进行解答!
提供的源码资源涵盖了小程序应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 适合毕业设计、课程设计作业。这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。 所有源码均经过严格测试,可以直接运行,可以放心下载使用。有任何使用问题欢迎随时与博主沟通,第一时间进行解答!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值