聊聊动态链接和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,' ')) 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) $ 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 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 [email?protected]_2.2.5 + 0 000000600ff0 000300000006 R_X86_64_GLOB_DAT 0000000000000000 [email?protected]_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 [email?protected]_2.4 + 0 000000601020 000200000007 R_X86_64_JUMP_SLO 0000000000000000 [email?protected]_2.2.5 + 0 .rel.dyn 包含了需要重定位的变量的信息,叫做变量重定位表 #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 文件中分为两个部分
#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位中这两个值在结构体里的位置不一样!)
利用原理动态链接下第一次调用glibc的函数需要通过plt表中的一段代码解析函数的真实地址,这也是linux的lazy bind的特点。具体的解析方式就是_dl_runtime_resolve(link_map_obj,reloc_arg) ,如果我们可以控制整个解析过程中的参数,那么就能解析我们想要的函数地址。回顾一下整个流程:
dl_runtime_resolve
32位情况下构造payload构造payload
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位情况下的一些注意事项。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |