Yusa的日常生活—美国大选 一开始只给了个nc,在Your vote后面输入一个十进制的数字,尝试复制上面的send过去,得到如下
可以通过尝试看出e=3(依次send1
,2
,3
这样一些比较小的数,得到的是各自立方转字节后的结果),猜测底层和RSA有关
后来才上了源码,关键的逻辑如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 print (r"Form of vote:{voter}:{votee}! eg: " )print ("Yusa:Trump!" )vote = pow (bytes_to_long(b"Yusa:Trump!" ), d, p * q) print ("vote:" , vote)try : yusa = int (input ("Your vote: " )) vote = long_to_bytes(pow (yusa, e, p * q)).split(b":" ) print (vote) if vote[-1 ] == "Trump!" : print (flag[:10 ]) elif vote[-1 ] == "Biden!" : print (flag[10 :]) except Exception as e: print (str (e)) exit()
RSA签名以及选择明文攻击,像下面这样可以搞出n来
1 2 3 4 5 6 7 8 9 10 11 12 from gmpy2 import *m = 11441834613624826172418663634046279113882505385891956376343615067285261133850481718598387372799863015467637880949144273419549065969128475439028853349338539694499051803136413392677937180372746004434886408226683060356000756320682607488203211095719491230534679239863637181733978031967798145219807433667347849650017657700970435971961705992349692203870546936934197834532861409558927204969349851725967348387731892180785297131580108320422093208933302332371110292195085685318752738251192012489360801049345566481979567998761296129083101798420484839100061738554182863915465349499485814533228278687226339268848069597015443528832 c2 = int (iroot(m, 3 )[0 ]) + 0x10001 c4 = c2 ** 2 c8 = c2 ** 3 m2 = b':' .join([b'Z\xa3\x04\x9a\xf5Co\x84xu?\xf4\xda\xd9%;\x81\xa6\x85=\x80Fp<\xf5\x01M2\x16\x15\xe0\xc5\xc3\xa4\x04\xc1\xe1\xc8)\xe6\xa0\xdeDX&\x90\xad\x00\x89a%C\xdek\xed\xde\xc8{\xf0\x7fW' , b"\xc8t\x81\xaaQ\xd5/D\xa0r]\x16\x8b\xbf=b\x19\x11\xf2V\xf0\xd7>\x8b\xa1=T0\x92\x15?\xbdR{\x0b\x91\xfbF\xf5H\xd7 [\xf9\xd1E\x12B3\xcf\x04VE\x9a\xa6K<\xc2-\x1b>V\xa8\x19\xfa\xf9\x8a\xc5_o\xf0)u\xb4\x92l\x1al=\xe9\n\x00C\x7f\x06\xd4\xf9\xfc\xeaX\x901\xceZ\xde\xcc 7\xb2j\x86-i\xbc|\x81\xf0\x08\r\x03\x16\x0c\x8aN\x18\xc90\xfc\xd8\x87\x07}A'D[\xa9\xb9\xb1\x11$N{\x10\x05\xab\x0e\x86~\xff\rT\xe3\xd7%\xb4\x8d6\x95\x9a\x96;\xc4h~\xe3\xbab\xecC\x9b\xf11'c\xfc&\xe3\x11\x850\xe5\xaf\t\xf1\x83\x83\x15M\x84=\x92\x8c\xda\xb9\xd7\xb2\x1e\x05\x9c\x1b\xb7\x06\xe8" ]) m4 = b':' .join([b"eW\xd5\xbd\x08w\x0c\xc9I\xac\x17\xecE\xcb\xda\xfd^;\xa4\xf7\xa6\xf3\x04PE/A\xe5qK\xe9JOm\xee\xd1h\xf7o\xa8\x1c\xa0\x068\xb3\xf1\xe7\x7f-\x9f\x19\xb4j\xa3\x90\x07\x1c\xb7\x12z\xad9A\xb9\xa7\xee\x8eBI\xd0]0\t\x9a\x08]\xbb\x1a\xca\x137}G\xc1\xfa\x14\xd1\t\xad\x1d\x1e\xa1\xb4\x88\x1a\x8d\xd3\x8a\xbcU\x86\x00Im\x05\xaaVJ(\xa9\x04rS\x00\x19\x02\x18\x8a\xa0g4\xcdg!9\xf5\x98z\xcc\x9b\x84\xd5>x\x13\xe2\xcd'1\x8b3a\xe6\x93\xd3\xa4\x06\xe0\xe9\x1aO\xa9\xb5\xe7@\xe8\x9atv\x8d\xf8_rK\xf2u\xf1\x04\x7f\xb45\xdf\x0f\x87\x97V\x15\xcc\xda\xd3\x8c\x8b\xd2V\x1e\x1d\xbd\xd1'\xca\xaa\xec\x1c\xee\xed\xe0\r6\xd7\xd3\x93;\x1e@Z\xaf6\xc4\xae\xe0\xe3\xbf\x85\xfd\x8f\x0b\xd9\xce^`\xbc\xc8\x7f\xfbQ\xd7\xae\xc8\x9bL\x02^\xcf\xbd\xcc\xa4D\xb7\x9f\xf9\x0c\xd5\xe9\xda\xa1\x03\x9d\xed0\x9aY\x85d$(}" ]) m8 = b':' .join([b'\x0fB\xc18\x04\x9c)\xbe\xf7J\xcc\xe2\x88\xd4\x12\xa7FwL\xa6\xc9\xf3\xaeRVV\xb6\xaeo\xd8\xc2h\xedo\xc7\xae\xd3\xf9\xab\xd6\xc43\x932\xf9\xff\xc9\xf6S\xd5\x9c\xba]f\xb6M\xde\x07\x9dx\xaaX6\xb5\x0fz\xf7\xc2\x0fY8\x81l\x14\x87\x91\x1c&\x1a\xce\xc30\xc4\xdd\xe2"V&(J\x9eD3S*O\t\x07A\xabZ\xdd>\xc0`\xc0d\xa3\xa7\xb4\xaa\xb5Tf\xae\x88\x8ex(\xf0\xaco0j\xdd\xc5\x13\xda\x86[\xee#\x18s\xe5\xfd\xb8 \xffP4\xd1\xc3\xd9\xe6\x18%\x1a\x13\x0ey+VQVb}{\xa5\xe1A\xa2y!Tu\xe2O\xf649\xa5u?\xbb\x02\xe4\x95z\xfef\x9c\x92E\x15\xadM\t\x98\x8dj\xd9\xd9wU\x93#;\xb6\xa2\x08\xe73\xcf\xf7;\xc7/\xc9\x0e\xa3\xf6\x18\xd2\xe8\xd7\xac\'\x07\xe6c\xc5\xedEhvf\xcdO\x11\xaf\xac^Y({\x19!\x81_h\x9c\x15\xfb\xbd\x0f\x89\xe8\xc6U0MM\xe0\x89}' ]) m1, m2, m3 = bytes_to_long(m2), bytes_to_long(m4), bytes_to_long(m8) n = gcd(m1 ** 2 - m2, m1 ** 3 - m3)
复现的时候重新搞了组数据
1 2 3 c = 15160582676846215658796665511167073968034171296661551317484064414530852799771618235942954787122243854880075538411956180191332121712749102940834469106520160911820689656980924928940857041289182520881995375555391429116695091017132599877027688472814801911156488488572658968858514549082874942198209100374290699361245526127362315158765216477920262317888955067180876826737792124089711465313919322691350383089478781115531028327464978498168550501234908278822070734957259085100730831106569924227163225782388696667537281068625312828784413432273812618076026392105724267064367773254829708738371876092746215522784893719413972288863 n = 15746945526432122479214630433270149949898055696087559742180830690847507891453515116075588796192340169435734319815395381666689989899222866782097160453565424738446970488798269177137604678808633207375125945542218010260891606865601540037996424336907526029576270546861238099120672228457910383070027371579273232850527367464802499941689387144111902397279853997069146494099457026354542952331566285341348508821513652475468126522170376081188733013126170230888215340038166274999418672245444957134175669183047604867371997408007732297783287752312442659031828940988055885376811763957465614461850705292428963651655262273479037747317 e = 3
已知enc,要用d签名一个以b’:Biden!’结尾的字节流并send过去,用e验证
也就是要使 $$ m=c^e\ mod\ n\notag $$ m在转字节流之后结尾是:Biden!
,显然这是RSA验证的过程,我们只要用d生成某个以:Biden!
结尾的字节流的密文 $$ c=m^{d}\ mod\ n\notag $$ 但是n无法分解,也就是说求不出d,那唯一的攻击点只有已知末尾是:Biden!
而实际上,不知道n也是完全可以做的,我这样搞出n反而把自己带偏
关键是构造一个在模2的倍数(且和3互素)的几次幂下的RSA签名,因为n是足够大的,完全可以构造使得三次方验证的时候,计算结果是小于模数的,也就是不用考虑n;但问题是模这样一个数为什么可以保留最低位?
我的理解是
初步理解是$c^3$低位就是f,因为在模$2^x$下,会保留低x位,这个比较容易懂;所以在$c^3$又是在RSA中的验证环节,得到的m,由于f是先d次方得到的c,在这个RSA签名验证的环节,低x位是成立的,所以m的低x位就是f。这应该是模$2^x$次方的性质(但好像模$14^x$也可以,不懂了)
完整的exp
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 from Crypto.Util.number import *from pwn import *from gmpy2 import *from re import *e = 3 sh = remote('47.96.253.167' , 10001 ) payload1 = sh.recvuntil(b'Your vote:' ).decode() payload1 = findall(r"\d+" , payload1)[0 ] sh.sendline(payload1) sh.recvuntil(b']\n' ) flag1 = sh.recvline()[:-1 ] sh.close() sh = remote('47.96.253.167' , 10001 ) f = bytes_to_long(b':Biden!' ) N = 2 ** 47 phiN = 2 ** 46 d = invert(e, phiN) c = pow (f, d, N) payload2 = str (c) sh.sendline(payload2) sh.recvuntil(b']\n' ) flag2 = sh.recvline()[:-1 ] sh.close() print (flag1 + flag2)
Yusa的密码学课堂—CBC第二课 来看主要代码逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 def task (): try : key = os.urandom(16 ) iv = os.urandom(16 ) cipher = _enc(flag, key, iv).hex () print (cipher) paintext = bytes .fromhex(input ("Amazing function: " )) print (enc(paintext, key).hex ()) backdoor = input ("Another amazing function: " ) assert backdoor != cipher if dec(bytes .fromhex(backdoor), key, iv) == flag: print (flag) else : print ("Wow, amazing results." ) except Exception as e: print (str (e)) exit()
先加密flag,然后给我们一个自己定IV和Plaintext的机会,进行加密,最后我们输入密文,提供一个解密的机会,如果这个解密的结果等于flag,但是我们输入的密文不能是flag加密后的密文,那么就输出flag
乍一听不可能啊,一个不是flag加密得到的密文,进行解密也可以得到flag?
这道题特殊就特殊在pad和unpad函数
1 2 3 4 5 6 7 8 9 10 def pad (data ): pad_len = BLOCKSIZE - (len (data) % BLOCKSIZE) if len (data) % BLOCKSIZE != 0 else 0 if pad_len == 0 : return data return data + chr (pad_len) * pad_len def unpad (data ): num = ord (data[-1 ]) return data[:-num]
这里unpad是关键,会根据最后一位将加上的pad给去掉;一般来说是没有问题的,但是如果有不怀好意的人刻意构造假的pad,那么情况就不一样了
下面是CBC加密的流程示意图
可以看出,无论我们在这三块中如何闹腾,都不可能整出一个解密是flag却又不是flag对应的密文
所以我们不妨手动加上一块,密文块2作为IV,而明文块3我们让最后其最后一位是16+flag的填充位数
转字节;这样就可以在加密的过程中,完全可以把这一块和前面三块并在一起,相当于加密flag+我们自己的填充
,而解密出来在unpad的时候,由于unpad的机制,就把所有的pad,包括flag本身的,以及我们添加的pad全部去掉了,得到的就是flag,但是密文不同
本地复现的,报一些奇怪的错,稍微改了下源码,应该无伤大雅
这样下面这个脚本在本地就可以打通了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from pwn import *sh = process(["python3" , "./server_py3.py" ]) cipher = bytes .fromhex(sh.recvline()[:-1 ].decode()) c0, c1, c2 = cipher[:16 ], cipher[16 :32 ], cipher[32 :] sh.recvuntil(b'Amazing function:' ) pad = chr (8 +16 ).encode() * 16 sh.sendline(pad.hex ()) sh.recvuntil(b'Your iv: ' ) sh.sendline(c2.hex ()) cx = bytes .fromhex(sh.recvline()[:-1 ].decode()) sh.recvuntil(b'Another amazing function: ' ) backdoor = cipher + cx sh.sendline(backdoor.hex ()) flag = sh.recvline().decode() print (flag)
因为看原附件中DASCTF{*********}
总长度是40,所以上面才这么果断设置了pad为chr(8+16).encode()*16
;如果不是的话,可以从一开始的密文知道,长48,也就是分三段,flag至少长33,稍微爆破下也可以出来的
Yusa的密码学课堂—CBC第三课 这道题就把pad给改掉了
1 2 3 4 5 6 def pad (data ): pad_len = BLOCKSIZE - (len (data) % BLOCKSIZE) if len (data) % BLOCKSIZE != 0 else 0 return data + "=" * pad_len def unpad (data ): return data.replace("=" ,"" )
来看主逻辑
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 def task (): try : key = os.urandom(16 ) asuy = enc(flag,key) print asuy.encode('hex' ) paintext = raw_input("Amazing function(in hex): " ) paintext = paintext.decode('hex' ) print enc(paintext,key).encode('hex' ) asuy = raw_input("Another amazing function(in hex): " ).decode('hex' ) yusa = dec(asuy,key) flag_l = s_2_l(flag) yusa_l = s_2_l(yusa) for each in yusa_l: if each in flag_l: print (r"You're not yusa!" ) exit() print yusa.encode('hex' ) except Exception as e: print str (e) exit() if __name__ == "__main__" : task()
先对flag进行加密,IV和KEY是相同的
然后是一个加密,可以由我们自己指定的明文的,而且IV和KEY也是之前的IV和KEY
最后是一个解密,同样也是IV和KEY
由于KEY和IV一样,这就可以提供我们很大的便利,最后一层解密不应该看做得到flag的直接途径,而是看成一次解密的机会更好
攻击流程如下图所示
左边是flag的加密过程,右边是我们借助已有的条件进行攻击的过程。主要的思路就是,因为KEY=IV,又分别有一次加密和解密,那为什么不直接把IV和KEY搞出来呢。
在我们自由控制明文的那里,我们输入两块全是\x00
的字节流,为的是消除明文对加密过程产生不可干预的影响,这样整个加密流程,除了KEY一切就变得透明起来。
做完一次加密,我们得到了两密文,再加上我们还剩下一次解密的机会,就可以想到把刚才的加密的流程给拆开,其中一半用解密函数来完全道破,另一半也就不攻自破
正如图上所画将c4
送过去解密,得到回显
;显然回显^key
就是c4
通过加密之前的c3
即回显 ^ key = c3
,那么c3
已知,key = c3 ^ 回显
,就此我们得到key,后面再现aes解密就好了
完整的exp如下
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 from pwn import *from Crypto.Cipher import AESdef unpad (s ): return s.replace(b"=" , b"" ) context.log_level = 'debug' sh = process(["python3" , "./server_py3.py" ]) key = bytes .fromhex(sh.recvline()[:-1 ].decode()) cipher = bytes .fromhex(sh.recvline()[:-1 ].decode()) c1, c2, c3 = cipher[:16 ], cipher[16 :32 ], cipher[32 :48 ] sh.recvuntil(b'Amazing function(in hex): ' ) plaintext = b'\x00' * 32 sh.sendline(plaintext.hex ()) cx = bytes .fromhex(sh.recvline()[:-1 ].decode()) sh.recvuntil(b'Another amazing function(in hex): ' ) yusa = cx[16 :32 ] sh.sendline(yusa.hex ()) asuy = bytes .fromhex(sh.recvline()[:-1 ].decode()) keyx = xor(asuy[:32 ], cx[:16 ]) assert key == keyxaes = AES.new(keyx, AES.MODE_CBC, keyx) print (unpad(aes.decrypt(cipher)).decode())
可以打通
当然和第二题一样,pad和unpad函数也做过一丢丢的修改,不知道这个修改伤不伤大雅
1 2 3 4 5 6 7 8 9 def pad (data ): pad_len = BLOCKSIZE - (len (data) % BLOCKSIZE) if len (data) % BLOCKSIZE != 0 else 0 if pad_len == 0 : return data return data + "=" * pad_len def unpad (data ): return data.replace(b"=" , b"" )
总而言之今天的题偏脑洞