加密过程
1、首先将明文分组(常见的以16字节为一组),位数不足的使用特殊字符填充。
2、生成一个随机的初始化向量(IV)和一个密钥。
3、将IV和第一组明文异或。
4、用key对3中xor后产生的密文加密。
5、用4中产生的密文对第二组明文进行xor操作。
6、用key对5中产生的密文加密。
7、重复4-7,到最后一组明文。
8、将IV和加密后的密文拼接在一起,得到最终的密文。
解密过程
(加密的逆过程)
设明文为X,密文为Y,解密函数为k。
X[i] = k(Y[i]) Xor Y[i-1]
对于CBC模式的解密算法,每一组明文进行分组算法解密之后,需要和前一组的密文异或才能得到明文。第一组则是和初始向量IV进行异或。
CBC字节翻转攻击的核心原理是通过破坏一个比特的密文来篡改一个比特的明文。攻击流程可参考下图:
可知: A ⊕ B = C 若想要改变输出的明文 C ,那么只需改变密钥 A 即可 那么要如何改变 A 呢? 我们从明文 C 入手,假设改变后的 C 为 C ′ , A 为 A ′ ,那么: 可知:A\oplus B = C\\若想要改变输出的明文C,那么只需改变密钥A即可\\那么要如何改变A呢?\\我们从明文C入手,假设改变后的C为C^{\prime},A为A^{\prime},那么: 可知:A⊕B=C若想要改变输出的明文C,那么只需改变密钥A即可那么要如何改变A呢?我们从明文C入手,假设改变后的C为C′,A为A′,那么:
C ′ = C ⊕ C ⊕ C ′ = A ⊕ B ⊕ C ⊕ C ′ = B ⊕ A ⊕ C ⊕ C ′ 令 A ′ = A ⊕ C ⊕ C ′ ⟹ A ′ ⊕ B = C ′ ( 翻转成功 ) ( 注意 : B 不管怎么样都是不变的 ) \begin{aligned}C^{\prime}&=C\oplus C\oplus C^{\prime}\\&=A\oplus B\oplus C\oplus C^{\prime}\\&=B\oplus A\oplus C\oplus C^{\prime}\\&令A^{\prime}=A\oplus C\oplus C^{\prime}\\&\Longrightarrow A^{\prime}\oplus B=C^{\prime}(\text{翻转成功})\end{aligned}\\(注意:B不管怎么样都是不变的) C′=C⊕C⊕C′=A⊕B⊕C⊕C′=B⊕A⊕C⊕C′令A′=A⊕C⊕C′⟹A′⊕B=C′(翻转成功)(注意:B不管怎么样都是不变的)
总结: 只要把 A 改变成 A ′ = A ⊕ C ⊕ C ′ 便能将输出的明文从 C 变为 C ′ 总结:\\只要把A改变成A^{\prime} = A\oplus C\oplus C^{\prime}\\便能将输出的明文从C变为C^{\prime} 总结:只要把A改变成A′=A⊕C⊕C′便能将输出的明文从C变为C′
import os
from Crypto.Cipher import AES
auth_major_key = os.urandom(16)
from flag import secret
BANNER = """
Login as admin to get the flag !
"""
MENU = """
Enter your choice
[1] Create NewStarCTF Account
[2] Create Admin Account
[3] Login
[4] Exit
"""
print(BANNER)
def bxor(b1, b2): # use xor for bytes
result = b""
for b1, b2 in zip(b1, b2):
result += bytes([b1 ^ b2])
return result
while True:
print(MENU)
option = int(input('> '))
if option == 1:
auth_pt = b'NewStarCTFer____'
user_key = os.urandom(16)
cipher = AES.new(auth_major_key, AES.MODE_CBC, user_key)
code = cipher.encrypt(auth_pt)
print(f'here is your authcode: {user_key.hex() + code.hex()}')
elif option == 2:
print('GET OUT !!!!!!')
elif option == 3:
authcode = input('Enter your authcode > ')
user_key = bytes.fromhex(authcode)[:16]
code = bytes.fromhex(authcode)[16:]
cipher = AES.new(auth_major_key, AES.MODE_CBC, user_key)
auth_pt = cipher.decrypt(code)
if auth_pt == b'AdminAdmin______':
a = user_key.hex() + code.hex()
print(a)
elif auth_pt == b'NewStarCTFer____':
print('Have fun!!')
else:
print('Who are you?')
elif option == 4:
print('ByeBye')
exit(0)
else:
print("WTF")
可以看出只要明文输出为 可以看出只要明文输出为 可以看出只要明文输出为b’AdminAdmin______‘, 即可得到 a , a 便是我们需要的 f l a g 即可得到a,a便是我们需要的flag 即可得到a,a便是我们需要的flag
现在我们已知①变换后的明文 现在我们已知①变换后的明文 现在我们已知①变换后的明文b’NewStarCTFer____‘ ②变换前的明文 ②变换前的明文 ②变换前的明文b’AdminAdmin______‘ ③密文④ i v ,毫无疑问 C B C 字节翻转攻击 ③密文④iv,毫无疑问CBC字节翻转攻击 ③密文④iv,毫无疑问CBC字节翻转攻击
A ′ = A ⊕ C ⊕ C ′ ,其中: A^{\prime} = A\oplus C\oplus C^{\prime},其中: A′=A⊕C⊕C′,其中:
C = b’NewStarCTFer____’
C’ = b’AdminAdmin______’
A = iv
A,C,C’都知道了,那么A’也就知道了,这题很容易就出来了
def strxor(a1, a2):
return bytes([b1 ^ b2 for b1,b2 in zip(a1,a2)])
authcode =
user_key = bytes.fromhex(authcode)[:16]
code = bytes.fromhex(authcode)[16:]
user_key=strxor(user_key,b'AdminAdmin______')
user_key=strxor(user_key,b'NewStarCTFer____')
authcode=user_key.hex()+code.hex()
print(authcode)
变换后的authcode出来了提交即可得到flag
import socketserver
import os, sys, signal
import string, random
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad,unpad
from secret import flag
key = os.urandom(32)
def decrypt(ciphertext,iv):
cipher = AES.new(key, AES.MODE_CBC, iv)
print(cipher.decrypt(ciphertext))
decrypted = unpad(cipher.decrypt(ciphertext),16)
if decrypted[23:31]==b'lingfeng' and decrypted[40:46]==b'123456':
a=b'welcome!,you are right!\n this is your flag\n'
return a+flag
else:
return decrypted
def encrypt(c):
iv = os.urandom(16)
payload = pad(c,16)
cipher = AES.new(key, AES.MODE_CBC, iv)
encrypted = cipher.encrypt(payload)
return iv.hex() + encrypted.hex()
class Task(socketserver.BaseRequestHandler):
def _recvall(self):
BUFF_SIZE = 2048
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):
if type(msg) is str:
msg = msg.encode()
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 close(self):
self.send(b"Bye~")
self.request.close()
def handle(self):
menu = '''1.register\n2.login\n'''
for _ in range(3):
self.send('\n' + menu)
try:
r = int(self.recv())
except:
continue
if r == 1:
self.send(b'please input your username')
username=self.recv().strip()
if username==b'lingfeng':
self.send(b'no,you is not lingfeng')
continue
self.send(b'please enter your password')
password=self.recv().strip()
m1=b'{"permission":username:'+username+b'password:'+password+b'}'
self.send(encrypt(m1))
elif r == 2:
self.send(b'please enter your encrypted: ')
m2=self.recv().strip().decode()
iv=bytes.fromhex(m2[:32])
data=bytes.fromhex(m2[32:])
self.send(decrypt(data, iv))
else:
self.send(b'please input again')
self.close()
class ThreadedServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
pass
class ForkedServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
pass
if __name__ == "__main__":
HOST, PORT = '0.0.0.0', 80
server = ForkedServer((HOST, PORT), Task)
server.allow_reuse_address = True
server.serve_forever()
(PS:非预期解,username = lingfeng111111111123456 即可得到,不是本文重点,下面以预期解来讲述)
本题流程和上题一样, u s e r n a m e 自己随便输,但不能是 l i n g f e n g 假设 u s e r n a m e = 0 i n g f e n g 我们要通过 C B C 字节翻转将最终明文 0 i n g f e n g 变为 l i n g f e n g 本题流程和上题一样,username自己随便输,但不能是lingfeng\\假设username = 0ingfeng\\我们要通过CBC字节翻转将最终明文0ingfeng变为lingfeng 本题流程和上题一样,username自己随便输,但不能是lingfeng假设username=0ingfeng我们要通过CBC字节翻转将最终明文0ingfeng变为lingfeng
现在我们已知①变换后的明文 现在我们已知①变换后的明文 现在我们已知①变换后的明文b’lingfeng’ ②变换前的明文 ②变换前的明文 ②变换前的明文b’0ingfeng’ ③密文④ i v ,毫无疑问 C B C 字节翻转攻击 ③密文④iv,毫无疑问CBC字节翻转攻击 ③密文④iv,毫无疑问CBC字节翻转攻击
A ′ = A ⊕ C ⊕ C ′ ,其中: A^{\prime} = A\oplus C\oplus C^{\prime},其中: A′=A⊕C⊕C′,其中:
C = b’0’
C’ = b’l‘
A = iv
(因为0ingfeng和lingfeng后面一部分一样,不需要翻转,翻转首字母即可)
注意!!!此处的iv是前一块密文,并不是初始向量
分块为1-16,17-32,此时0的位置为24,那么前一个密文相对应的位置是8,bytes转16进制长度变为2倍,在输出的encrypt中前32位是初始向量,encrypt[46,48]是0的前一个密文相对应的位置(好吧,确实有些拗口),总之iv不一定是初始向量,看流程图也能知道iv总是变化的
我的(哈哈哈,太low了):
encrypt = '598a4ebb9cd737b35e64b64522a2a0eb9d324530fda1dd2a1f712dc1b2b80291912eb35dc912ae1359b5affc6aebc989b66a55437c0ecf76246f37ccbf99ccf0'
password = 123456
iv_bytes = bytes.fromhex(encrypt[:32])
encrypt = 'b14bcfb810c6df7c2241ab1f9d37daace87ab04a67fe9dfd89835d0212078c0ade230b479e7eaf409c39d167062941f0ef17a798c440a1df616d15761a2833cb'
new_encrypt = encrypt[:46] + hex(int(encrypt[46:48],16) ^ ord('0') ^ ord('l'))[2:] + encrypt[48:]
print(new_encrypt)
官方的:
from pwn import *
sh = remote('39.105.144.62', 718)
sh.sendlineafter(b'>', b'1')
sh.sendlineafter(b'>', b'1ingfeng')
sh.sendlineafter(b'>', b'123456')
a = sh.recvline().decode().strip()
a = a[:32]+a[32:46]+hex(int(a[46:48],16)^ord('1')^ord('l'))[2:]+a[48:]
sh.sendlineafter(b'>', b'2')
sh.sendlineafter(b'>', a.encode())
sh.interactive() # flag{31be9656-81bb-4e8b-9d5e-db84f21184b2}
做题过程中发现一个好玩的
for i in b'kdlsjf':
print(i) # 107 100 108 115 106 102
for b1, b2 in zip(b'dsk', b'kdj'):
print(b1, b2)
输出结果是每个字节对应的整数值,我说怎么有时字节异或报错,有时又不报错,原来是这个原因
还发现一个好玩的
long_to_bytes(8) ①
bytes(8) ②
bytes([8]) ③
# 输出结果:
b'\x08'
b'\x00\x00\x00\x00\x00\x00\x00\x00'
b'\x08'
①③输出结果相同,这我是知道的,但②真不怎么用,不太了解,我说怎么用bytes()还给我报错,原来是这个原因,哈哈
long_to_bytes():用于将长整数转换为对应的字节对象。在这个例子中,将整数 8 转换为字节对象。
bytes():可以创建一个指定长度的字节对象,其中每个字节初始化为零。在这个例子中,创建了一个长度为 8 的字节对象。
bytes([ ]):不同的是,它接受一个整数列表作为参数,列表中的整数会被转换为相应的字节值。在这个例子中,创建了一个只包含一个字节值为8 的字节对象
又给我学到了,继续加油!