###### tags: `writeups` # Writeup for Fast Crypto in CyBRICS ## Description ![](https://i.imgur.com/4XQArEj.png) 意思就是说,这是一个*very modern*的加密算法,试着凭借一个*secret message*来解密这个*WAV*文件。 ## Analysis 加密脚本: ```python import json from tqdm import tqdm from egcd import egcd from pathlib import Path import sys def get_next(a, power, N): b = a ** power % N return b, b % 256 if not Path('public.key').exists(): print("Key file not found") exit(1) if len(sys.argv) != 3: print("Usage: python3 cryptor.py <filename_in> <filename_out>") exit(1) try: key = json.loads(Path('public.key').open().read()) data = Path(sys.argv[1]).open('rb').read() except Exception as e: print("Error during keyfile load: {}".format(e)) exit(1) else: seed = int(input("Input seed value (int16): ")) if seed < 0 or seed > 2**16: print("Invalid seed. Encryption may be too slow.") exit(1) power = int(input("Input power value (2-16): ")) if power < 2 or power > 16: print("Invalid power. Encryption may be too slow.") exit(1) if egcd(power, key['N'])[0] != 1: print("Invalid power value") exit(1) # calculate offset for _ in range(key['O']): seed = get_next(seed, power, key['N'])[0] with Path(sys.argv[2]).open('wb') as w: for i in tqdm(range(len(data))): seed, bt = get_next(seed, power, key['N']) w.write(bytes([data[i] ^ bt])) ``` 分析后得到以下信息: - seed: [0, 2**16] - power: [2, 16] - 先对seed做`key['O']=31337`轮**幂模**运算。 - 再用seed的低8-bit来和数据异或(也就是相当于一个`stream cipher`)。 `seed`和`power`都是随机的,后面所有东西都是由这两个随机数决定的。 先提取`key`再说。 ``` N = 73882271767225067628282422302548400069735434660808564149855701357643617388546710750167259617348302921921308565033188464703728096338599435641707966042336489111486646451993143053931136428343207443971020988133537590763029595507074481482058514976650388077801669364711731111970923913958083815347282871017052613916475670517 O = 31337 ``` 出于直觉,验证了一下N和O是否为质数。发现O是个质数,但是N不是。 ![](https://i.imgur.com/8SMEqMi.png) 用`sage`来*factor*`N`: ![](https://i.imgur.com/Q6kFKfZ.png) 都是一些比较小的质数。 想了想从N出发应该如何crack这个加密算法。31337轮**幂模**后,`seed`肯定巨大无比,再来取低位做`stream cipher`的`key`,这个好像真的crack不了。 一个比较奇怪的地方是,它的那个幂模运算居然是: ```python def get_next(a, power, N): b = a ** power % N return b, b % 256 ``` 为什么不用*python*自带的基于快速幂(猜测,但多半就是了)的`pow()`函数呢?这样先乘方,再取余的算法实在是需要太大的运算量了。 未果,睡了一会儿。 --- 醒来的时候,感觉可以用`brute-force`,2**16才只是65536,再加上`power`的15种可能,总共就65536*15=983040种可能,爆破起来肯定很快(后面可以看到,快不快还是要取决于算法的`optimization`)。 顺着它的思路写脚本: ```python from tqdm import tqdm N = 73882271767225067628282422302548400069735434660808564149855701357643617388546710750167259617348302921921308565033188464703728096338599435641707966042336489111486646451993143053931136428343207443971020988133537590763029595507074481482058514976650388077801669364711731111970923913958083815347282871017052613916475670517 O = 31337 raw = [82, 73, 70, 70] enc = [0x46, 0x83, 0x49, 0x44] head = [] for i in range(8): head.append(raw[i] ^ enc[i]) # head = [20, 202, 15, 2, 239, 26, 199, 210] # brute-force for power in range(2, 17): print(power) for s in tqdm(range(2**16+1)): seed = s for _ in range(O): seed = pow(seed, power, N) q = 1 for i in range(4): seed = pow(seed, power, N) bt = seed & 255 if bt != head[i]: q = 0 break if(q): print(s, power) ``` **说明一下**:前面关于`raw, enc, head`的一段。题目提示加密前的文件格式是`.wav`,去找了一下`wav`文件的开头,是以`RIFF`为开头的前4个byte数据,记为`raw`;再对加密后文件开头的4个byte(记为`enc`)分别异或,得到的结果`head`应当为`stream key`的前4个byte,以此作为验证的依据。这里4个byte数据已经足够了,因为256^4=4294967296 >> 983040=2^16*15。 可是这个脚本跑起来却很慢: ![](https://i.imgur.com/mL1IP0D.png) 这可能要跑一天才能跑完,开多进程可以快一些,但总的来说,还是要需要**小时**级别的时间。 思考应当如何**优化算法**。回过头来看代码时,发现O轮那个循环实在是有点费时间,每次都需要31337次pow(),即使是快速幂也顶不住啊。 O轮pow(),也就是相当于直接幂`power**O`,可以改写成`pow(seed, power**O, N)`。但是`2**31337`已经是个超级无敌变态大的数字了,这里应该还可以再优化一下。 幂模,联想到了**欧拉定理**`pow(a, phi(n), n) = 1`。也就是说,那个幂其实是个周期性的东西,`pow(a, k*phi(n)+b, n) = pow(a, b, n)`,可以对幂`mod phi(n)`,结果仍然不变。 再回到这题,前面分析到,`N`不是一个质数并且能够被轻易地质因数分解。那么,计算`phi(N)`就是一个很简单的事情了。 ![](https://i.imgur.com/QcVWWR1.png) ![](https://i.imgur.com/iPYocIS.png) 有了`phi(N)`,就还能再改写成`pow(seed, pow(power, O, phi_N), N)`,这就快多了。 ```python from tqdm import tqdm N = 73882271767225067628282422302548400069735434660808564149855701357643617388546710750167259617348302921921308565033188464703728096338599435641707966042336489111486646451993143053931136428343207443971020988133537590763029595507074481482058514976650388077801669364711731111970923913958083815347282871017052613916475670517 phi_N = 73518206147262443676837361159127776505334808540008666455558114391523228702493449359589995596913129043911224919282022228135566170964663692657377830877081156633758852294230067854975206472518946297562604445162853306974953289714803969929459936619828333107726452353102277743999390152590440267776000000000000000000000000000 O = 31337 raw = [82, 73, 70, 70] enc = [0x46, 0x83, 0x49, 0x44] head = [] for i in range(8): head.append(raw[i] ^ enc[i]) # head = [20, 202, 15, 2, 239, 26, 199, 210] # brute-force for power in range(2, 17): print(power) for s in tqdm(range(2**16+1)): seed = s seed = pow(seed, pow(power, O, phi_N), N) q = 1 for i in range(4): seed = pow(seed, power, N) bt = seed & 255 if bt != head[i]: q = 0 break if(q): print(s, power) ``` ![](https://i.imgur.com/uU5PN00.png) 这相比之前,简直就像火箭一样快。修改power的值,手动多进程了一下,很快就跑出来了: ![](https://i.imgur.com/V6frTVt.png) (注意换行那边) 所以`seed=4485, power=7`。 接着就是*decode*了 ```python from tqdm import tqdm power = 7 seed = 4485 N = 73882271767225067628282422302548400069735434660808564149855701357643617388546710750167259617348302921921308565033188464703728096338599435641707966042336489111486646451993143053931136428343207443971020988133537590763029595507074481482058514976650388077801669364711731111970923913958083815347282871017052613916475670517 phi_N = 73518206147262443676837361159127776505334808540008666455558114391523228702493449359589995596913129043911224919282022228135566170964663692657377830877081156633758852294230067854975206472518946297562604445162853306974953289714803969929459936619828333107726452353102277743999390152590440267776000000000000000000000000000 O = 31337 seed = pow(seed, pow(power, O, phi_N), N) with open("enc.wav", "rb") as fin: data = fin.read() with open("flag.wav", "wb") as fout: for i in tqdm(range(len(data))): seed = pow(seed, power, N) bt = seed & 255 fout.write(bytes([data[i] ^ bt])) ``` 打开解密后的`wav`文件,一开始还听不出来,多听了好几遍(吐槽一下这个文本转语音),才听出来`The flag is cybrics bracket b l u m underscore b l u m underscore crypto rear bracket`,也就是`cybrics{blum_blum_crypto}`。提交,正确! ## Summary - 了解到了`wav`文件的一些基本格式。 - 学到了一手`from tqdm import tqdm`,可以进度条显示。 - 看来,密码题还是需要爆破啊。 - 学到了一点算法的*optimization*。 - 听力有待提升。