聊聊动态链接和dl_runtime_resolve
写在前面
linux下的动态链接相关结构,重新回顾_dl_runtime_resolve的流程以及利用方法
动态链接相关结构
为了高效率的利用内存,多个进程可以共享代码段、程序模块化方便更新维护等,动态链接技术自然就出现了。不详细介绍位置无关代码和位置无关可执行程序这些基本知识,这里着重记录一下ELF实现运行时重定位为了提高效率做的各种工作和用到的结构。动态链接的可执行文件装载过程和静态链接基本一样,OS读取可执行文件的头部信息,检查文件合法性后从Program Header中读取每一个Segment的虚拟地址、文件地址和属性,然后把他们映射到进程虚拟空间的相对位置。但是OS接下来不能把控制权交给可执行文件,因为动态链接中还有很多依赖于共享对象的无效地址,需要进一步处理。映射完成后,OS会启动一个动态链接器(Dynamic Linker)——linux下就是ld.so。这个动态链接器实际上也是一个共享对象,OS也会通过映射的方式把它载入进程的地址空间中。然后OS就会把控制权交给DL的入口地址,它会进行一系列初始化操作,根据当前环境参数对可执行文件进行动态链接工作。完成之后再把控制权转交给可执行文件。
pwn常见的so hell
动态链接器并非由系统配置或者环境参数决定,而是由ELF可执行文件自己决定!动态链接的ELF可执行文件中,有一个专门的段叫做.interp,这个段里就保存了一个字符串(可执行文件所需要的动态链接器的路径)一般就是这个路径
$ readelf -x .interp RNote3
Hex dump of section '.interp':
0x00000238 2f6c6962 36342f6c 642d6c69 6e75782d /lib64/ld-linux-
0x00000248 7838362d 36342e73 6f2e3200 x86-64.so.2.
比赛中遇到一个和系统ld不匹配的libc.so时,由于ELF中的动态链接器路径指向系统默认的ld,然后就会出现修改LD_PRELOAD仍然无法加载指定libc的情况。一个做法是找到题目给的libc版本然后找一个匹配的ld,通过change_ld来加载指定libc。
def change_ld(binary, ld):
"""
Force to use assigned new ld.so by changing the binary
"""
if not os.access(ld, os.R_OK):
log.failure("Invalid path {} to ld".format(ld))
return None
if not isinstance(binary, ELF):
if not os.access(binary, os.R_OK):
log.failure("Invalid path {} to binary".format(binary))
return None
binary = ELF(binary)
for segment in binary.segments:
if segment.header['p_type'] == 'PT_INTERP':
size = segment.header['p_memsz']
addr = segment.header['p_paddr']
data = segment.data()
if size <= len(ld):
log.failure("Failed to change PT_INTERP from {} to {}".format(data, ld))
return None
binary.write(addr, ld.ljust(size, '\0'))
if not os.access('/tmp/pwn', os.F_OK): os.mkdir('/tmp/pwn')
path = '/tmp/pwn/{}_debug'.format(os.path.basename(binary.path))
if os.access(path, os.F_OK):
os.remove(path)
info("Removing exist file {}".format(path))
binary.save(path)
os.chmod(path, 0b111000000) #rwx------
success("PT_INTERP has changed from {} to {}. Using temp file {}".format(data, ld, path))
return ELF(path)
#example
elf = change_ld('./pwn', './ld.so')
p = elf.process(env={'LD_PRELOAD':'./libc.so.6'})
.dynamic段
ELF里专门用于动态链接的段还有几个,首先是.dynamic。这个段里保存了动态链接器所需的基本信息,比如依赖于哪些共享对象,动态链接符号表的位置,动态链接重定位表的位置,共享对象初始化代码的地址。.dynamic段的结构由一个类型变量加上一个附加的数值或者指针组成。
typedef struct {
Elf32_Sword d_tag;
union {
Elf32_Word d_val;
Elf32_Addr d_ptr;
} d_un;
} Elf32_Dyn;
这里的d_tag表示这个表项的类型,可供取值范围(参考程序员的自我修养p205)
这个section给动态链接器提供动态链接的各种信息入口,用readelf -d bin
可以看到详细的关于此模块的详细动态链接信息。
$ readelf -d test
Dynamic section at offset 0xe18 contains 24 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000c (INIT) 0x400468
0x000000000000000d (FINI) 0x4006a4
0x0000000000000019 (INIT_ARRAY) 0x600e08
0x000000000000001b (INIT_ARRAYSZ) 8 (bytes)
0x000000000000001a (FINI_ARRAY) 0x600e10
0x000000000000001c (FINI_ARRAYSZ) 8 (bytes)
0x000000006ffffef5 (GNU_HASH) 0x400298
0x0000000000000005 (STRTAB) 0x400350
0x0000000000000006 (SYMTAB) 0x4002c0
0x000000000000000a (STRSZ) 95 (bytes)
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000015 (DEBUG) 0x0
0x0000000000000003 (PLTGOT) 0x601000
0x0000000000000002 (PLTRELSZ) 48 (bytes)
0x0000000000000014 (PLTREL) RELA
0x0000000000000017 (JMPREL) 0x400438
0x0000000000000007 (RELA) 0x4003f0
0x0000000000000008 (RELASZ) 72 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x000000006ffffffe (VERNEED) 0x4003c0
0x000000006fffffff (VERNEEDNUM) 1
0x000000006ffffff0 (VERSYM) 0x4003b0
0x0000000000000000 (NULL) 0x0
重定位表(.rel.plt和.rel.dyn)
使用readelf -r bin
可以查看elf文件的重定位section.
$ readelf -r test
Relocation section '.rela.dyn' at offset 0x3f0 contains 3 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000600fe8 000500000006 R_X86_64_GLOB_DAT 0000000000000000 printf@GLIBC_2.2.5 + 0
000000600ff0 000300000006 R_X86_64_GLOB_DAT 0000000000000000 __libc_start_main@GLIBC_2.2.5 + 0
000000600ff8 000400000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
Relocation section '.rela.plt' at offset 0x438 contains 2 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000601018 000100000007 R_X86_64_JUMP_SLO 0000000000000000 __stack_chk_fail@GLIBC_2.4 + 0
000000601020 000200000007 R_X86_64_JUMP_SLO 0000000000000000 read@GLIBC_2.2.5 + 0
.rel.dyn 包含了需要重定位的变量的信息, 叫做变量重定位表
.rel.plt 包含了需要重定位的函数的信息, 叫做函数重定位表
32位和64位使用的重定位表有一点区别,都是结构体数组但是一般32位使用Rel,64位使用Rela.比如上面readelf解析出来的就是一个64位程序的重定位表。结构体长成下面这个样子。
#define ELF32_R_SYM(i) ((i)>>8) // 获得高24位,表示在符号表中的偏移 R_SYMBOL
#define ELF32_R_TYPE(i) ((unsigned char)(i)) //获得低8位,表示重定位类型
#define ELF32_R_INFO(s,t) (((s)<<8)+(unsigned char)(t)) //通过R_SYM和Type重组info
typedef struct
{
Elf32_Addr r_offset; /* Address */
Elf32_Word r_info; /* Relocation type and symbol index */
} Elf32_Rel;
typedef struct elf32_rela{
Elf32_Addr r_offset;
Elf32_Word r_info;
Elf32_Sword r_addend;
} Elf32_Rela
typedef struct
{
Elf64_Addr r_offset; /* Address */
Elf64_Xword r_info; /* Relocation type and symbol index */
Elf64_Sxword r_addend; /* Addend */
} Elf64_Rela;
我们拿32位的举例子,重定位表项结构体是Elf32_Rel类型,包含r_offset和r_info两个信息,都是4个byte。r_info的高24位表示这个动态符号在动态链接符号表.dynsym中的位置。而r_info的低8位,表示这个待重定位对象的重定位类型。动态链接的重定向类型写在下面了
/* i386 relocs. */
#define R_386_NONE 0 /* No reloc */
#define R_386_32 1 /* Direct 32 bit */
#define R_386_PC32 2 /* PC relative 32 bit */
#define R_386_GOT32 3 /* 32 bit GOT entry */
#define R_386_PLT32 4 /* 32 bit PLT address */
#define R_386_COPY 5 /* Copy symbol at runtime */
#define R_386_GLOB_DAT 6 /* Create GOT entry */
#define R_386_JMP_SLOT 7 /* Create PLT entry */
#define R_386_RELATIVE 8 /* Adjust by program base */
/* AMD x86-64 relocations. */
#define R_X86_64_NONE 0 /* No reloc */
#define R_X86_64_64 1 /* Direct 64 bit */
#define R_X86_64_PC32 2 /* PC relative 32 bit signed */
#define R_X86_64_GOT32 3 /* 32 bit GOT entry */
#define R_X86_64_PLT32 4 /* 32 bit PLT address */
#define R_X86_64_COPY 5 /* Copy symbol at runtime */
#define R_X86_64_GLOB_DAT 6 /* Create GOT entry */
#define R_X86_64_JUMP_SLOT 7 /* Create PLT entry */
#define R_X86_64_RELATIVE 8 /* Adjust by program base */
#define R_X86_64_GOTPCREL 9 /* 32 bit signed PC relative offset to GOT */
好像32位一般用来函数重定位就是R_386_JMP_SLOT,64位函数重定位R_X86_64_JUMP_SLOT类型,是看源码的注释也是Create PLT entry。转而也能理解重定位表项里的r_offset的含义,r_offset为重定位对象的入口,用readelf做实验可以发现对于函数重定位其实就是指向了.got.plt的对应项。后面会说.got.plt是全局偏移表中存储重定位函数地址的地方。那么我们大概知道了,dl_runtime_resolve就是通过这个offset得知把解析出来的地址写到哪里。
全局偏移表(.got和.got.plt)
GOT 表在 ELF 文件中分为两个部分
- .got,存储全局变量的引用。
- .got.plt,存储函数的引用
在 Linux 的实现中,.got.plt 的前三项的具体的含义如下 - GOT[0],.dynamic 的地址。
- GOT[1],指向内部类型为 link_map 的指针,只会在动态装载器中使用,包含了进行符号解析需要的当前 ELF 对象的信息。每个 link_map 都是一条双向链表的一个节点,而这个链表保存了所有加载的 ELF 对象的信息。
- GOT[2],指向动态装载器中 _dl_runtime_resolve 函数
之后的got表项存的是函数的真实地址(解析过后),解析前存的是对应plt表项中那段胶水代码的第二条指令地址。整个解析过程就是在各自的plt传递reloc_arg, 在plt0传递link_map_obj.接着调用_dl_runtime_resolve.要注意的是,32位的reloc_arg和64位的有区别:32位使用reloc_offset, 64位使用reloc_index
#ifndef reloc_offset
#define reloc_offset reloc_arg
#define reloc_index reloc_arg / sizeof (PLTREL)
#endif
动态链接符号表(.dynsym)
是一个结构体数组,结构体为Elf32_Sym:
typedef struct
{
Elf32_Word st_name; /* Symbol name (string tbl index) */
Elf32_Addr st_value; /* Symbol value */
Elf32_Word st_size; /* Symbol size */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility under glibc>=2.2 */
Elf32_Section st_shndx; /* Section index */
} Elf32_Sym;
typedef struct
{
Elf64_Word st_name; /* Symbol name (string tbl index) */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf64_Section st_shndx; /* Section index */
Elf64_Addr st_value; /* Symbol value */
Elf64_Xword st_size; /* Symbol size */
} Elf64_Sym;
#define ELF32_R_SYM(val) ((val) >> 8)
#define ELF32_R_TYPE(val) ((val) & 0xff)
#define ELF32_R_INFO(sym, type) (((sym) << 8) + ((type) & 0xff))
#define ELF64_R_SYM(i) ((i) >> 32)
#define ELF64_R_TYPE(i) ((i) & 0xffffffff)
#define ELF64_R_INFO(sym,type) ((((Elf64_Xword) (sym)) << 32) + (type))
我们主要关注动态符号中的两个成员(注意32位和64位中这两个值在结构体里的位置不一样!)
- st_name, 该成员保存着动态符号在 .dynstr 表(动态字符串表)中的偏移。
- st_value,如果这个符号被导出,这个符号保存着对应的虚拟地址。
利用原理
动态链接下第一次调用glibc的函数需要通过plt表中的一段代码解析函数的真实地址,这也是linux的lazy bind的特点。具体的解析方式就是_dl_runtime_resolve(link_map_obj, reloc_arg) ,如果我们可以控制整个解析过程中的参数,那么就能解析我们想要的函数地址。回顾一下整个流程:
- call printf@plt
- jmp *(printf@got) -> (第一次会jmp回来,之后就直接jmp到解析出来的地址了) -> push n -> jmp &plt[0] (跳到公共表项)
- push got[1] (link_map 可以理解为模块ID) -> jmp *got[2] (跳转到dl_runtime_resolve函数)
- 以上步骤相当于调用了dl_runtime_resolve(link_map_obj, reloc_arg)
- 解析完毕后会把解析出来的地址写回reloc_arg定位到的.rel.plt表项中r_offset指向的位置(其实就是.got.plt的对应项)
- 弄懂dl_runtime_resolve的解析过程后,就可以通过伪造reloc_arg来解析出我们想要的libc函数地址并且写回可控区域了
dl_runtime_resolve
- 通过link_map_obj访问.dynamic section,分别取出.dynstr, .dynsym, .rel.plt的地址
- .rel.plt + reloc_index 求出当前函数重定位表项 Elf32_Rel的指针,记为rel
- rel->r_info的高24位作为.dynsym的下标,求出Elf32_Sym的指针,记作sym
- .dynstr + sym->st_name得到符号名字符串
- 在动态链接库查找这个函数的地址,并且把找到的地址赋值给rel->r_offset,即.got.plt
- 最后调用这个函数
32位情况下构造payload
构造payload
- 思路:伪造reloc_arg,使得函数重定位表项落在可控内存段,就可以伪造r_offset和r_info让动态链接符号表表项落在可控区域,接着伪造st_name让动态链接字符串表项值为目标函数名称。这些参数的构造需要拿到.dynstr,.dynsym,.rel.plt的地址。pwntools的ELF函数提供了这个接口:
elf= ELF(name)
rel_plt_addr = elf.get_section_by_name('.rel.plt').header.sh_addr #0x8048330
dynsym_addr = elf.get_section_by_name('.dynsym').header.sh_addr #0x80481d8
dynstr_addr = elf.get_section_by_name('.dynstr').header.sh_addr #0x8048278
伪造reloc_arg指向fake—rel,fake-rel里伪造好r_offset指到可控区域,构造r_info指向fake-sym,同时r_info要的低8位必须是7.fake-sym里伪造好st_name,让.dynstr+st_name指向伪造好的system字符串,就完成了整个构造过程。例子和exp很容易找到,之后有空再补64位情况下的一些注意事项。