Byte线上, 勉强前20挤进线下了, 属实刺激
题目难度中等把, 不会特别白给但也不会特别难, 就是某道题CFB搞不懂摸了将近一天才在最后一个小时摸出来
easyxor 题目把$flag$分成两部分$flagFront \ and \ flagBack$, 并且给了一个加密用的函数$convert$, 看了眼$key$的大小, $2^{24}$是吧, 喜欢短的是吧, 待会就把它给爆了
先来看$flagFront$, 加密用的模式是$OFB$, 这里有一个点要想到, $flag$的前缀是ByteCTF{
刚好8字节也就是一个分组.
对着上面的解密过程, 因为第一个明文ByteCTF{
密文分组都有了, 可以直接异或计算出第一块的$Block \ Cipher \ $ 第二块的$Block \ Cipher$是第一块加密得到, 如果我们能够运行加密程序, 我们就能还原出后面的明文, 但我们没有key, 没办法跑加密
不过一开始有提到, $key$的大小就$2^{24}$这么大, 完全可以花一点时间爆破, 所以思路有了, 直接穷举所有的$key$, 能把后面的两组密文还原成uuid(这个uuid形式也是猜出来的, 觉得有可能就试了一下)那种形式的字符串的$key$就是对的$key$.
爆破出$key$以后就是考虑怎么写$convert$的逆操作, 也就是解密操作,逆出来了就能拿到$IV$了,但是,整了半天没啥头绪, 干脆上Z3直接日, 没想到真的能日出来.
拿到了$IV$和$Key$, 我们又可能逆操作$convert$, $flagBack$用的是$CBC$模式, 那不是要什么有什么?
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 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 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 )))
md爆破key的时候忘了每次都重置一下last
, 爆了一上午的key都没有结果, 属实纯铸币了这波, 还好下午赶着打apex, 硬是重新写了遍脚本才发现问题.
总之z3 yyds!
abusedkey 两个没有见过的协议, 看到就想摸了, 后面没新题属实是摸不下去了, 题目都快穿了都, 赶紧解压附件看题. 这协议也就是看着烦而已, 认真看下去还是可以做的, 就是tm给hint还给的这么麻烦, 直接跳过hint了
题目给的协议过程就不复述了, 第一个协议的关键部分在下面
$\underleftarrow{\qquad \ \ \ \ \ msg_{12} = T_S \qquad \ \ \ \ \ }$
$t_C \stackrel{R}{\leftarrow} {\mathbb Z}_p^*$
$T_C = r_C \cdot G$
$K_{CS} = (d_C+t_C)\cdot T_S + t_C\cdot P_S$
$sk_1 = \mathcal{H}({K_{CS}}_x)$
$\underrightarrow{\qquad \ msg_{13} = sid_1||T_C\qquad \ }$
$K_{CS} = (d_S+t_S)\cdot T_C + t_S\cdot P_C$
$sk_1 = \mathcal{H}({K_{CS}}_x)$
$C_{flag} = E_{sk_1}(flag)$
$\underleftarrow{\qquad \ \ \ msg_{14} = C_{flag} \qquad \ \ \ \ }$
$flag = D_{sk_1}(C_{flag})$
最后得到的是$AES-GCM$加密过后的$flag$, 要解密肯定得知道$sk$也就是要知道$K_{CS}$, 问题在于这题我们不知道应该知道的$d_C$和$P_S$ , 这就没办法算$K_{CS}$了.
不过后面有提到:
Server的私钥/口令哈希,都源自同一个2字节口令,即$\pi_S \in {0,1}^{16}$,并且$d_S = {\mathcal H}(\pi_S)$;
好家伙$16$位的口令, 又是这么短的, $d_S$那不是直接爆吗老哥,
有$d_S$就等于有$P_S$了, 那么问题的关键就在于怎么去选择$t_C$, 使得$d_C$不用知道也能计算$sk$, 那第一眼看到的不就是$d_C+t_C$这一项吗? 直接取$t_C = - d_C$, 那整个$K_{CS}$的计算就变成 $$ K_{CS} = t_C \cdot P_S = -d_C \cdot d_S \cdot G = -P_C \cdot d_S $$ $P_C$就是客户的公钥, 这个是有的,
到这里我们就能够计算$K_{CS}$再计算$sk$,最后用$sk$解密$flag$就行
至于协议二, 没研究怎么整, 赛后听L-team的帝叁叁币四踢零爷说协议二就是给$d_S$的, 所以结果是我太暴力了是吗?
爆, 能爆的都可以爆, 爆就完事了, 就是爆$d_S$的时候发现小小的一个16位居然要花我20分钟, 赶紧打开TIMI来一把, 没想到十分钟$flag$就出来了, 果断点了投降交$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 import requestsfrom Crypto.Util.number import long_to_bytes,bytes_to_longfrom hashlib import sha256from Crypto.Cipher import AESfrom tqdm import tqdmurl = '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啥的, 后来才发现就是日格子?
题目给的条件先列举一下 $$ Alice : g^a = A\mod p \ Bob : g^b = B\mod p \ g^{ab} = C_1 + x_1 \mod p \ g^{2ab} = C_2 + x_2 \mod p $$ $A,B,C_1,C_2$是已知的, 解释一下后面两行, 后面两行就是利用to_bob()
函数, 传$g^a,g^{2a}$得到的, 重点就是后面那两行式子, 把$g^{ab}$代进最后一行就可以有 $$ (C_1 + x_1)^2 = C_2 + x_2 + kp \ => x_1^2 - x_2 = (C_2 - C_1^2) - 2C_1x_1 + kp $$ 看到这种等式马上就想到鸽鸡龟约了, 而且$x$都很小, 64位, 直接根据等式造格子就vans了 $$ (1,x_1,k) \begin{pmatrix} R & 0 & C_2 - C_1^2 \ 0 & 1 & -2C_1 \ 0 & 0 & p \end{pmatrix} = (R,x_1,x_1^2 - x_2) $$ 这个格子有个细节就是第三行得是$p$而不是$-2C_1$, 否则目标向量里会有$k$, 而不是$x_1$, $x_1$才是我们想要的啊. 这波细不细
$R$喜闻乐见就是调$det$用的, 从$1$遍历到$128$就行了
规约出短向量就有$x_1$了, 直接算$C_1 + x_1$就是$g^{ab} \mod p$了
由于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 pwnfrom gmpy2 import invertfrom random import randintp = 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) 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
JustDecrypt 就是这题试了快一天, 在别人家的学校的图书馆吹风坐了一下午cao
说实话这题整了半天到现在也没搞懂这Crypto库的AES-CFB
是怎样的模式, 反正就很离谱, 那个IV就不知道它是拿来怎么用的,还好硬是看规律摸出来了
要关注的第一个点是unpad()
函数, 这个函数太简单了,出大问题, 可以通过控制解密出的明文的最后一个字节, 来进行对解密的明文按自己需要来截断
第二个点就是这个奇怪的Crypto.AES_CFB
, 反正就是乱传各种数字给服务器, 传了可能一个上午吧大概,发了一堆0和1后发现,
更改密文的某一个字节的时候, 最多影响后面的16个字节的解密
某个字节从\x00
更改到\x??
, 那么解密出的明文对应的那个字节会异或\x??
每次解密用的$IV$, 似乎是上一个密文的后$16$个字节(?存疑问
用上面三个结论, 我们就能构造目标明文对应的密文了
我们先传2048 * \x00
, 会得到一串abababababab
这种形式的明文, 根据这个明文计算END = ab ^ 1
, 以后每次传的密文最后都带着END
, 并且END
前面的密文的16个字节都是\x00
,就能控制解密出的明文的最后一个字节是\x01
, 这样把返回的明文的长度控制在最长, 我们可以更改END = ab ^ 1
中的1, 从而可以控制返回的明文为任意长度.
Hello, I'm a Bytedancer. Please give me the flag!
对应的十六进制是
48656c6c6f2c2049276d2061204279746564616e6365722e20506c656173652067697665206d652074686520666c616721
假设我们第一步得到的ababababab
形式的明文是7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c
那么构造的密文的第一位就是0x7c ^ 0x48 = 0x34
, 接下来就传\x00
* 32 +\x34
+ \x00
* 2012 + END
假设返回的明文是xxxxxxxxxxxxxxxxxxxxxxx4820506c656173xxxxxxxxx7c7c7c7c7c7c7c7c7c7c7c7c7c7cc7c7c7
c
那么构造的密文的第二位就是0x20 ^ 0x65 = 0x45
(0x20
是上面48
的后面一个字节, 0x65
是我的目标明文也就是Hello
中的e
), 然后继续传\x00
* 32 +\x34\x45
+ \x00
* 2010 + END
以此类推
最后构造出我们需要的密文后, 再用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 pwnfrom hashlib import sha256import codecsfrom time import sleepdef 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)
总之就是硬摸, 反正也不是什么复杂的过程, 看图找规律了属于是