prev = 0x34; for (i = 0; i < len; i++) { tmp[i] = input[i] ^ prev; prev = input[i]; }第二轮继续链式异或,并把结果与.data段中的 32 字节常量比较:
for (i = 0; i < len; i++) { old = tmp[i]; tmp[i] ^= prev; prev = old; }目标常量是:
66 0a 09 e0 e2 e3 cb 09 14 15 0c 38 01 1f 05 42 71 6e 56 7a 00 20 e4 bf e6 cd 28 30 2c 75 a0 3a如果比较成功,驱动不会返回真正的新字符串,而是返回:
flag is you input这句话很关键。它说明 flag 不是驱动输出内容,而是用户输入本身。也就是说,攻击目标变成了:构造一个 32 字节输入,使它经过两层变换后等于.data段常量。
先逆第二层校验。由于第二层是链式异或,异或本身可逆,所以可以从目标常量倒推出第一层变换后的中间值。逆推得到的 32 字节中间态是:
2f 3e 26 de c4 3d 0f 34 1b 21 17 19 16 06 13 44 62 2a 34 50 34 70 d0 cf 36 02 1e 32 32 47 92 7d接下来逆第一层变换。第一层有这个结构:
buf[i - 1] = original[i - 1] ^ f(original[i - 1], original[i])因为它依赖右侧相邻字节,所以从最后一个字节开始向前回溯最自然。末尾buf[31]没被第一层循环修改,因此可以直接确定原始输入最后一字节。然后逐位向前枚举original[i - 1],要求它变换后等于已知中间态。
回溯后得到多个数学上可行的候选,但大多数包含不可打印字符或不符合 flag 格式。唯一符合常见 CTF flag 形态、且完整可打印的是:
flag{wnNCZJbBOqL3QA1C1cypiKYII4}为了避免只靠格式猜测,还需要做正向验证。把这个候选作为原始输入,先执行驱动第一层变换,得到:
2f3e26dec43d0f341b21171916061344622a34503470d0cf36021e323247927d再执行第二层校验变换,得到:
660a09e0e2e3cb0914150c38011f0542716e567a0020e4bfe6cd28302c75a03a它与.data段目标常量完全一致,因此该输入一定会命中成功分支。
最终驱动成功分支提示flag is you input,说明正确输入就是 flag。经过逆向两层变换并正向验证,最终 flag 为:
flag{wnNCZJbBOqL3QA1C1cypiKYII4}SimpleSocket
一个轻量级客户端与服务端程序通过自定义通信流程保护返回信息。请逆向分析交换逻辑,还原最终内容。
题目目录中有四个文件:py、packet1、packet2、packet3。
先看py
from Crypto.PublicKey import RSA from Crypto.Cipher import PKCS1_OAEP, AES from Crypto.Util.Padding import pad, unpad import os import socket import time def generate_rsa_keys(): key = RSA.generate(1024) #生成一对RSA密钥(1024位) private_key = key.export_key() #调用.export_key(),把key转换为文本格式 public_key = key.publickey().export_key() #只取出公钥部分 return private_key, public_key def client_logic(public_key): current_time = int(time.time()) aes_key = os.urandom(16) #生成16个字节(128位)的完全随机的数据,作为AES加密的密钥 cipher_rsa = PKCS1_OAEP.new(RSA.import_key(public_key)) encrypted_aes_key = cipher_rsa.encrypt(aes_key) #加密 with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.connect(('localhost', 9999)) s.sendall(encrypted_aes_key) encrypted_data = s.recv(1024) def server_logic(private_key): with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.bind(('localhost', 9999)) s.listen() conn, addr = s.accept() with conn: encrypted_aes_key = conn.recv(256) cipher_rsa = PKCS1_OAEP.new(RSA.import_key(private_key)) aes_key = cipher_rsa.decrypt(encrypted_aes_key) #解密 flag = b"this is flag" cipher_aes = AES.new(aes_key, AES.MODE_ECB) encrypted_flag = cipher_aes.encrypt(pad(flag, AES.block_size)) conn.sendall(encrypted_flag) private_key, public_key = generate_rsa_keys() import threading server_thread = threading.Thread(target=server_logic, args=(private_key,)) server_thread.start() client_logic(public_key)整体逻辑是用RSA加密AES密钥,再用AES加密数据
packet2: PEM 格式 RSA PRIVATE KEY,但换行被写成了字面量 \n packet3: 256 个十六进制字符,即 128 字节 packet1: 96 个十六进制字符,即 48 字节packet3的 128 字节长度正好对应 1024-bit RSA 密文;packet1的 48 字节长度是 16 的倍数,符合 AES 分组密文特征;packet2则是泄露的 RSA 私钥。
packet3 --RSA私钥解密--> AES key AES key + packet1 --AES-ECB解密--> flag注意packet1和packet3都是十六进制文本,不是原始二进制,所以使用前要先bytes.fromhex()。
packet2里的私钥不是标准 PEM 换行,而是包含字面量\n,所以导入 RSA 私钥前要替换成真实换行:
private_key = packet2_text.replace("\\n", "\n").encode()