对称该不会做还是不会做TT
xor 题目:
1 2 mimic is a keyword. 0b050c0e180e585f5c52555c5544545c0a0f44535f0f5e445658595844050f5d0f0f55590c555e5a0914
将mimic和密文异或一下就行。
watermarking 题目描述:
题目:
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 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 from Crypto.Util.number import *from Crypto.Cipher import AESfrom hashlib import sha256import socketserverimport signalimport osimport stringimport randomfrom Crypto.PublicKey import RSAfrom Crypto.Cipher import PKCS1_v1_5from Crypto.Util.number import getPrime,getStrongPrimefrom Crypto.Random import get_random_bytesfrom sympy import nextprimeclass Task (socketserver.BaseRequestHandler): def _recvall (self ): BUFF_SIZE = 4096 data = b'' while True : part = self.request.recv(BUFF_SIZE) data += part if len (part) < BUFF_SIZE: break return data.strip() def send (self, msg, newline=True ): try : if newline: msg += b'\n' self.request.sendall(msg) except : pass def recv (self, prompt=b'> ' ): self.send(prompt, newline=False ) return self._recvall() def handle (self ): e=3 maxlens=4096 RSAparameter=None signal.alarm(60 ) watermark=os.urandom(512 ) self.send(b"welcome to my proxy encryption program with watermarking" ) messages=[] while 1 : self.send(b"1 for free encrypt with time limit" ) self.send(b"2 for check your watermarking " ) self.send(b"plz input function code" ) try : mode=int (self.recv()) except : self.send(b"plz input right function code" ) continue if mode==1 : if maxlens<=0 : self.send(b"sorry, no free chance, try last time~" ) continue self.send(b"now choose your n bitlens" ) self.send(b"1. 1024 2. 2048 3. 4096" ) try : mode=int (self.recv()) except : self.send(b"plz input right function code" ) continue if mode==1 : newparameter=1024 if mode==2 : newparameter=2048 if mode==3 : newparameter=4096 if newparameter!= RSAparameter: RSAparameter=newparameter p=getStrongPrime(RSAparameter//2 ) q=getStrongPrime(RSAparameter//2 ) n=p*q phi=(p-1 )*(q-1 ) self.send(b"your n is changed to" ) self.send(hex (n).encode()) try : rsa_key = RSA.construct((n, e)) public_key = rsa_key.publickey().export_key() cipher = PKCS1_v1_5.new(RSA.import_key(public_key)) self.send(b"plz input your message with hex" ) message=self.recv().decode() messages.append(message) message=watermark[:RSAparameter//16 ]+bytes .fromhex(message) c = (cipher.encrypt(message)).hex ().encode() self.send(c) e=nextprime(e) maxlens-=RSAparameter except : self.send(b"wrong~ try again" ) continue elif mode==2 : if maxlens>0 : self.send(b"sorry, not even yet~" ) continue self.send(b"now give me your message without free chance" ) e=65537 d=pow (e,-1 ,phi) rsa_key = RSA.construct((n, e,d)) public_key = rsa_key.publickey().export_key() cipher = PKCS1_v1_5.new(rsa_key) cts=self.recv().decode() cts=bytes .fromhex(cts) try : p = (cipher.decrypt(cts,b"bad cipertext" )) print (p) except Exception as e: print (e) self.send(b"wrong~ try again" ) continue if p[:RSAparameter//16 ]!=watermark[:RSAparameter//16 ]: self.send(b"not used the watermark!" ) continue if p[RSAparameter//16 :].hex () in messages: self.send(b"not cost!" ) continue f=open ("./flag" ,"rb" ) flag=f.read() f.close() self.send(b"cong! your flag is " +flag) self.request.close() class ThreadedServer (socketserver.ThreadingMixIn, socketserver.TCPServer): pass class ForkedServer (socketserver.ForkingMixIn, socketserver.TCPServer): pass if __name__ == "__main__" : HOST, PORT = '0.0.0.0' , 9999 print ("HOST:POST " + HOST+":" + str (PORT)) server = ForkedServer((HOST, PORT), Task) server.allow_reuse_address = True server.serve_forever()
题目比较长,在连接上靶机后,靶机会先生成一个512字节的随机串作为watermark,主要的交互功能有:
看源码可以知道,pkcs#1 v1.5的填充是这样的:
也就是说,假设n的字节数为k,那么待加密明文对应的字节数要小于等于k-11才行,因为至少要留出11个字节做前面的填充。而前面的填充为:
1 b'\x00\x02' + ps + b'\x00'
其中ps是urandom产生的对应长度的字节流。
我们的目的是求出watermark,比较自然的想法是取两次2048的公钥长度,由于长度一致,他们就对应同一个2048bit的公钥n。此时假设我们输入两个长为117的消息,那么被加密的两次明文就分别是:
1 2 b'\x00\x02' + ps1 + b'\x00' + watermark[:128 ] + msg1b'\x00\x02' + ps2 + b'\x00' + watermark[:128 ] + msg2
此时ps1和ps2都是8字节的小量,所以比较自然的想法是联立两式,消掉watermark之后二元copper求解。先将两次加密的式子表示出来:
resultant一下会发现一个很大的问题,联立消元之后虽然的确是ps1、ps2组成的度为15的多项式,然而项实在太多了,对应的用copper去做的话会有非常多的shift,完全做不了。而直接用这个多项式去格的话界是完全不够的,所以要想其他办法。
而可以发现,如果两次msg取的完全一样的话就有:
此时一个技巧是可以把如下式子看作一个变量:
那么就有:
这样换元之后消元带来的一个好处在于,消元得到的是关于小量t的单元方程,可以很有效的减少多项式项的个数,所以单元copper就可以解出t来。
解出t之后做个多项式gcd自然就有S,也就求出了水印,然后随意构造个消息发送回去即可。
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 from Crypto.Util.number import *from pwn import *from os import urandomsh = remote("pwn-2419eb29ff.challenge.xctf.org.cn" , 9999 , ssl=True ) sh.recvuntil(b"plz input function code\n" ) sh.sendline(b"1" ) sh.sendline(b"2" ) sh.recvuntil(b"your n is changed to\n" ) n = eval (sh.recvline().strip().decode()) msg1 = urandom(117 ) sh.recvuntil(b"plz input your message with hex\n" ) sh.sendline(msg1.hex ().encode()) m1 = bytes_to_long(msg1) c1 = int (sh.recvline().strip().decode()[2 :], 16 ) sh.recvuntil(b"plz input function code\n" ) sh.sendline(b"1" ) sh.sendline(b"2" ) msg2 = msg1 sh.recvuntil(b"plz input your message with hex\n" ) sh.sendline(msg2.hex ().encode()) m2 = bytes_to_long(msg2) c2 = int (sh.recvline().strip().decode()[2 :], 16 ) length = 8 prefix = bytes_to_long(b"\x00\x02" + b"\x00" *254 ) PR.<e,x> = PolynomialRing(Zmod(n)) f1 = (x)^3 - c1 f2 = (x + 256 ^(128 - 3 - length + 128 + 1 )*e)^5 - c2 g = f1.sylvester_matrix(f2,x).det() g = str (g).replace("^" , "**" ) PR1.<e> = PolynomialRing(Zmod(n)) g = eval (g) g = g.monic() res = g.small_roots(X=256 ^8 , beta=1 , epsilon=0.05 ) e = int (res[0 ]) if (len (bin (e)) > 100 ): e = n-e PR.<x> = PolynomialRing(Zmod(n)) f1 = (x)^3 - c1 f2 = (x + 256 ^(128 - 3 - length + 128 + 1 )*e)^5 - c2 def gcd (g1, g2 ): while g2: g1, g2 = g2, g1 % g2 return g1.monic() m = -gcd(f1, f2)[0 ] watermark = long_to_bytes(int (m), 256 )[11 :11 +128 ] print (watermark)sh.recvuntil(b"plz input function code\n" ) sh.sendline(b"2" ) sh.recvuntil(b"now give me your message without free chance\n" ) msg = b"\x00\x02" + urandom(8 ) + b"\x00" + watermark + urandom(117 ) ct = pow (bytes_to_long(msg), 65537 , n) sh.sendline(long_to_bytes(ct).hex ().encode()) sh.recvuntil(b"cong! your flag is " ) print (sh.recvline())
CFBchall 题目描述:
1 遭遇过几次拖库之后,我们直接删除了数据库!请你保存好生成的token,不要篡改哦~
题目:
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 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 from flask import Flask, render_template, request, jsonifyfrom Crypto.Cipher import AESimport osfrom string import printableapp = Flask(__name__) def initialize_globals (): global key, iv,register_open, login_attempts key = os.urandom(16 ) iv = os.urandom(16 ) register_open = True login_attempts = 500 initialize_globals() def encrypt (data, key ): cipher = AES.new(key, AES.MODE_CFB, iv=iv) ct_bytes = cipher.encrypt(data.encode('utf-8' )) return ct_bytes.hex () def decrypt (ct, key ): ct_bytes = bytes .fromhex(ct) cipher = AES.new(key, AES.MODE_CFB, iv=iv) data = cipher.decrypt(ct_bytes[:]) return data @app.route('/' , methods=['GET' , 'POST' ] ) def index (): global register_open, login_attempts message = "" if request.method in ['POST' ]: if request.is_json: data = request.get_json() action = data.get('action' ) if action == 'register' : if not register_open: message = "Registration function is closed" else : username = data.get('username' ) password = data.get('password' ) if len (password)<8 : message = "Registration failed, password too short" elif not set (password+username)<=set (printable): message = "Registration failed, illegal character" elif 'admin' in username: message = "Registration failed, you are not an admin" else : token = encrypt(f"{username} \x00{password} \x01\x02\x03" , key) register_open = False message = f"Please save your token: {token} " elif action == 'login' : if login_attempts <= 0 : message = "Too many attempts" else : username = data.get('username' ).encode() password = data.get('password' ).encode() token = data.get('token' ) try : decrypted = decrypt(token, key) token_username, *_, token_password = decrypted.split(b'\x00' ) assert (token_password[-3 :]==b"\x01\x02\x03" ) token_password=token_password[:-3 ] if username == token_username and password == token_password: if username == b'admin' : if password == b'123456' : f=open ("./flag" ,"r" ) flag=f.read() f.close() message = "You have logged in with admin privileges, here is your flag: " +flag else : message = "Admin login failed, please try again" else : message = "You have logged in as a regular user" else : message = "Login failed, please try again" except : message = "Login wrong, please try again" finally : login_attempts -= 1 elif action == 'restart' : initialize_globals() message = "System has been restarted. AES key and function counts have been reset." else : message = "Unsupported Media Type: Content-Type must be application/json" return jsonify({'message' : message}) return render_template('index.html' , message=message) if __name__ == '__main__' : app.run(host="0.0.0.0" )
题目基于AES的CFB分组模式,简单叙述下题意:
靶机初始会随机生成key和iv,有500次login的机会,用完之后必须要刷新key和iv才能继续登录
用register的话,可以选择username和password注册,限制如下:
username不能是admin
password长度要大于等于8
username和password都必须是可打印字符
符合上述限制的话,靶机就会生成如下消息:
1 f"{username} \x00{password} \x01\x02\x03"
并用当前的key和iv,基于AES的CFB模式加密,发送给我们密文,作为token
用login,可以输入username、password和token尝试登录,当且仅当满足下列条件时获得flag:
username是admin
password是123456
我们的任务是对token做篡改,使得其解密后是我们需要的内容,而这种篡改很类似于CBC的字节翻转,似乎并没有多大难度。
但实际尝试的时候,发现CFB和CBC模式在pycryptodome的实现有很大不同,CFB模式并没有完全按照分组来,而是有一个参数为segment_size,搜索一下发现了这篇文章:
Flash CTF - Cleverly Forging Breaks - MetaCTF
里面有对实际实现的详细解释:
简而言之,这种实现的影响在于——修改当前分组第i个字节的密文,那么解密有如下影响:
当前分组第i个字节以及当前分组后续的所有字节
下一个分组的所有字节
所以直接字节翻转不太行得通,而一个漏洞点在于他split的方式:
1 2 decrypted = decrypt(token, key) token_username, *_, token_password = decrypted.split(b'\x00' )
这样split的话相当于是把一串消息的第一个\x00和最后一个\x00内部的内容全部去掉,只留下头和尾作为token_username和token_password,一个具体例子就是:
1 2 3 4 5 if (1 ): t = b"msg\x00msg\x00msg\x00123456\x01\x02\x03" token_username, *_, token_password = t.split(b"\x00" ) print (token_username) print (token_password)
1 2 b'msg' b'123456\x01\x02\x03'
这提供了一个很重要的思路就是:我们只需要修改两个字节使得其为\x00,那么中间任何内容都会被去掉,也就是说我们实际上只需要完成两个字节的篡改即可!
基于这个思路,可以构造如下username和password,问号代表任意字节的意思:
1 2 admiN + ??...?? (pad to 45 bytes ) ??123456
此时显然是满足题目要求的,那么靶机加密的就是如下明文,共四个分组:
1 admiN??...? | ??...? | ??...?\x00?? | 123456 \x01\x02\x03
我们就能获取这个明文对应的CFB密文,接下来就是如何篡改他。
part1 首先是要把用户名改成admin,这很简单,由于CFB的解密方式,只需要对密文第五个字节对应做异或就可以修改正确。
part2 修改了第一个分组的第五字节之后:
第一个分组的后续字节全部会被影响
第二个分组的所有字节全部会被影响
第三、四个分组不受影响
所以此时解密出来的明文应该是:
1 admin??...? | ??...? | ??...?\x00?? | 123456 \x01\x02\x03
由于split的漏洞,我们现在只需要把下面两个@的位置改成\x00即可:
1 admin@?...? | ??...? | ??...?\x00?@ | 123456 \x01\x02\x03
第一个位置修改很简单,虽然我们不知道具体该改成多少,但是爆破256个字符总有一个会将他修改为\x00,所以我们可以认为在256次login内,一定有一次解密会是:
1 admin\x00?...? | ??...? | ??...?\x00?@ | 123456 \x01\x02\x03
part3 此时只剩最后一步,就是把最后一个@改成\x00即可。而为了修改它,最简单的办法是随机修改第二个分组的最后一个字节,那么有1/256的概率可以得到:
1 admin\x00?...? | ??...? | ??...?\x00?\x00 | 123456 \x01\x02\x03
就可以通过login得到flag了。
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 36 37 38 39 40 from Crypto.Util.number import *from tqdm import *import requestsimport jsonwhile (1 ): base_url = "http://node7.anna.nssctf.cn:27901/" msg = {'action' : 'restart' } response = requests.post(base_url, json=msg) username = 'admiN' + "1" *(11 +16 +13 ) password = 'ab123456' msg = {'action' : 'register' , 'username' : username, 'password' : password} response = requests.post(base_url, json=msg) token = (json.loads(response.text))["message" ].strip("Please save your token: " ).strip().zfill(2 *(len (username)+len (password)+4 )) token = bytes .fromhex(token) for i in trange(256 ): token_ = token[:4 ] + long_to_bytes(token[4 ] ^ ord ("N" ) ^ ord ("n" )) + long_to_bytes(i,1 ) + token[6 :] msg = token_ data = {'action' : 'login' , 'username' : 'admin' , 'password' : 'ab123456' , 'token' : msg.hex ()} response = requests.post(base_url, json=data) res = (json.loads(response.text))["message" ] if ("Login failed" not in res): for j in trange(256 ): msg = token_[:31 ] + long_to_bytes(token_[31 ] ^ j) + token_[32 :] data = {'action' : 'login' , 'username' : 'admin' , 'password' : '123456' , 'token' : msg.hex ()} response = requests.post(base_url, json=data) res = (json.loads(response.text))["message" ] if ("failed" not in res): print (res) exit()
revenge 春哥改了一下出了个revenge,revenge里做了如下限制:
1 2 elif action == 'restart' : message = "Uh-oh, this function is no longer available :("
也就是说开启容器后,总共就只有777次机会可以尝试login,用完了这个容器就完了,必须要新开容器才行。
硬碰运气的方法当然还可以用,只是会麻烦很多,777次机会里总共可以完成3次完整的256字节爆破,而平均需要256次才会成功一次,所以要碰运气的话平均需要反复关闭/打开容器$\frac{256}{3}$次,多少还是笨了一点,所以要想其他办法才行。
而注意到每次尝试login,返回信息都会根据具体情况有很大的不同:
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 elif action == 'login' : if login_attempts <= 0 : message = "Too many attempts" else : username = data.get('username' ).encode() password = data.get('password' ).encode() token = data.get('token' ) try : decrypted = decrypt(token, key) token_username, *_, token_password = decrypted.split(b'\x00' ) assert (token_password[-3 :]==b"\x01\x02\x03" ) token_password=token_password[:-3 ] if username == token_username and password == token_password: if username == b'admin' : if password == b'123456' : f=open ("./flag.txt" ,"r" ) flag=f.read() f.close() message = "You have logged in with admin privileges, here is your flag: " +flag else : message = "Admin login failed, please try again" else : message = "You have logged in as a regular user" else : message = "Login failed, please try again" except : message = "Login wrong, please try again"
而revenge的思路就是利用这些返回信息的差异去分别爆破username和password,为此依然还是做如下构造:
1 2 admiN + ??...?? (pad to 45 bytes ) ab123456
此时靶机加密的是:
1 admiN??...? | ??...? | ??...?\x00ab | 123456 \x01\x02\x03
修改密文使得N为n,并且爆破n后面的一个字节,那么在256次机会里,一定会有一次满足解密得到的结果为:
1 admin\x00?...? | ??...? | ??...?\x00ab | 123456 \x01\x02\x03
这个时候注意,我们不拿admin和123456去直接login,而是用admin和ab123456去login! ,那么在爆破得到上述信息时,返回的消息应该是:
1 "Admin login failed, please try again"
此时我们保持该密文不变,去爆破第二组的最后一个字节使得解密后为:
1 admin\x00?...? | ??...? | ??...?\x00a\x00 | 123456 \x01\x02\x03
并且拿admin和123456去login就可以拿到flag了,这样做在最差的情况也只需要256+256次,所以777次完全足够。
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 36 37 38 39 40 from Crypto.Util.number import *from tqdm import *import requestsimport jsonwhile (1 ): base_url = "http://node6.anna.nssctf.cn:21152/" msg = {'action' : 'restart' } response = requests.post(base_url, json=msg) username = 'admiN' + "1" *(11 +16 +13 ) password = 'ab123456' msg = {'action' : 'register' , 'username' : username, 'password' : password} response = requests.post(base_url, json=msg) token = (json.loads(response.text))["message" ].strip("Please save your token: " ).strip().zfill(2 *(len (username)+len (password)+4 )) token = bytes .fromhex(token) for i in trange(256 ): token_ = token[:4 ] + long_to_bytes(token[4 ] ^ ord ("N" ) ^ ord ("n" )) + long_to_bytes(i,1 ) + token[6 :] msg = token_ data = {'action' : 'login' , 'username' : 'admin' , 'password' : 'ab123456' , 'token' : msg.hex ()} response = requests.post(base_url, json=data) res = (json.loads(response.text))["message" ] if ("Login failed" not in res): for j in trange(256 ): msg = token_[:31 ] + long_to_bytes(token_[31 ] ^ j) + token_[32 :] data = {'action' : 'login' , 'username' : 'admin' , 'password' : '123456' , 'token' : msg.hex ()} response = requests.post(base_url, json=data) res = (json.loads(response.text))["message" ] if ("failed" not in res): print (res) exit()