writeup for 2021 ByteCTF 线上

Byte线上, 勉强前20挤进线下了, 属实刺激

题目难度中等把, 不会特别白给但也不会特别难, 就是某道题CFB搞不懂摸了将近一天才在最后一个小时摸出来

easyxor

题目把分成两部分, 并且给了一个加密用的函数, 看了眼的大小, 是吧, 喜欢短的是吧, 待会就把它给爆了

先来看, 加密用的模式是, 这里有一个点要想到, 的前缀是ByteCTF{刚好8字节也就是一个分组.

img

对着上面的解密过程, 因为第一个明文ByteCTF{密文分组都有了, 可以直接异或计算出第一块的 第二块的是第一块加密得到, 如果我们能够运行加密程序, 我们就能还原出后面的明文, 但我们没有key, 没办法跑加密

不过一开始有提到, 的大小就这么大, 完全可以花一点时间爆破, 所以思路有了, 直接穷举所有的, 能把后面的两组密文还原成uuid(这个uuid形式也是猜出来的, 觉得有可能就试了一下)那种形式的字符串的就是对的.

爆破出以后就是考虑怎么写的逆操作, 也就是解密操作,逆出来了就能拿到了,但是,整了半天没啥头绪, 干脆上Z3直接日, 没想到真的能日出来.

拿到了, 我们又可能逆操作, 用的是模式, 那不是要什么有什么?

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
# key爆破
cip = '89b8aca257ee2748f030e7f6599cbe0cbb5db25db6d3990d3b752eda9689e30fa2b03ee748e0da3c989da2bba657b912'
cp = [cip[i:i+16] for i in range(0,len(cip),16)]

flag = b'ByteCTF{'
last = bytes_to_long(flag) ^ int(cp[0],16)

for i in range(-32, 33):
for j in range(-32, 33):
for k in range(-32, 33):
for l in range(-32, 33):
last = bytes_to_long(flag) ^ int(cp[0],16)
keys = [i,j,k,l]
try:
m1,last = decry_ofb(int(cp[1],16),keys,last)
M = m1.decode('ASCII')
m2,last = decry_ofb(int(cp[2],16),keys,last)
M = M + m2.decode('ASCII')
if check(M):
print(M,keys)
except:
pass
# [-12, 26, -3, -31]
key = [-12, 26, -3, -31]
IV = 16476971533267772345

S = z3.Solver()
x = z3.BitVec('x',65)
S.add(convert(x,key) == int(cp[5],16))
if S.check():
print(S.model())

print(long_to_bytes(10936161096540945944 ^ int(cp[4],16)))
# 这是最后一个密文分组的Z3脚本, 每个分组都是手动填参数算出来的, 一共四个分组, OFB一个分组, CBC三个分组都需要Z3
# 注意的是有时候Z3会有多个解, 需要转成字符串判断是不是正确的解, 不正确的排除掉

md爆破key的时候忘了每次都重置一下last, 爆了一上午的key都没有结果, 属实纯铸币了这波, 还好下午赶着打apex, 硬是重新写了遍脚本才发现问题.

总之z3 yyds!

abusedkey

两个没有见过的协议, 看到就想摸了, 后面没新题属实是摸不下去了, 题目都快穿了都, 赶紧解压附件看题. 这协议也就是看着烦而已, 认真看下去还是可以做的, 就是tm给hint还给的这么麻烦, 直接跳过hint了

题目给的协议过程就不复述了, 第一个协议的关键部分在下面

最后得到的是加密过后的, 要解密肯定得知道也就是要知道, 问题在于这题我们不知道应该知道的 , 这就没办法算了.

不过后面有提到:

Server的私钥/口令哈希,都源自同一个2字节口令,即,并且

好家伙位的口令, 又是这么短的, 那不是直接爆吗老哥,

就等于有了, 那么问题的关键就在于怎么去选择, 使得不用知道也能计算, 那第一眼看到的不就是这一项吗? 直接取, 那整个的计算就变成

就是客户的公钥, 这个是有的,

到这里我们就能够计算再计算,最后用解密就行

至于协议二, 没研究怎么整, 赛后听L-team的帝叁叁币四踢零爷说协议二就是给的, 所以结果是我太暴力了是吗?

爆, 能爆的都可以爆, 爆就完事了, 就是爆的时候发现小小的一个16位居然要花我20分钟, 赶紧打开TIMI来一把, 没想到十分钟就出来了, 果断点了投降交了.

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
import requests
from Crypto.Util.number import long_to_bytes,bytes_to_long
from hashlib import sha256
from Crypto.Cipher import AES
from tqdm import tqdm
url = 'http://39.105.181.182:30000/abusedkey/server/msg11'
session_id = '8cae789b1eac36ad274ee14cac147bdf2dde49585c8db2463d5b6b5f7f44e4da'
p = 2 ^ 256 - 2^32 - 977
Ep = EllipticCurve(GF(p),[0,7])
G = Ep((0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798,0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8))
Pc = Ep((0xb5b1b07d251b299844d968be56284ef32dffd0baa6a0353baf10c90298dfd117,0xea62978d102a76c3d6747e283091ac5f2b4c3ba5fc7a906fe023ee3bc61b50fe))

Msg12 = requests.get(url,data=session_id).text

url = 'http://39.105.181.182:30000/abusedkey/server/msg13'
Tc = -Pc

data13 = session_id + hex(Tc[0])[2:].ljust(64,'0') + hex(Tc[1])[2:].ljust(64,'0')
Msg14 = long_to_bytes(int(requests.get(url,data=data13).text,16))
iv = Msg14[:12]
enc_flag = Msg14[12:-16]

for i in tqdm(range(2**16)):
ds = bytes_to_long(sha256(long_to_bytes(i)).digest())
KCS = -ds * Pc
sk = sha256(long_to_bytes(KCS[0])).digest()
A = AES.new(sk,AES.MODE_GCM,iv)
flag = A.decrypt(enc_flag)
if b'Byte' in flag:
print(flag)
break

Overheard

题目就那么几行, 流程倒是挺容易懂的, 就是一开始以为考点是Elgamal啥的, 后来才发现就是日格子?

题目给的条件先列举一下

是已知的, 解释一下后面两行, 后面两行就是利用to_bob()函数, 传得到的, 重点就是后面那两行式子, 把代进最后一行就可以有

看到这种等式马上就想到鸽鸡龟约了, 而且都很小, 64位, 直接根据等式造格子就vans了

这个格子有个细节就是第三行得是而不是, 否则目标向量里会有, 而不是, 才是我们想要的啊. 这波细不细

喜闻乐见就是调用的, 从遍历到就行了

规约出短向量就有了, 直接算就是

由于sagemath很难装pwntools, 我的机器没办法直接写一次性打的脚本, 只能靠手速把条件复制出来扔进sage里跑再手动扔回给服务器(其实就是懒得开wsl了, 你还别说, 这10s内复制黏贴进sage再复制结果回来发过去还真有些难, 还好平时有锻炼手速, 不然还真拿不到这题的二血

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
# 连接用脚本
import pwn
from gmpy2 import invert
from random import randint
p = 62606792596600834911820789765744078048692259104005438531455193685836606544743
g = 5


con = pwn.remote('39.105.38.192',30000)
resp = con.recvuntil("$ ").decode()
con.sendline('1'.encode())
resp = con.recvuntil("$ ").decode()
Alice = int(resp.split('\n')[0])

con.sendline('2'.encode())
resp = con.recvuntil("$ ").decode()
Bob = int(resp.split('\n')[0])


con.sendline('3'.encode())
resp = con.recv(1024).decode()
con.sendline(str(Alice).encode())
resp = con.recvuntil("$ ").decode()
AliceBob = int(resp.split('\n')[0])
print('c1 = ',AliceBob)

con.sendline('3'.encode())
resp = con.recv(1024).decode()
con.sendline(str(pow(Alice,2,p)).encode())
resp = con.recvuntil("$ ").decode()
AliceBob = int(resp.split('\n')[0])
print('c2 = ',AliceBob)

con.sendline('4'.encode())
resp = con.recv(1024).decode()
SEND = input('send: ')
con.sendline(str(SEND).encode())
resp = con.recv()
print(resp)

# sage
c1 = 29599245400103510126844665705234428663959969687042945989429410277722893058048
c2 = 17549134874331317464534964139671627000441097203060226867571953366738719997952
p = 62606792596600834911820789765744078048692259104005438531455193685836606544743

for i in range(128):
M = Matrix([[2^i,0,c2 - c1 ^2],[0,1,-2 * c1],[0,0,-p]])
if M.LLL()[0][0] // (2 ^ i) == 1:
res = M.LLL()[0]
print(M.LLL()[0])
break

x1 = res[1]
x1 + c1 # g^ab


JustDecrypt

就是这题试了快一天, 在别人家的学校的图书馆吹风坐了一下午cao

说实话这题整了半天到现在也没搞懂这Crypto库的AES-CFB是怎样的模式, 反正就很离谱, 那个IV就不知道它是拿来怎么用的,还好硬是看规律摸出来了

要关注的第一个点是unpad()函数, 这个函数太简单了,出大问题, 可以通过控制解密出的明文的最后一个字节, 来进行对解密的明文按自己需要来截断

第二个点就是这个奇怪的Crypto.AES_CFB , 反正就是乱传各种数字给服务器, 传了可能一个上午吧大概,发了一堆0和1后发现,

  1. 更改密文的某一个字节的时候, 最多影响后面的16个字节的解密
  2. 某个字节从\x00更改到\x??, 那么解密出的明文对应的那个字节会异或\x??
  3. 每次解密用的, 似乎是上一个密文的后个字节(?存疑问

用上面三个结论, 我们就能构造目标明文对应的密文了

  1. 我们先传2048 * \x00, 会得到一串abababababab这种形式的明文, 根据这个明文计算END = ab ^ 1 , 以后每次传的密文最后都带着END, 并且END前面的密文的16个字节都是\x00 ,就能控制解密出的明文的最后一个字节是\x01, 这样把返回的明文的长度控制在最长, 我们可以更改END = ab ^ 1中的1, 从而可以控制返回的明文为任意长度.

  2. Hello, I'm a Bytedancer. Please give me the flag!对应的十六进制是

    48656c6c6f2c2049276d2061204279746564616e6365722e20506c656173652067697665206d652074686520666c616721

  3. 假设我们第一步得到的ababababab形式的明文是7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c

    那么构造的密文的第一位就是0x7c ^ 0x48 = 0x34, 接下来就传\x00 32 +\x34 + \x00 2012 + END

    假设返回的明文是xxxxxxxxxxxxxxxxxxxxxxx4820506c656173xxxxxxxxx7c7c7c7c7c7c7c7c7c7c7c7c7c7cc7c7c7c

    那么构造的密文的第二位就是0x20 ^ 0x65 = 0x45 (0x20是上面48的后面一个字节, 0x65是我的目标明文也就是Hello中的e), 然后继续传\x00 32 +\x34\x45 + \x00 2010 + END 以此类推

  4. 最后构造出我们需要的密文后, 再用1中控制返回明文长度的方法, 把明文截断到刚好是

    Hello, I'm a Bytedancer. Please give me the flag!即可

上面的过程需要不断的调试才能整的明白… 由于远程是有proof_of_work()的, 建议在本地关掉proof_of_work()打完本地再打远程, 不然可能就会感受半分钟调试一次, 半年拿到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
49
50
51
52
53
54
55
56
import pwn
from hashlib import sha256
import codecs
from time import sleep

def proof(END, HASH):
table = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
for i in table:
for j in table:
for k in table:
for l in table:
STR = i + j + k + l + END
if sha256(STR.encode()).hexdigest() == HASH:
print(i + j + k + l)
return i + j + k + l

pt = b"Hello, I'm a Bytedancer. Please give me the flag!"
con = pwn.remote('39.105.181.182', 30001)
resp = con.recvuntil('> ').decode().split('\n')[-2]
END = resp[12:40]
HASH = resp[45:]
con.sendline(proof(END,HASH).encode())
resp = con.recvuntil('> ').decode()
con.sendline(('0' * 1024).encode())
resp = con.recvuntil('> ').decode()
tmp = int(resp.split('\n')[1][32:34], 16)
END = hex(tmp ^ 1)[2:].ljust(2, '0')
DATA = ['0'] * 1022 + [END[0] , END[1]]
roundnum = ''
TABLE = [50,250,450]
for i in range(49):
sleep(1)
idx = TABLE[i % 3]
roundnum += hex(tmp ^ pt[i])[2:].rjust(2,'0')
for j in range(len(roundnum)):
DATA[idx + j] = roundnum[j]
con.sendline((''.join(DATA)).encode())
resp = con.recvuntil('> ').decode()
print(i,roundnum.zfill(98))
tmp = int(resp.split('\n')[1][TABLE[i%3]+2*i + 2:TABLE[i%3]+2+2*i + 2],16)


roundnum += hex(tmp ^ pt[i])[2:].ljust(2,'0')
con.sendline(('0' * 2048).encode())
resp = con.recvuntil('> ').decode()
tmp = int(resp.split('\n')[1][32:34], 16)
print(hex(tmp))
END = hex(tmp ^ 31)[2:].ljust(2, '0')
print(len(roundnum),len(roundnum + END * 15))
con.sendline((roundnum + 58 * '0'+ END).encode())
resp = con.recv()
print(resp)
resp = con.recv()
print(resp)
resp = con.recv()
print(resp)

总之就是硬摸, 反正也不是什么复杂的过程, 看图找规律了属于是