2025鹏城杯 -- Crypto -- RandomAudit详解
题目描述:
cmd
某内部审计系统号称"全部采用自研高强度密码模块",为了节省日志空间,他们把敏感日志AES加密上传后,又用RSA对关键密钥做了"备份加密",最后还用"秘密分享"来保护真正的FLAG。不行的是,开发同学对"随机数"和"密码学"理解似乎有点... ...偏差。你从审计服务器拿到以下文件你的任务是:从这些文件中还原出最终的flag。
题目附件
cmd
aes_ct.txt
rsa_pub.txt
session_ids.txt
shares.enc
题目解析
-
四个文件,打开查看一下数据信息
aes_ct.txtcmdiv = 42c92a1cdbb02d347b4b12b9609cf761 ciphertext = 1268d5a5513b091aa91bd263479e066b07fadc2fbbe45bef3c551f5743df6290efac6008efec815a92d781f56d46a70crsa_pub.txt
pythonfrom Crypto.Util.number import isPrime n = 14448446619901911082459007889685721725680439060725350743569322155645458011734556057675947993220704371020476516568646586885283913148938740926737898805445614714032672445417590527807998210095698512153249389800494251979617943578391716055258550807448612914300134993287344474991004931078983886017648843976928491853922545681367649020611719416129281298024839864263193236272253977406788185152922537075953161349835965958367362419140954183280033874927397931106816815848243331537133623628059136650221110030928439294443459932885523261901634369102152105618433841943766668404214910274232926198895411937001894534370492568479216945821 e = 65537 print(isPrime(n)) # False print(n.bit_length()) # 2047session_ids.txt
pythonsession_id = [ 18180927788752802137, 17282895955721322606, 6099480220253429739, 6300661433352782096, 16375414315099041565 ] for i in session_id: print(i.bit_length(), end = " ") # 64 64 63 63 64shares.en
pythonenc = [ 10490446977619103069001003040486311071978258142421336052507257522875474797470114964826023919192727268692922256856652858277992495171032562230040420656266197389810578603393339709249435149905947489583447642314971169203129701324545382296605329968093805623085091037041383918898863606006040657610555353505097417933879241971194430768615532922845973883691987688479825332766028800851528002249541074495918035787961557869335382147521674475402254162082487461333820079507872678735070636201634641388295625530938494923015901907636472951311020506187207731872422229080880406432931064027509903450627233085289319384639770111853961194173, 743543530169071239116476194995130287577397444839878381190323439278920253193070703147739971809232103531663814326885396949461522610039497489729382962956546531947712520339554515115672653928505197575781138133407250554871658829577958508307679587210232844184449562790093797350878222669855209441010484097564691896453015601662453896822719270868321247424008816580067245157535459310278265418077691933129567685517708559976634296731742166139336765265993523200086705895447733895631261834250507718941102582843992200156414235064121973675311925953582587558104979368104990418797298789916260850166852409948945330444433345847549654807, 2029042383383543423822480961040444011316223475888350349028507603033900194528521850810498682278559241519416272300206175404920960777005556199927430201052663390710468284846655098680434563893510099837791622819154273540446883979349265553248678057937124051615313506877155912975894736822847688340682915149135134624117775360734190231872747665460355709192535066382845245616076861873767871845621325187272538154068026983469680440318851689871048209435452483669288691299287219165436861956437625332354783255272742721764184259145427360294854601680425317982867911864451490378130715517007586443259629430886305855145096221339953320736 ] for c in enc: print(c.bit_length(), end = " ") # 2047 2043 2045 -
我们通过上面的分析可以看出来,session_id 分别为64-bit 的参数,而RSA 为常规的CTF-RSA 赛题,大概率对应着shares.enc 的公钥。此后,我以为这是一个基本符合安全标准的"伪加密系统",注意到这里给的其他信息很少,我们需要一层一层抽丝剥茧,题目描述中提及到的随机数 我认为是非常重要的,抱着多尝试的心态,这里我以为这里的随机数是出题人提及的密码系统 以时间戳为种子产生的相应随机数字,于是我开始以2020年~2026年 的所有时间戳为种子,在Python/C++/Java 编程环境下都进行了尝试,但是都没有找到满足session_ids.txt 的连续随机数,死马当活马医,我尝试使用LCG 试一下,后面发现的确是LCG。
pythontemp = [18180927788752802137, 17282895955721322606, 6099480220253429739, 6300661433352782096, 16375414315099041565] a = (temp[2] - temp[1]) * pow(temp[1] - temp[0], -1, 2 ** 64) % (2 ** 64) b = (temp[1] - a * temp[0]) % (2 ** 64) print(f"a = {a}"); print(f"b = {b}") # a = 2985016781437487305 # b = 7999203871274490253 for i in range(1, len(temp)): print((a * temp[i - 1] + b) % (2 ** 64) == temp[i], end = " ") # True True True True -
之后问题感觉变简单了许多了,学过一些TLS 协议的话,当握手结束之后,接下来的适应大概就是密钥协商了,这里还是抱着尝试一下的心态,我利用现有的LCG 状态向后寻找AES的密钥,来对应密码系统中的会话密钥,有如下发现:
pythonnext = lambda state : (a * state + b) & (1 << 64) - 1 state = 16375414315099041565 a = 2985016781437487305 b = 7999203871274490253 for i in range(10): state = next(state) print(hex(state)[2:]) """ e18e0b036273ff52 1ae1729d164360ef 42c92a1cdbb02d34 # iv[:16] 7b4b12b9609cf761 # iv[16:] c27b9072fa995cb6 23ef04e1eae1d473 2d60a86204b9ffd8 9c5147b880d57a25 ae1acaa5c523289a 7edc153c41ef0a77 """这里我发现LCG 再向后的第3 个状态和第4 个状态就是iv ,于是这里就尝试将第1 个状态和第2 个状态作为key 进行AES密钥的猜测。
-
由于这里存在iv ,并且密码系统中现在常用的对称加密一般为AES-GCM ,于是这边先尝试了一下AES-GCM解密:
pythonfrom Crypto.Cipher import AES key = bytes.fromhex('e18e0b036273ff521ae1729d164360ef') iv = bytes.fromhex('42c92a1cdbb02d347b4b12b9609cf761') ciphertext = bytes.fromhex('1268d5a5513b091aa91bd263479e066b07fadc2fbbe45bef3c551f5743df6290efac6008efec815a92d781f56d46a70c') tag = ciphertext[-16:] ciphertext = ciphertext[:-16] my_aes = AES.new(key = key, mode = AES.MODE_GCM, nonce = iv) print(my_aes.decrypt(ciphertext)) # b'oY\xa2\xebt?^\xe7\x9b\xe3\xb4\xae\xb2oJ\xb9\x83\x7f&:\x05\x02\xbd]\xdam^;\x15GC\xdc'这里发现并没有什么题目中所说的日志 ,于是乎,我采用了CBC 、CTR模式解密...
python# CBC from Crypto.Cipher import AES key = bytes.fromhex('e18e0b036273ff521ae1729d164360ef') iv = bytes.fromhex('42c92a1cdbb02d347b4b12b9609cf761') ciphertext = bytes.fromhex('1268d5a5513b091aa91bd263479e066b07fadc2fbbe45bef3c551f5743df6290efac6008efec815a92d781f56d46a70c') # tag = ciphertext[-16:] # ciphertext = ciphertext[:-16] my_aes = AES.new(key = key, iv = iv, mode = AES.MODE_CBC) print(my_aes.decrypt(ciphertext)) # b'\xf1\x0b?\xd0&\x7f`_\x83\xfct\x87\x9ei\xf9$5\x91\x9d\xe5KoC\xfe\x030\xd5^\xc6\x1bE,\xc5\xc5\x8c\xb3\xf4!\xad+\x1d\xf9\x14\x13\xe2\xabq\x10'python# CTR from Crypto.Cipher import AES from Crypto.Util import Counter key = bytes.fromhex('e18e0b036273ff521ae1729d164360ef') iv = bytes.fromhex('42c92a1cdbb02d347b4b12b9609cf761') ciphertext = bytes.fromhex('1268d5a5513b091aa91bd263479e066b07fadc2fbbe45bef3c551f5743df6290efac6008efec815a92d781f56d46a70c') ctr = Counter.new(128, initial_value = int(iv.hex(), 16)) my_aes = AES.new(key = key, mode = AES.MODE_CTR, counter = ctr) print(my_aes.decrypt(ciphertext)) # b'rsa_seed=106141198856029082317733263764736461207'这里我们就发现了有意义的明文rsa_seed=106141198856029082317733263764736461207 ,这个应该就是题目描述中所说的日志了。(其实,恢复LCG 之后吗,我觉得接下来就是由LCG 产生RSA 的分解素数了,因为在C++这类的编程语言中,大素数的来源一般都是由小素数经过一定的变化而来的,但是由于生成方式太多了,并且爆破一种的时间就很多,因此打算换条路径,看一看key 和iv 与LCG的关系)
-
此后,我们就找到了这里的关键点,那就是我们可以分解RSA 的模数N 了(这里由于N 是2047-bit 的模数,因此采用1024的参数获取):
pythonimport random n = 14448446619901911082459007889685721725680439060725350743569322155645458011734556057675947993220704371020476516568646586885283913148938740926737898805445614714032672445417590527807998210095698512153249389800494251979617943578391716055258550807448612914300134993287344474991004931078983886017648843976928491853922545681367649020611719416129281298024839864263193236272253977406788185152922537075953161349835965958367362419140954183280033874927397931106816815848243331537133623628059136650221110030928439294443459932885523261901634369102152105618433841943766668404214910274232926198895411937001894534370492568479216945821 seed = 106141198856029082317733263764736461207 random.seed(seed) while True: p = random.getrandbits(1024) if n % p == 0: print(f"p = {p}") break # p = 90609714628980382832928314067735122109537000815405703652905812504340171456915234535659504024209916610947095995757554416998229460671634350728278188486124871962281306292021145245327286154856863282195166928593488382708996274001548097657625671236135154197435672004124259946571008184688393674125715687022036411833如果大家要问,为何这里由rsa_seed=106141198856029082317733263764736461207 就想到要用random 模块了,是因为在常规的密码系统中,我们并没有getPrime()这样的函数使用,一般的生成方式就是伪随机数发生器,直接利用或组合生成。
-
之后,按照题目描述的逻辑,我们要进行秘密分享 的恢复阶段了。秘密分享 一般的方式就是两种,一种是基于CRT 的,另外一种就是基于拉格朗日插值法 的。前者的碎片是**(m, s),后者碎片为 (x, f(x)),但是我们剩下的唯一的数据就是 shares.enc中的 3个将近 2048-bit的数据了,做到这里的时候我才真觉得这道题目离谱,此后经过不断地尝试,我决定直接对shares.enc**中的内容进行解密了:
pythonfrom Crypto.Util.number import long_to_bytes n = 14448446619901911082459007889685721725680439060725350743569322155645458011734556057675947993220704371020476516568646586885283913148938740926737898805445614714032672445417590527807998210095698512153249389800494251979617943578391716055258550807448612914300134993287344474991004931078983886017648843976928491853922545681367649020611719416129281298024839864263193236272253977406788185152922537075953161349835965958367362419140954183280033874927397931106816815848243331537133623628059136650221110030928439294443459932885523261901634369102152105618433841943766668404214910274232926198895411937001894534370492568479216945821 e = 65537 p = 90609714628980382832928314067735122109537000815405703652905812504340171456915234535659504024209916610947095995757554416998229460671634350728278188486124871962281306292021145245327286154856863282195166928593488382708996274001548097657625671236135154197435672004124259946571008184688393674125715687022036411833 q = n // p d = pow(e, -1, (p - 1) * (q - 1)) shares = [ 10490446977619103069001003040486311071978258142421336052507257522875474797470114964826023919192727268692922256856652858277992495171032562230040420656266197389810578603393339709249435149905947489583447642314971169203129701324545382296605329968093805623085091037041383918898863606006040657610555353505097417933879241971194430768615532922845973883691987688479825332766028800851528002249541074495918035787961557869335382147521674475402254162082487461333820079507872678735070636201634641388295625530938494923015901907636472951311020506187207731872422229080880406432931064027509903450627233085289319384639770111853961194173, 743543530169071239116476194995130287577397444839878381190323439278920253193070703147739971809232103531663814326885396949461522610039497489729382962956546531947712520339554515115672653928505197575781138133407250554871658829577958508307679587210232844184449562790093797350878222669855209441010484097564691896453015601662453896822719270868321247424008816580067245157535459310278265418077691933129567685517708559976634296731742166139336765265993523200086705895447733895631261834250507718941102582843992200156414235064121973675311925953582587558104979368104990418797298789916260850166852409948945330444433345847549654807, 2029042383383543423822480961040444011316223475888350349028507603033900194528521850810498682278559241519416272300206175404920960777005556199927430201052663390710468284846655098680434563893510099837791622819154273540446883979349265553248678057937124051615313506877155912975894736822847688340682915149135134624117775360734190231872747665460355709192535066382845245616076861873767871845621325187272538154068026983469680440318851689871048209435452483669288691299287219165436861956437625332354783255272742721764184259145427360294854601680425317982867911864451490378130715517007586443259629430886305855145096221339953320736 ] for share in shares: m = pow(share, d, n) print(long_to_bytes(m)) """ b'\\\xe0\xe9\xa5`\x15\xfe\xc5\xaa\xdf\xa3(\xae9\x81\x07' b'\xb9\xc1\xd3J\xc0+\xfd\x8bU\xbfFQ\\s\x02\x1c' b'\x01\x16\xa2\xbc\xf0 A\xfcQ\x00\x9e\xe9z\n\xac\x831' """ -
至此,的确没辙了,过了一段时间之后了解到题目有提示说flag为**flag{uuid}**的格式,但是这里实在找不到思路了,想着会不会解密之后的三个值,就是秘密分享的碎片?
pythonfrom Crypto.Util.number import long_to_bytes n = 14448446619901911082459007889685721725680439060725350743569322155645458011734556057675947993220704371020476516568646586885283913148938740926737898805445614714032672445417590527807998210095698512153249389800494251979617943578391716055258550807448612914300134993287344474991004931078983886017648843976928491853922545681367649020611719416129281298024839864263193236272253977406788185152922537075953161349835965958367362419140954183280033874927397931106816815848243331537133623628059136650221110030928439294443459932885523261901634369102152105618433841943766668404214910274232926198895411937001894534370492568479216945821 e = 65537 p = 90609714628980382832928314067735122109537000815405703652905812504340171456915234535659504024209916610947095995757554416998229460671634350728278188486124871962281306292021145245327286154856863282195166928593488382708996274001548097657625671236135154197435672004124259946571008184688393674125715687022036411833 q = n // p d = pow(e, -1, (p - 1) * (q - 1)) shares = [ 10490446977619103069001003040486311071978258142421336052507257522875474797470114964826023919192727268692922256856652858277992495171032562230040420656266197389810578603393339709249435149905947489583447642314971169203129701324545382296605329968093805623085091037041383918898863606006040657610555353505097417933879241971194430768615532922845973883691987688479825332766028800851528002249541074495918035787961557869335382147521674475402254162082487461333820079507872678735070636201634641388295625530938494923015901907636472951311020506187207731872422229080880406432931064027509903450627233085289319384639770111853961194173, 743543530169071239116476194995130287577397444839878381190323439278920253193070703147739971809232103531663814326885396949461522610039497489729382962956546531947712520339554515115672653928505197575781138133407250554871658829577958508307679587210232844184449562790093797350878222669855209441010484097564691896453015601662453896822719270868321247424008816580067245157535459310278265418077691933129567685517708559976634296731742166139336765265993523200086705895447733895631261834250507718941102582843992200156414235064121973675311925953582587558104979368104990418797298789916260850166852409948945330444433345847549654807, 2029042383383543423822480961040444011316223475888350349028507603033900194528521850810498682278559241519416272300206175404920960777005556199927430201052663390710468284846655098680434563893510099837791622819154273540446883979349265553248678057937124051615313506877155912975894736822847688340682915149135134624117775360734190231872747665460355709192535066382845245616076861873767871845621325187272538154068026983469680440318851689871048209435452483669288691299287219165436861956437625332354783255272742721764184259145427360294854601680425317982867911864451490378130715517007586443259629430886305855145096221339953320736 ] for share in shares: m = pow(share, d, n) print(m) # print(long_to_bytes(m)) """ 123456789012345678901234567890123456775 246913578024691357802469135780246913564 370370367037037036703703703670370370353 """这里先尝试一下,将解密后的内容当作**(m, s)或者说是 (x, f(x))中的s**,看一下能否恢复:
pythonfrom Crypto.Util.number import * def recover_secret(shares, threshold, modulus): if len(shares) < threshold: raise ValueError(f"Need at least {threshold} shares to recover secret, but only {len(shares)} provided") secret = 0 for i, (xi, yi) in enumerate(shares): numerator = 1 denominator = 1 for j, (xj, _) in enumerate(shares): if i != j: numerator = (numerator * (0 - xj)) % modulus denominator = (denominator * (xi - xj)) % modulus denominator_inv = pow(denominator, -1, modulus) lagrange_coef = (numerator * denominator_inv) % modulus secret = (secret + yi * lagrange_coef) % modulus return secret if __name__ == "__main__": n = 14448446619901911082459007889685721725680439060725350743569322155645458011734556057675947993220704371020476516568646586885283913148938740926737898805445614714032672445417590527807998210095698512153249389800494251979617943578391716055258550807448612914300134993287344474991004931078983886017648843976928491853922545681367649020611719416129281298024839864263193236272253977406788185152922537075953161349835965958367362419140954183280033874927397931106816815848243331537133623628059136650221110030928439294443459932885523261901634369102152105618433841943766668404214910274232926198895411937001894534370492568479216945821 shares = [(123456789012345678901234567890123456775, 10490446977619103069001003040486311071978258142421336052507257522875474797470114964826023919192727268692922256856652858277992495171032562230040420656266197389810578603393339709249435149905947489583447642314971169203129701324545382296605329968093805623085091037041383918898863606006040657610555353505097417933879241971194430768615532922845973883691987688479825332766028800851528002249541074495918035787961557869335382147521674475402254162082487461333820079507872678735070636201634641388295625530938494923015901907636472951311020506187207731872422229080880406432931064027509903450627233085289319384639770111853961194173), (246913578024691357802469135780246913564, 743543530169071239116476194995130287577397444839878381190323439278920253193070703147739971809232103531663814326885396949461522610039497489729382962956546531947712520339554515115672653928505197575781138133407250554871658829577958508307679587210232844184449562790093797350878222669855209441010484097564691896453015601662453896822719270868321247424008816580067245157535459310278265418077691933129567685517708559976634296731742166139336765265993523200086705895447733895631261834250507718941102582843992200156414235064121973675311925953582587558104979368104990418797298789916260850166852409948945330444433345847549654807), (370370367037037036703703703670370370353, 2029042383383543423822480961040444011316223475888350349028507603033900194528521850810498682278559241519416272300206175404920960777005556199927430201052663390710468284846655098680434563893510099837791622819154273540446883979349265553248678057937124051615313506877155912975894736822847688340682915149135134624117775360734190231872747665460355709192535066382845245616076861873767871845621325187272538154068026983469680440318851689871048209435452483669288691299287219165436861956437625332354783255272742721764184259145427360294854601680425317982867911864451490378130715517007586443259629430886305855145096221339953320736)] flag = recover_secret(shares, 3, n) print(long_to_bytes(flag))到这里我有个很脑洞的发现,那就是,当我们对shares.enc 的内容进行RSA 解密后,得到的是一个等差数列 ,这个会不会就是题目中所说的分享的秘密?这个等差数列一定是出题人故意而为之的,但是目前走到这里,有了下面的发现之后,虽然猜测很大胆,但是目前也没有想到别的思路了。
pythonm = [ 123456789012345678901234567890123456775, 246913578024691357802469135780246913564, 370370367037037036703703703670370370353 ] assert m[2] - m[1] == m[1] - m[0] diff = m[1] - m[0] # sum(m) secret = hex(diff)[2:] print(secret) # 5ce0e9a56015fec5aadfa328ae398115 print(len(secret)) # 32 -
我先尝试的是对m 直接求和,但是发现16 进制下,以及直接转bytes 都是不满足uuid(长度32)的。至此,到这里平台的提交系统已经关闭了,我也不知道我的推断是否是正确的,我认为的最终的flag为:
cmdflag{5ce0e9a5-6015-fec5-aadf-a328ae398115} -
真想看看正确的flag 是什么...,大脑燃烧了那么久...这道题目虽然出题人想要复现TLS协议的场景,但是还是有些脑洞,鄙人认为相当于是现实中分析着密码方案,信息搜集的人告诉你"可能吧"这种场景,感觉改成真实的流量包,可以看到完整的操作流程的那种考察效果会更好。