香港人工智能网络安全挑战赛Writeup(初赛)-Reverse全解

admin 2026-07-10 07:37:20 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文为香港人工智能网络安全挑战赛初赛逆向题目的全解Writeup。作者详细解析了两道题目:第一题SimpleSocket通过分析Python源码与网络数据包,识别出RSA与AES混合加密流程,并发现数据包顺序错位,最终提取RSA私钥解密还原出flag;第二题CodeSign针对AndroidAPK进行逆向,发现其利用APK签名证书SHA1指纹作为XOR密钥,通过解析APK签名块结构提取v2签名证书并计算哈希,成功解密出flag。文章逻辑清晰,对密码学与移动端逆向有较高参考价值。 综合评分: 88 文章分类: 逆向分析,CTF,移动安全


cover_image

香港人工智能网络安全挑战赛Writeup(初赛)-Reverse全解

原创

Flowers aq Flowers aq

flower安全

2026年7月6日 19:29 甘肃

在小说阅读器读本章

去阅读

CyberSecurity

前言

香港AI x Cybersecurity Challenge人工智能挑战赛是由数字政策办公室、香港网络安全专业协会、中国网络空间研究院、中国移动香港有限公司主办的新兴网络安全赛事比赛采用动态计分机制,即先完成解题的队伍将获得较高分数。初赛为线上解题赛,决赛为线下地点为维多利亚港。

笔者队伍在本次挑战赛初赛中也取得了不错的成绩成功晋级线下,而笔者负责逆向和二进制分析两部分且都已全解,此篇文章为Reverse篇。

CyberSecurity

CyberSecurity

正文

一,SimpleSocket:

1,题目背景:

“A lightweight client-server program protects its returned message through a custom communication flow. Reverse the logic behind the exchange and recover the final content.一個輕量級用戶端與伺服器程式透過自訂通訊流程保護回傳訊息。請逆向分析交換邏輯,還原最終內容。”

题目给了我们一个轻量级客户端服务端程序,它通过自定义的加密通信流程来保护返回的消息。我们需要做的就是逆向分析这个交换逻辑,还原出最终的机密内容(flag)。

2,题目分析:

解压附件后,我们得到 4 个文件:

SimpleSocket.zip├── py          # Python 源码(1540 字节)├── packet1     # 数据包1(96 字节)├── packet2     # 数据包2(900 字节)└── packet3     # 数据包3(256 字节)

初步判断:

  • py = 服务端/客户端的参考实现代码

  • packet1、packet2、packet3 = 网络通信中捕获的原始数据包

这明显是一道给你源码 + 抓包数据,逆向解密的经典 CTF 题型。

首先打开 py 文件,一探究竟:

from Crypto.PublicKey import RSAfrom Crypto.Cipher import PKCS1_OAEP, AESfrom Crypto.Util.Padding import pad, unpadimport osimport socketimport time

导入了 RSA、AES、PKCS1_OAEP、pad/unpad经典的混合加密体系。

def generate_rsa_keys():    key = RSA.generate(1024)    private_key = key.export_key()    public_key = key.publickey().export_key()    return private_key, public_key

生成了一个 1024 位的 RSA 密钥对。

def client_logic(public_key):    current_time = int(time.time())         aes_key = os.urandom(16)                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)  # AES-ECB 模式            encrypted_flag = cipher_aes.encrypt(pad(flag, AES.block_size))            conn.sendall(encrypted_flag)

这一段属于服务端逻辑代码。

整个代码的主流程为:

private_key, public_key = generate_rsa_keys()import threadingserver_thread = threading.Thread(target=server_logic, args=(private_key,))server_thread.start()client_logic(public_key)

综上分析可以初步得知整体流程:客户端生成RSA密钥对、168 AES密钥K、用RSA公钥加密K得到了RSA_C(K),再发送RSA_C(K)到服务端,服务端用RSA私钥解密得到AES密钥K、AES-ECB又加密flag得到AES_C(flag),最后服务端反回AES_C(flag)到客户端,客户端再用AES密钥K解密得到flag。

它的核心思路就是:非对称加密(RSA)安全传输对称密钥(AES),再用对称密钥高效加密数据。

3,数据包分析:

首先看 packet2,它长这样(部分)

-----BEGIN RSA PRIVATE KEY-----\nMIICXAIBAAKBgQC5hMOyHMbH2no95Br5xHuurgcxzAcK2fULAPQGLkSz5St8Te2L\nG8jUnxjnbOUF1a/72s3rOkZH8IbtUsTvtrcg/WUlyMWSjpiqhbpPNxGXbDQs2KyP\n...-----END RSA PRIVATE KEY-----

这里的 \n 是字面量字符(反斜杠 + 字母 n),而不是真正的换行符。用 Python 查看确认:

>>> with open('packet2', 'r') as f:...     data = f.read()>>> data[:100]'-----BEGIN RSA PRIVATE KEY-----\\nMIICXAIBAAKBgQC5hMOyHMbH2no95Br5xHuurgcxzAcK2fULAPQGLkSz5St8Te2L\\nG'

一共 14 处 \n,需要替换为真正的换行符才能得到标准 PEM 格式。 修复后的 PEM 私钥:

-----BEGIN RSA PRIVATE KEY----- MIICXAIBAAKBgQC5hMOyHMbH2no95Br5xHuurgcxzAcK2fULAPQGLkSz5St8Te2L G8jUnxjnbOUF1a/72s3rOkZH8IbtUsTvtrcg/WUlyMWSjpiqhbpPNxGXbDQs2KyP ... -----END RSA PRIVATE KEY-----

可知RSA密钥信息:

密钥长度:1024 bits模数 N:13027559903439716274...(309 位十进制数)公钥指数 E:65537(0x10001)私钥指数 D:30169603970959981967...(309 位十进制数)

再来看看packet1这是加密数据包:

e41663ce2af0328f88f97fcc992a3e6d9f1f8a0de3b5b0c47fcc2e439db4a71a48b46de06059a701d5ff4cac88ace640
  • 纯 ASCII 文本,内容是 96 个十六进制字符

  • 解码后 = 48 字节原始数据

同样是加密数据包的packet3:

4560c76721e670ecde8b553a112ae052060a478c83ca3806c2eb39eb8da2b80f7dccb14fa985b23fe78c6dfc891897bd...
  • 同样是纯 ASCII 文本,256 个十六进制字符

  • 解码后 = 128 字节原始数据

参考代码告诉我们:

1.客户端发送 → RSA 加密的 AES 密钥(encrypted_aes_key)

2.服务端返回 → AES-ECB 加密的 Flag(encrypted_flag)

  • 按理说: RSA-1024 加密输出 = 128 字节(1024 bits ÷ 8)

  • AESECB 加密输出 = 16 的整数倍。Flag 约 40+ 字节,pad 后约 48 字节(3 个 AES 块)

然而实际数据包中:packet1为48字节太小了(本来需 128B),但它的AES密文又恰好为48B,packet3为128 字节正好128B但它的AES密文又太大了,所有猜测packet1 和 packet3 在抓包文件中的角色被交换了。

4.解题过程:

第一步:提取 RSA 私钥

from Crypto.PublicKey import RSAwith open('packet2', 'r') as f:    raw_key = f.read().strip()# 替换字面量 \n 为真正的换行符pem_key = raw_key.replace('\\n', '\n')private_key = RSA.import_key(pem_key)print(f"RSA Key Size: {private_key.size_in_bits()} bits")# 输出: RSA Key Size: 1024 bits

第二步:解密 packet3 得到 AES 密钥

from Crypto.Cipher import PKCS1_OAEP# 读取 packet3with open('packet3', 'r') as f:    hex_data = f.read().strip()encrypted_aes_key = bytes.fromhex(hex_data)# RSA-OAEP 解密cipher_rsa = PKCS1_OAEP.new(private_key)aes_key = cipher_rsa.decrypt(encrypted_aes_key)print(f"AES Key (hex): {aes_key.hex()}")print(f"AES Key length: {len(aes_key)} bytes")

输出:

AES Key (hex): f9134243d20e0a1f4ab78466015d5efeAES Key length: 16 bytes

解密成功得到了的AES密钥f9134243d20e0a1f4ab78466015d5efe。

第三步:解密 packet1 得到 Flag

from Crypto.Cipher import AESfrom Crypto.Util.Padding import unpad# 读取 packet1with open('packet1', 'r') as f:    hex_data = f.read().strip()encrypted_flag = bytes.fromhex(hex_data)# AES-ECB 解密cipher_aes = AES.new(aes_key, AES.MODE_ECB)decrypted = cipher_aes.decrypt(encrypted_flag)# 去除 PKCS7 填充flag = unpad(decrypted, AES.block_size)print(f"Flag: {flag.decode()}")

输出:

Flag: flag{28b0e93d-1b8c-40ff-9075-2049102f1e26}

二,CodeSign:

1.题目描述:

“A protected message is mixed with a long stream of seemingly random data. Determine what is truly random, recover the missing context, and decrypt the hidden flag. 一段受保護的訊息被混入大量看似隨機的資料之中。請判斷其中的規律,還原缺失的上下文並解開隱藏的 flag。”

附件:SecureGate.apk

2.解题过程:

2.1.第一步:反编译APK 定位解密逻辑

$ unzip SecureGate.apk -d apk_contents$ jadx -d decompiled SecureGate.apk

目标代码:com.icqctf.signcheck 包下两个核心文件。

2.2.第二步:分析 MainActivity — 发现XOR解密

// 29字节密文private&nbsp;static&nbsp;final&nbsp;byte[] SECRET_DATA = {&nbsp; &nbsp;&nbsp;86,&nbsp;10,&nbsp;3,&nbsp;1,&nbsp;77,&nbsp;124,&nbsp;123,&nbsp;97,&nbsp;109,&nbsp;37,&nbsp;64,&nbsp;90,&nbsp;2,&nbsp;89,&nbsp;8,&nbsp;5,&nbsp; &nbsp;&nbsp;111,&nbsp;115,&nbsp;64,&nbsp;66,&nbsp;4,&nbsp;16,&nbsp;65,&nbsp;62,&nbsp;123,&nbsp;8,&nbsp;88,&nbsp;81,&nbsp;30};// 循环XOR解密private&nbsp;String&nbsp;decrypt(byte[] cipher, String keyStr)&nbsp;{&nbsp; &nbsp;&nbsp;byte[] key = keyStr.getBytes();&nbsp; &nbsp;&nbsp;byte[] plain =&nbsp;new&nbsp;byte[cipher.length];&nbsp; &nbsp;&nbsp;for&nbsp;(int&nbsp;i&nbsp;=&nbsp;0; i < cipher.length; i++) {&nbsp; &nbsp; &nbsp; &nbsp; plain[i] = (byte)(cipher[i] ^ key[i % key.length]);&nbsp; &nbsp; }&nbsp; &nbsp;&nbsp;return&nbsp;new&nbsp;String(plain);}// 调用String&nbsp;result&nbsp;=&nbsp;decrypt(SECRET_DATA, SignUtils.getAppSignature(this));if&nbsp;(result.startsWith("flag{")) {&nbsp;/* 解密成功 */&nbsp;}

关键发现:

  • 算法:循环XOR

  • 密文:SECRET_DATA(29字节)

  • 密钥:SignUtils.getAppSignature(this) 的返回值

2.3.第三步:分析 SignUtils — 密钥生成机制

public&nbsp;static&nbsp;String&nbsp;getAppSignature(Context&nbsp;context) {&nbsp; &nbsp;&nbsp;// 获取APK签名证书的原始DER字节&nbsp; &nbsp; byte[] certBytes = context.getPackageManager()&nbsp; &nbsp; &nbsp; &nbsp; .getPackageInfo(context.getPackageName(),&nbsp;64)&nbsp; &nbsp; &nbsp; &nbsp; .signatures[0].toByteArray();&nbsp; &nbsp;&nbsp;return&nbsp;hex(certBytes); &nbsp;// SHA1(DER) → 小写hex字符串}

解密公式为:

flag&nbsp;= SECRET_DATA ⊕ SHA1(X.509_CERT_DER_BYTES).hex()

也就是密钥 = APK签名证书的SHA1指纹(40字符小写hex字符串)。

第四步:提取APK签名证书

APK使用 APK Signature Scheme v2/v3,签名信息不在 META-INF/CERT.RSA,嵌入在了 APK Signing Block 中。所以像下面这样传统的提取办法会失效。

$&nbsp;keytool -printcert -jarfile SecureGate.apkNot a signed jar file

它的Signing Block数据结构为:

[8 bytes] &nbsp;block_size[16 bytes] magic:&nbsp;"APK Sig Block 42"[可变] &nbsp; &nbsp; ID-value&nbsp;对序列&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;[4B ID] &nbsp;[4B&nbsp;长度] &nbsp;[值]&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;0x7109871a&nbsp;= v2 Block&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;0xf05368c0&nbsp;= v3 Block&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;0x42726577&nbsp;= v3.1&nbsp;Block[8 bytes] &nbsp;block_size (重复)[16 bytes] magic:&nbsp;"APK Sig Block 42"&nbsp;(重复)

知道这些过后直接编写py脚本提取证书就行:

import&nbsp;struct, hashlibwith&nbsp;open("SecureGate.apk",&nbsp;"rb") as f:&nbsp; &nbsp;&nbsp;data&nbsp;= f.read()# 定位Signing Blockmagic&nbsp;= b'APK Sig Block&nbsp;42'pos&nbsp;= data.find(magic)block_size&nbsp;= struct.unpack('<Q', data[pos-8:pos])[0]block_start&nbsp;= pos -&nbsp;8&nbsp;- block_size# 遍历ID-value对offset&nbsp;= block_start +&nbsp;24while&nbsp;offset < block_start +&nbsp;24&nbsp;+ block_size:&nbsp; &nbsp;&nbsp;pair_id&nbsp;= struct.unpack('<I', data[offset:offset+4])[0]&nbsp; &nbsp;&nbsp;pair_len&nbsp;= struct.unpack('<I', data[offset+4:offset+8])[0]&nbsp; &nbsp;&nbsp;if&nbsp;pair_id ==&nbsp;0x7109871a: &nbsp;# v2签名块&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;v2&nbsp;= data[offset+8:offset+8+pair_len]&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 提取X.509证书(ASN.1 SEQUENCE标记 0x30 0x82)&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;idx&nbsp;=&nbsp;0&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;while&nbsp;idx < len(v2) -&nbsp;4:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;v2[idx:idx+2] == b'0\x82':&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;cert_len&nbsp;= struct.unpack('>H', v2[idx+2:idx+4])[0]&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;200&nbsp;< cert_len <&nbsp;3000:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;cert_der&nbsp;= v2[idx:idx+4+cert_len]&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;sha1&nbsp;= hashlib.sha1(cert_der).hexdigest()&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f"证书 @{idx}, DER长度:{4+cert_len}, SHA1:{sha1}")&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;idx&nbsp;+=&nbsp;4&nbsp;+ cert_len&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;continue&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;idx&nbsp;+=&nbsp;1&nbsp; &nbsp;&nbsp;offset&nbsp;+=&nbsp;8&nbsp;+ pair_len

输出结果:

ICQCTF CA根证书

0fbf65802a94649f01920c2a0966c2934e817f73

签名子证书

10da7c0bcabc2ac73130adea122a7bba7f2f2740

2.4.最后一步获取flag:XOR解密

SECRET_DATA = [86,10,3,1,77,124,123,97,109,37,64,90,2,89,8,5,&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;111,115,64,66,4,16,65,62,123,8,88,81,30]sha1_root =&nbsp;"0fbf65802a94649f01920c2a0966c2934e817f73"key = sha1_root.encode()flag = bytes([SECRET_DATA[i] ^ key[i % 40] for i in range(29)])print(flag.decode())

完整解密脚本:

import&nbsp;structimport&nbsp;hashlibwith&nbsp;open("SecureGate.apk",&nbsp;"rb") as f:&nbsp; &nbsp;&nbsp;data&nbsp;= f.read()magic&nbsp;= b'APK Sig Block&nbsp;42'pos&nbsp;= data.find(magic)block_size&nbsp;= struct.unpack('<Q', data[pos-8:pos])[0]block_start&nbsp;= pos -&nbsp;8&nbsp;- block_sizeoffset&nbsp;= block_start +&nbsp;24while&nbsp;offset < block_start +&nbsp;24&nbsp;+ block_size:&nbsp; &nbsp;&nbsp;pair_id&nbsp;= struct.unpack('<I', data[offset:offset+4])[0]&nbsp; &nbsp;&nbsp;pair_len&nbsp;= struct.unpack('<I', data[offset+4:offset+8])[0]&nbsp; &nbsp;&nbsp;if&nbsp;pair_id ==&nbsp;0x7109871a:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;v2&nbsp;= data[offset+8:offset+8+pair_len]&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;idx&nbsp;=&nbsp;0&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;while&nbsp;idx < len(v2) -&nbsp;4:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;v2[idx:idx+2] == b'0\x82':&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;cert_len&nbsp;= struct.unpack('>H', v2[idx+2:idx+4])[0]&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;200&nbsp;< cert_len <&nbsp;3000:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;cert_der&nbsp;= v2[idx:idx+4+cert_len]&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;sha1&nbsp;= hashlib.sha1(cert_der).hexdigest()&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;SECRET&nbsp;=&nbsp;[86,10,3,1,77,124,123,97,109,37,64,90,2,89,8,5,&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 111,115,64,66,4,16,65,62,123,8,88,81,30]&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;key&nbsp;= sha1.encode()&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;flag&nbsp;= bytes([SECRET[i] ^ key[i%40] for i in range(29)])&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;flag.startswith(b'flag{'):&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f"FLAG: {flag.decode()}")&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;idx&nbsp;+=&nbsp;4&nbsp;+ cert_len&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;continue&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;idx&nbsp;+=&nbsp;1&nbsp; &nbsp;&nbsp;offset&nbsp;+=&nbsp;8&nbsp;+ pair_len

成功获取flag:

flag{ICQ_Dyn4m1c_Byp4ss_K1ng}

三,Drive:

1.题目描述:

“The company’s driver program seems to have a bug, and the connected program cannot connect. To reconnect to the driver, you need to re-enter the key. Please fix the program and connect to the driver as soon as possible! (Missing symbol md5=f2c6151d6c0d99f36666129b97e2100f5) 公司的驅動程序好像出bug了,連接的程序也連接不上,重新連接驅動還需要重新輸入密鑰,請儘快修復程序連接驅動!(缺失的符號md5=f2c6151d6c0d99f3666129b97e2100f5)。”

题目附件是一个zip包,里面有两个文件:

  • Driver.sys —— 驱动程序

  • link.exe —— 连接程序

根据题目描述其实这道题目就已经比较明确了:修复驱动程序,找到正确的密钥,获取flag。

2.解题过程:

2.1.第一步:文件初步分析

解压与识别

$&nbsp;unzip driver_xxx.zip&nbsp;-d&nbsp;driver_analysis/$&nbsp;cd&nbsp;driver_analysis/$&nbsp;file *Driver.sys:&nbsp;datalink.exe: &nbsp; PE32+ executable&nbsp;for&nbsp;MS Windows&nbsp;6.00&nbsp;(console), x86-64,&nbsp;11&nbsp;sections

link.exe是标准的PE32+ x86-64控制台程序,但 Driver.sys被识别为普通data正常的 .sys驱动应该是PE格式才对,这说明驱动文件被混淆了。

查看link.exe的导入表

先看看link.exe导入了哪些关键函数:

KERNEL32.dll:&nbsp; CreateFileA &nbsp; &nbsp; &nbsp; - 打开设备&nbsp; CloseHandle &nbsp; &nbsp; &nbsp; - 关闭句柄&nbsp; DeviceIoControl &nbsp; - 与驱动通信&nbsp; CreateFileW &nbsp; &nbsp; &nbsp; - 打开设备(Unicode)

关键字符串:

\\.\Device?????&nbsp; &nbsp; &nbsp;- 设备路径(名称被隐藏)flag:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;- 输出flaginput &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; - 提示输入

可以确定的是link.exe打开一个设备,向其发送IOCTL请求,验证密钥后输出flag。

2.2.第二步:破解Driver.sys的混淆

用十六进制查看Driver.sys的头部

00000000:&nbsp;09&nbsp;5a&nbsp;90&nbsp;00&nbsp;47&nbsp;00&nbsp;00&nbsp;00&nbsp;40&nbsp;00&nbsp;00&nbsp;00&nbsp;bb ff&nbsp;00&nbsp;00

正常的MZ头部应该是 4D 5A 90 00(”MZ”标记)。这里第一个字节是 0x09 而不是 0x4D。 尝试XOR分析: 0x09 ^ 0x4D = 0x44 再检查DOS stub区域(标准DOS消息区域):

原始:&nbsp;"!This program cannot be run in DOS mode."文件: &nbsp;"!Th-s p6ogr%m&nbsp;c%nno0&nbsp;bedrundin "

逐字节对比XOR值,发现了一个有趣的规律:

&nbsp;偏移 &nbsp;原始值 &nbsp; 文件值 &nbsp;XOR值&nbsp;&nbsp;0x00 &nbsp;0x4D (M) &nbsp;0x09 &nbsp; 0x44&nbsp;0x04 &nbsp;0x03 &nbsp; &nbsp; &nbsp;0x47 &nbsp; 0x44&nbsp;&nbsp;0x08 &nbsp;0x04 &nbsp; &nbsp; &nbsp;0x40 &nbsp;0x44&nbsp;0x0C &nbsp;0xFF &nbsp; &nbsp; &nbsp;0xBB &nbsp;0x44

每4字节中的第1个字节(offset % 4 == 0)被 XOR 0x44

编写解码脚本:

with&nbsp;open('Driver.sys',&nbsp;'rb')&nbsp;as&nbsp;f:&nbsp; &nbsp; data =&nbsp;bytearray(f.read())# 修复混淆:每4字节的第1个字节 XOR 0x44for&nbsp;i&nbsp;in&nbsp;range(0,&nbsp;len(data),&nbsp;4):&nbsp; &nbsp; data[i] ^=&nbsp;0x44with&nbsp;open('Driver_decoded.sys',&nbsp;'wb')&nbsp;as&nbsp;f:&nbsp; &nbsp; f.write(data)

验证解码结果

解码后文件头部:

00000000:&nbsp;4d&nbsp;5a&nbsp;90&nbsp;00&nbsp;03&nbsp;00&nbsp;00&nbsp;00&nbsp;04&nbsp;00&nbsp;00&nbsp;00&nbsp;ff ff&nbsp;00&nbsp;00

MZ 标记出现! 这是有效的PE文件头。再用PE解析工具查看:

Machine: &nbsp; &nbsp;&nbsp;0x8664 (x86-64)Sections: &nbsp; &nbsp;6Subsystem: &nbsp; Native (Windows驱动)EntryPoint: &nbsp;0x5000 (INIT节区)
&nbsp;节区 &nbsp; &nbsp;VA &nbsp; &nbsp; &nbsp;原始偏移 &nbsp;大小&nbsp;&nbsp;.text &nbsp;&nbsp;0x1000&nbsp;&nbsp;0x400&nbsp; &nbsp;&nbsp;0xA00&nbsp;&nbsp;.rdata &nbsp;0x2000&nbsp;&nbsp;0xE00&nbsp; &nbsp;&nbsp;0x600&nbsp;&nbsp;.data&nbsp; &nbsp;0x3000&nbsp;&nbsp;0x1400&nbsp; &nbsp;0x200&nbsp;&nbsp;INIT &nbsp; &nbsp;0x5000&nbsp;&nbsp;0x1800&nbsp; &nbsp;0x200

驱动修复成功

2.3.第三步:反汇编分析驱动逻辑

使用 objdump 反汇编解码后的驱动,找到了以下关键函数:

0x400 &nbsp;密钥校验函数(核心)&nbsp;0x510 &nbsp;设备创建分发例程 (IRP_MJ_CREATE)&nbsp;0x560 &nbsp;设备关闭分发例程 (IRP_MJ_CLOSE)&nbsp;0x5B0 &nbsp;设备控制分发例程 (IRP_MJ_DEVICE_CONTROL)&nbsp;0x1800 &nbsp;DriverEntry(驱动入口)

在 0x5B0 的设备控制函数中,识别出两个IOCTL代码:

cmp&nbsp;[rsp+0x28],&nbsp;0x222000 &nbsp; ; 密钥验证je &nbsp;0x649cmp&nbsp;[rsp+0x28],&nbsp;0x222400 &nbsp; ; 写操作je &nbsp;0x791

IOCTL 0x222000(密钥验证)的完整流程:

  1. 拷贝输入:从用户缓冲区拷贝最多32字节到栈上

  2. Scramble混淆:从i=1到31,对缓冲区进行变换

  3. 调用校验函数:调用地址0x400的函数验证结果

  4. 返回结果:成功返回”flag is you input”,失败返回”wrong”

关键引用字符串:

  • 0xB70: "flag is you input\0"(成功时的输出)

  • 0xBA0: "wrong\0"(失败时的输出)

在从从驱动中提取的Unicode字符串:

\??\DeviceDrive &nbsp; &nbsp; &nbsp;(符号链接名)\Device\MYDEVICE&nbsp; &nbsp; &nbsp;(设备名)

2.4.第四步:核心算法逆向

密钥验证由三层变换组成:第1层:Scramble混淆、第2层: 反馈XOR Pass 1、第3层: 反馈XOR Pass 2

第1层:Scramble混淆

这是IOCTL处理函数中的混淆循环(地址0x6A7-0x716):

; 循环 i = 1 到 316B1: mov eax, [rsp+0x20] &nbsp; &nbsp; &nbsp;; i6B5: inc eax &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; i++6C2: mov eax, [rsp+0x20] &nbsp; &nbsp; &nbsp;; i6C6: dec eax &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; i-16D8: movzx ecx, byte [rsp+rcx+0x60] &nbsp;; ecx = buffer[i-1]6DD: xor edx, edx6DF: mov eax, ecx &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; eax = buffer[i-1]6E1: mov ecx, 0x12 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; ecx = 186E6: div ecx &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; edx = buffer[i-1] % 186E8: mov eax, edx &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; eax = buffer[i-1] % 186EF: movzx ecx, byte [rsp+rcx+0x60] &nbsp;; ecx = buffer[i]6F4: lea eax, [rax+rcx+0x5] &nbsp; ; eax = (buffer[i-1]%18) + buffer[i] + 56F8: xor eax, 0x34 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; eax ^= 0x34700: movzx ecx, byte [rsp+rcx+0x60] &nbsp;; ecx = buffer[i-1]705: xor ecx, eax &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; ecx = buffer[i-1] ^ key712: mov [rsp+rcx+0x60], al &nbsp; &nbsp;; buffer[i-1] = result

用Python表示:

def&nbsp;scramble(buffer):&nbsp; &nbsp;&nbsp;n&nbsp;=&nbsp;32&nbsp; &nbsp;&nbsp;buf&nbsp;= list(buffer)&nbsp; &nbsp;&nbsp;for&nbsp;i in range(1, n):&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;prev&nbsp;= buf[i-1]&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;key&nbsp;= ((prev %&nbsp;18) + buf[i] +&nbsp;5) ^&nbsp;0x34&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;buf[i-1] = prev ^ key&nbsp; &nbsp;&nbsp;return&nbsp;buf

关键观察每个位置 i-1 只被修改一次,修改时使用的 buf[i-1] 是原始值(因为该位置之前未被修改过),而 buf[i] 也是原始值。最后一个字节 buf[31] 不变。

第2层:反馈XOR Pass 1(函数0x400)

425: mov byte [rsp+0x20], 0x34 &nbsp; ; key = 0x3442A: mov dword [rsp+0x28], 0 &nbsp; &nbsp; ; i = 0; 循环体448: movsxd rax, [rsp+0x28] &nbsp; &nbsp; &nbsp;; rax = i452: movzx eax, byte [rcx+rax] &nbsp; ; eax = buffer[i]45F: movzx ecx, byte [rsp+0x20] &nbsp;; ecx = key464: xor eax, ecx &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; eax = buffer[i] ^ key470: mov [rdx+rcx], al &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; temp[i] = buffer[i] ^ key478: mov [rsp+0x20], al &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; key = buffer[i] (反馈!)

Python表示:

def&nbsp;pass1(buffer):&nbsp; &nbsp; n =&nbsp;32&nbsp; &nbsp; key =&nbsp;0x34&nbsp; &nbsp; temp = [0] * n&nbsp; &nbsp;&nbsp;for&nbsp;i in&nbsp;range(n):&nbsp; &nbsp; &nbsp; &nbsp; temp[i] = buffer[i] ^&nbsp;key&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;key&nbsp;=&nbsp;buffer[i] &nbsp; &nbsp; &nbsp;&nbsp; &nbsp;&nbsp;return&nbsp;temp

第3层:反馈XOR Pass 2(函数0x400后半段)

def pass2(temp, buffer):&nbsp; &nbsp; n&nbsp;=&nbsp;32&nbsp; &nbsp; key&nbsp;=&nbsp;buffer[n-1] &nbsp;&nbsp; &nbsp;&nbsp;result&nbsp;=&nbsp;[0]&nbsp;*&nbsp;n&nbsp; &nbsp;&nbsp;for&nbsp;j&nbsp;in&nbsp;range(n):&nbsp; &nbsp; &nbsp; &nbsp; cur&nbsp;=&nbsp;temp[j]&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;result[j]&nbsp;=&nbsp;cur&nbsp;^&nbsp;key&nbsp; &nbsp; &nbsp; &nbsp; key&nbsp;=&nbsp;cur &nbsp; &nbsp; &nbsp; &nbsp;&nbsp; &nbsp;&nbsp;return&nbsp;result

2.5.第五步:逆向计算正确密钥

找到硬编码比较数据 指令 lea rdx, [rip+0x1F16] 中的目标地址是 VA 0x3000,对应 .data 节区的原始偏移 0x1400:

比较数据(32字节):66 0a 09 e0 e2 e3 cb 09 14 15 0c 38 01 1f 05 4271 6e 56 7a 00 20 e4 bf e6&nbsp;cd&nbsp;28 30 2c 75 a0 3a

逆向Pass 2和Pass 1 从比较数据 B[k] 逆向推导 S[k]:

S[1] &nbsp;= B[1] ^ 0x34 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;(奇数链起点)S[3] &nbsp;= B[3] ^ S[1]S[5] &nbsp;= B[5] ^ S[3]... &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; (递推奇数位置)S[31] = B[31] ^ S[29]S[0] &nbsp;= B[0] ^ 0x34 ^ S[31] &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; (偶数链起点)S[2] &nbsp;= B[2] ^ S[0]S[4] &nbsp;= B[4] ^ S[2]... &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; (递推偶数位置)

Python代码:

def&nbsp;reverse_pass2_pass1(B):&nbsp; &nbsp;&nbsp;n&nbsp;= len(B)&nbsp; &nbsp;&nbsp;S&nbsp;=&nbsp;[0] * n&nbsp; &nbsp; # 奇数链&nbsp; &nbsp; S[1] = B[1] ^ 0x34&nbsp; &nbsp; for k in range(3, n, 2):&nbsp; &nbsp; &nbsp; &nbsp; S[k] = B[k] ^ S[k-2]&nbsp; &nbsp;&nbsp;# 偶数链(需要先知道S[31])&nbsp; &nbsp;&nbsp;S[31] = B[31] ^ S[29]&nbsp; &nbsp;&nbsp;S[0] = B[0] ^&nbsp;0x34 ^ S[31]&nbsp; &nbsp;&nbsp;for&nbsp;k in range(2, n,&nbsp;2):&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;S[k] = B[k] ^ S[k-2]&nbsp; &nbsp;&nbsp;return&nbsp;S

逆向Scramble层(含DFS回溯)这是最棘手的部分。Scramble层的方程是:

S[k] = U[k] ^ (((U[k] %&nbsp;18) + U[k+1] +&nbsp;5) ^&nbsp;0x34)

我们需要从 S[k] 和已知的 U[k+1] 求出 U[k]。因为 U[k] % 18 只有18种可能,我们可以逐个尝试:

def&nbsp;find_candidates(S_k, next_val):&nbsp; &nbsp;&nbsp;"""找到所有满足条件的U[k]"""&nbsp; &nbsp; candidates = []&nbsp; &nbsp;&nbsp;for&nbsp;c&nbsp;in&nbsp;range(256):&nbsp; &nbsp; &nbsp; &nbsp; key = ((c %&nbsp;18) + next_val +&nbsp;5) ^&nbsp;0x34&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;c ^ key == S_k:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; candidates.append(c)&nbsp; &nbsp;&nbsp;return&nbsp;candidates

但问题是,每个位置可能有多个候选值有些候选会导致后续位置无解。因此需要使用深度优先搜索和回溯来找到正确的路径:

def&nbsp;dfs(k, U):&nbsp; &nbsp;&nbsp;"""从k位置向前回溯"""&nbsp; &nbsp;&nbsp;if&nbsp;k <&nbsp;0:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;True&nbsp;&nbsp;# 找到完整解&nbsp; &nbsp; next_val = U[k+1]&nbsp;if&nbsp;k <&nbsp;31&nbsp;else&nbsp;0&nbsp; &nbsp; candidates = find_candidates(S[k], next_val)&nbsp; &nbsp;&nbsp;for&nbsp;c&nbsp;in&nbsp;candidates:&nbsp; &nbsp; &nbsp; &nbsp; U[k] = c&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;dfs(k-1, U):&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;True&nbsp; &nbsp; U[k] =&nbsp;0&nbsp;&nbsp;# 回溯&nbsp; &nbsp;&nbsp;return&nbsp;FalseU = [0] *&nbsp;32U[31] = S[31] &nbsp;# 最后一位已知dfs(30, U) &nbsp; &nbsp;&nbsp;# 从倒数第二位开始回溯

验证结果

用户输入U[]:&nbsp;62&nbsp;6c&nbsp;61&nbsp;67&nbsp;7b&nbsp;77&nbsp;6e&nbsp;4e&nbsp;43&nbsp;5a&nbsp;4a&nbsp;62&nbsp;42&nbsp;4f&nbsp;71&nbsp;4c&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;33&nbsp;51&nbsp;41&nbsp;31&nbsp;43&nbsp;31&nbsp;63&nbsp;79&nbsp;70&nbsp;69&nbsp;4b&nbsp;59&nbsp;49&nbsp;49&nbsp;34&nbsp;7dASCII: &nbsp; &nbsp; &nbsp; b l a g &nbsp;{ &nbsp;w &nbsp;n &nbsp;N &nbsp;C &nbsp;Z &nbsp;J &nbsp;b&nbsp;&nbsp;B&nbsp; O &nbsp;q&nbsp; L&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;3&nbsp;&nbsp;Q&nbsp;&nbsp;A&nbsp;&nbsp;1&nbsp; C &nbsp;1&nbsp; c &nbsp;y&nbsp;&nbsp;p&nbsp;&nbsp;i&nbsp; K &nbsp;Y&nbsp;&nbsp;I&nbsp;&nbsp;I&nbsp;&nbsp;4&nbsp; }

完整正向验证通过,但过程中可能有问题,最后flag为“flag”不是“blag”但是问题不大。

flag{wnNCZJbBOqL3QA1C1cypiKYII4}

,这是

CyberSecurity


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:flower安全 Flowers aq Flowers aq《香港人工智能网络安全挑战赛Writeup(初赛)-Reverse全解》

评论:0   参与:  0