1. 从“看热闹”到“看门道”:逆向中的编码与加密
刚入门逆向那会儿,我总盯着那些花里胡哨的算法库和复杂的汇编跳转,觉得那才是“真功夫”。后来踩坑踩多了才明白,真正卡住新手脖子的,往往不是那些高深的虚拟机保护或者混淆,而是最基础的编码转换和标准加密算法。你费了九牛二虎之力跟到关键函数,结果发现它传进去的是一串U2FsdGVkX1...,或者内存里一堆0x41 0x42 0x43,如果认不出这是Base64或者ASCII,直接就懵了。再比如,你看到程序调用了CryptEncrypt或者某个libcrypto.so里的函数,却不知道它用的是AES还是DES,是ECB还是CBC模式,那后续的密钥查找、数据解密就无从谈起。
所以,我把逆向学习中的“编码”和“加密”比作是“识字”和“语法”。编码是信息的表现形式,就像把一句话写成英文、拼音或摩斯电码,内容没变,只是样子变了。而加密是信息的保护手段,就像把这句话锁进保险箱,没有钥匙(密钥)你就看不懂原意。在逆向分析中,我们经常需要先“识字”(识别并解码各种编码),才能去尝试“开锁”(分析加密逻辑并获取密钥)。这篇文章,我就结合自己踩过的坑和实战经验,系统梳理一下逆向工程中最常遇到的编码与加密算法,重点讲清楚怎么识别、怎么分析、怎么搞定。
2. 逆向中的“识字”课:常见编码识别与处理
编码本身不是加密,它只是为了方便传输、存储或显示而对数据进行的一种格式转换,通常是公开、可逆的。在逆向中,快速识别编码类型能帮你立刻看清数据的“真面目”。
2.1 Base64:网络传输的“常客”
Base64可以说是出场率最高的编码,没有之一。它的特征极其明显:字符串通常由A-Z、a-z、0-9、+、/以及填充符=组成,长度通常是4的倍数。
为什么逆向中总遇到它?因为二进制数据(比如加密后的密文、图片、序列化数据)直接放在JSON、XML或URL里传输会出问题(有控制字符)。Base64把它们变成纯文本,就安全了。所以,你在HTTP请求/响应体、配置文件、注册表、甚至内存字符串里看到的一长串看似乱码的字符,首先就该怀疑是Base64。
识别与实战处理:
- 肉眼识别:看到类似
"dGVzdA=="、"U2FsdGVkX18="(OpenSSL Salted开头密文的特征)这种格式的,八九不离十。 - 工具验证:
- 在线网站:很多在线工具支持Base64解码,随手一贴就知道是不是。
- CyberChef:这个“网络瑞士军刀”的
From Base64模块非常好用。 - Python脚本:自己写几行代码验证最灵活。
import base64 import re def try_decode_base64(data): # 先简单清理一下可能的引号或空格 data = data.strip('\"\' \n\r\t') # Base64正则匹配(宽松版) if re.match(r'^[A-Za-z0-9+/]*={0,2}$', data) and len(data) % 4 == 0: try: decoded = base64.b64decode(data, validate=True) # 尝试以UTF-8解码,如果不是文本就显示hex try: return decoded.decode('utf-8') except: return decoded.hex() # 返回十六进制表示 except Exception as e: return f"解码失败: {e}" return "不符合Base64特征" test_str = "U2FsdGVkX19q4zEwWL2hP2VJ5V4hX8NpFw==" print(try_decode_base64(test_str)) - 逆向中的坑:
- 变种Base64:有些实现会修改码表,比如把
+/换成-_(URL安全的Base64),或者自定义码表。这就需要你逆向编码函数,还原出码表。 - 多层嵌套:CTF中常见,数据被反复Base64编码多次。解决思路就是写个循环,直到解码结果不再符合Base64特征。
- 包含换行:有些格式(如PEM证书)的Base64会每76字符加一个换行,解码前需要去掉。
- 变种Base64:有些实现会修改码表,比如把
注意:
U2FsdGVkX1开头的字符串,是OpenSSL使用Salted前缀的加密结果再进行Base64编码的典型特征,这提示你后面很可能接的是一个对称加密(如AES、DES)的密文。
2.2 十六进制(Hex)编码:内存的直白写照
十六进制编码就是把每个字节转换成两个0-9a-f的字符。它在逆向中几乎无处不在,因为这是查看内存原始数据最直观的方式。
识别与实战处理:
- 特征:字符串仅由
0-9和a-f(或A-F)组成,长度通常是偶数。 - 常见场景:
- 硬编码密钥/IV:在二进制文件的字符串区域,你可能会发现像
"0123456789ABCDEF"这样的字符串,这很可能就是一个16字节的AES密钥的Hex表示。 - 网络数据包:某些协议或自定义协议会用Hex格式传输二进制数据。
- 调试器显示:IDA、OllyDbg、Ghidra等工具的内存窗口默认就是以Hex形式展示数据。
- 硬编码密钥/IV:在二进制文件的字符串区域,你可能会发现像
- 工具与代码:
- Python:
bytes.fromhex()和binascii.hexlify()是常用函数。
hex_str = "48656c6c6f20576f726c64" # "Hello World"的Hex bytes_data = bytes.fromhex(hex_str) print(bytes_data.decode('utf-8')) # 输出: Hello World raw_data = b'Hello' hex_str = raw_data.hex() print(hex_str) # 输出: 48656c6c6f - Python:
2.3 URL编码/百分号编码:网络请求的“安全帽”
URL编码是为了把URL中不允许的字符(如空格、中文、&、=)转换成%XX的形式,其中XX是字符的ASCII码十六进制值。
识别与实战处理:
- 特征:字符串中包含大量
%符号,如%20(空格)、%E4%B8%AD(“中”字的UTF-8编码)。 - 逆向场景:分析HTTP请求参数、Cookie时一定会遇到。你需要解码后才能看到参数的真实值。
- 处理:
- 浏览器开发者工具:Network面板会自动解码显示。
- Python:使用
urllib.parse模块。
from urllib.parse import unquote, quote encoded = "name%3D%E5%BC%A0%E4%B8%89%26age%3D20" decoded = unquote(encoded, encoding='utf-8') print(decoded) # 输出: name=张三&age=20 # 编码 raw = "key=值" encoded = quote(raw, safe='') # safe=''表示不对/和=等字符进行编码 print(encoded)
2.4 Unicode与UTF-8:字符串的“内心世界”
在Windows逆向和涉及多国语言的程序中,Unicode编码是绕不开的。内存中一个英文字符可能占2字节(UTF-16LE),而中文字符在UTF-8下占3字节。
识别与实战处理:
- 特征:
- UTF-16LE(Windows宽字符):内存中英文字符后跟一个
0x00字节,如'H'存为0x48 0x00。字符串常以L"Hello"形式出现在代码中。 - UTF-8:变长编码,ASCII字符不变,中文等字符占多个字节(如
0xE4 0xB8 0xAD代表“中”)。
- UTF-16LE(Windows宽字符):内存中英文字符后跟一个
- 逆向要点:
- 在IDA等反编译工具中,注意字符串的显示方式。有时你需要手动指定编码才能正确显示字符串。
- 跟踪函数调用时,注意区分
strlen(单字节)和wcslen(宽字节)。 - 遇到乱码时,尝试用不同的编码(GBK、GB2312、UTF-8、UTF-16)去解码,很可能有意外收获。Python的
bytes_data.decode('gbk', errors='ignore')可以帮你快速尝试。
3. 逆向中的“开锁”术:常见加密算法分析
识别了数据的“样子”(编码),接下来就要对付它的“保护壳”(加密)。逆向分析加密算法的目标通常不是破解算法本身(现代加密算法几乎不可破解),而是找到密钥和加密模式。
3.1 对称加密算法:同一把钥匙
对称加密加解密用同一个密钥。逆向中,找到这个密钥是关键。
3.1.1 AES (Advanced Encryption Standard)这是目前最主流、最安全的对称加密算法。
如何识别:
- 字符串特征:代码中可能出现
AES、Rijndael(AES的原名)、CryptImportKey、EVP_aes_256_cbc(OpenSSL)等常量字符串。 - 密钥长度:AES的密钥长度固定为128、192或256位(即16、24、32字节)。如果你在代码或数据中发现一个固定长度为16/24/32字节的数据块,它很可能是AES密钥。
- S-Box:AES算法内部有一个固定的256字节的替换表(S-Box)。在IDA中搜索字节序列
0x63, 0x7C, 0x77, 0x7B...(AES S-Box的开头),如果能找到,基本可以确定使用了AES。 - API/库调用:在Windows上,可能调用
CryptAPI(CryptEncrypt/CryptDecrypt);在Linux/Android上,可能链接libcrypto.so(OpenSSL)并使用AES_encrypt、EVP_CipherInit_ex等函数。
- 字符串特征:代码中可能出现
模式分析(关键!): 只知道是AES还不够,模式(Mode)决定了安全性。逆向时必须确定模式。
- ECB (Electronic Codebook):最不安全!相同的明文块加密后得到相同的密文块。在数据中能看到重复的块(比如加密一张纯色图片,密文会有规律重复)。尽量避免使用,但一些老旧或简单系统仍用。
- CBC (Cipher Block Chaining):最常用!需要一个初始化向量(IV)。每个明文块先与前一个密文块异或再加密。IV通常是随机的,并和密文一起传输(常放在密文开头)。你需要找到IV和密钥。
- 其他模式:CTR、GCM等也较常见,分析思路类似,重点是找到nonce/IV和密钥。
实战逆向步骤:
- 定位加密函数:通过字符串、导入表或调用栈找到加密入口。
- 跟踪密钥来源:密钥可能是硬编码、从文件读取、通过网络下发、或由用户输入派生(如PBKDF2)。用调试器(如x64dbg, IDA Debugger, Frida)在加密函数下断点,回溯密钥生成或传入的过程。
- 确定模式与IV:观察加密函数的参数。对于CBC模式,通常会有一个独立的IV参数。IV可能硬编码,也可能是随机生成后与密文拼接。
- 验证:用找到的密钥、IV和模式,编写脚本尝试解密一段已知的密文,看是否能得到有意义的明文。
3.1.2 DES/3DES (Data Encryption Standard/Triple DES)DES已不安全,3DES仍在一些遗留系统中使用。密钥长度:DES为8字节(实际56位),3DES为16或24字节。
- 识别:搜索字符串
DES、3DES、TripleDES。DES的S-Box也与AES不同。API调用类似AES。 - 逆向要点:与AES思路完全一致,重点是找密钥。注意3DES的密钥可能包含三个8字节密钥(K1, K2, K3)。
3.1.3 SM4(国密算法)在国内的软件、物联网设备、金融系统中越来越常见。它是国家标准,分组长度和密钥长度均为128位。
- 识别:
- 字符串:
SM4、GM/T 0002-2012。 - 常量:SM4有固定的FK和CK常量数组。在IDA中搜索这些常量是定位SM4代码的可靠方法。例如,FK数组通常为
{0xa3b1bac6, 0x56aa3350, 0x677d9197, 0xb27022dc}。 - 库:可能使用
libsm4.so、gmssl或厂商自实现的库。
- 字符串:
- 模式:同样支持ECB、CBC等模式,分析思路同AES。
- 实战:我曾分析过一个智能家居设备的固件,其网络通信数据就是用SM4-CBC加密的。通过逆向固件,在初始化函数里找到了硬编码的密钥和IV(藏在一个看似无关的配置字符串处理函数里)。用Python的
gmssl库成功解密了通信数据。
3.2 非对称加密算法:公钥与私钥
非对称加密使用一对密钥:公钥加密,私钥解密(或反之用于签名)。逆向中通常目标是拿到私钥,或者理解其加解密/签名流程。
3.2.1 RSA最经典的非对称算法。密钥长度(模数n的位数)常见有1024、2048、4096位。
- 如何识别:
- 字符串:
RSA、PublicKey、PrivateKey、BEGIN RSA PRIVATE KEY。 - 大整数:RSA的模数
n、公钥指数e(通常是65537=0x10001)都是非常大的整数,在IDA的静态分析中,这些大整数常量非常显眼。 - API/库:OpenSSL的
RSA_public_encrypt、RSA_private_decrypt;Windows的CryptImportKey导入RSA类型的密钥。
- 字符串:
- 逆向分析目标:
- 获取私钥:这是最理想的情况。私钥可能硬编码在程序中(非常危险!)、存放在配置文件或密钥库中。搜索
-----BEGIN RSA PRIVATE KEY-----这样的PEM头。 - 理解流程:如果拿不到私钥,就分析程序如何使用公钥。例如,客户端用服务器公钥加密一个临时生成的对称密钥(会话密钥),然后后续通信使用对称加密。这时,你的目标就变成了获取这个被加密的会话密钥,或者在内存中拦截它。
- 获取私钥:这是最理想的情况。私钥可能硬编码在程序中(非常危险!)、存放在配置文件或密钥库中。搜索
- 工具:
openssl命令行工具可以方便地解析PEM格式的密钥,并进行加解密操作。
3.2.2 SM2(国密非对称)国产非对称算法,基于椭圆曲线密码学(ECC)。
- 识别:字符串
SM2、ECC、GM/T 0003-2012。同样有固定的参数(如椭圆曲线方程、基点G等)。 - 逆向:思路与RSA类似,寻找公钥/私钥对。SM2的公钥是一对坐标(x, y),私钥是一个大整数。分析时需关注国密算法库的调用。
3.3 哈希与消息认证码:验证“身份”
哈希(如MD5、SHA1、SHA256、SM3)将任意数据映射为固定长度的摘要,不可逆。常用于校验数据完整性、密码存储(需加盐!)。消息认证码(如HMAC)在哈希基础上加入了密钥,用于验证消息来源和完整性。
- 识别:字符串
MD5、SHA、HMAC、摘要。哈希函数调用特征明显,输入数据,输出固定长度(MD5:16字节, SHA256:32字节)。 - 逆向中的作用:
- 校验绕过:程序可能用哈希校验文件或关键数据是否被篡改。你需要修改数据后,重新计算正确的哈希值并替换。
- 密码破解:如果程序存储的是明文密码的哈希值(且未加盐或盐已知),你可以尝试彩虹表碰撞或暴力破解。但加盐的哈希很难破解。
- 密钥派生:
HMAC-SHA256常用于从主密钥派生出子密钥,需要分析其输入(密钥、数据)是什么。
4. 实战逆向案例拆解:一个Android Native层的3DES通信协议
理论说再多,不如看个实例。假设我们有一个Android应用,其核心通信协议在Native层(so库)中用3DES加密。我们的目标是解密其网络数据包。
4.1 初步侦察
- 使用
Jadx-GUI打开APK,搜索loadLibrary或System.load,找到加载的so库名,比如libprotocol.so。 - 在Java层搜索与加密、网络相关的关键词(
encrypt,decrypt,Cipher,DES,3DES),发现JNI调用native_encrypt_data和native_decrypt_data。
4.2 静态分析so库
- 用IDA Pro打开
libprotocol.so。 - 导出函数窗口找到
Java_com_xxx_app_NativeHelper_native_encrypt_data。 - 分析该函数,发现它调用了另一个内部函数
do_3des_cbc。 - 进入
do_3des_cbc,发现它调用了DES_set_key_unchecked和DES_ncbc_encrypt(这是OpenSSL的函数)。确认算法:3DES-CBC。 - 关键:寻找密钥和IV。在
do_3des_cbc函数开头或调用它的上层函数中,追踪传入的参数。发现密钥(24字节)和IV(8字节)来自于两个全局变量g_enc_key和g_enc_iv。 - 查看这两个全局变量的交叉引用,发现它们在
JNI_OnLoad函数中被初始化。分析JNI_OnLoad,发现密钥和IV是通过一个固定的字符串,经过一个简单的XOR和MD5变换后生成的。我们拿到了密钥和IV的生成逻辑。
4.3 动态验证与Frida Hook静态分析得出的结论需要验证。我们用Frida写个Hook脚本。
Java.perform(function() { var NativeHelper = Java.use('com.xxx.app.NativeHelper'); // Hook native方法,打印输入输出 var encryptPtr = Module.findExportByName('libprotocol.so', 'Java_com_xxx_app_NativeHelper_native_encrypt_data'); Interceptor.attach(encryptPtr, { onEnter: function(args) { // args[1]是jobject,args[2]是jbyteArray (明文) this.input = Java.vm.getEnv().getByteArrayElements(args[2], null); var inputLen = Java.vm.getEnv().getArrayLength(args[2]); console.log('[+] native_encrypt_data 输入长度: ' + inputLen); // 可以在这里把字节数组转为hex打印 console.log('输入Hex: ' + bytesToHex(this.input, inputLen)); }, onLeave: function(retval) { // retval是jbyteArray (密文) var output = Java.vm.getEnv().getByteArrayElements(retval, null); var outputLen = Java.vm.getEnv().getArrayLength(retval); console.log('[+] native_encrypt_data 输出长度: ' + outputLen); console.log('输出Hex: ' + bytesToHex(output, outputLen)); // 用我们静态分析得到的算法、密钥、IV,验证是否能解密密文 // 这里可以调用一个Python脚本或本地函数进行验证 verifyDecryption(bytesToHex(output, outputLen)); } }); });运行Frida脚本,触发应用的网络请求,成功抓取到加密前后的数据。
4.4 编写解密脚本根据静态分析得到的密钥生成逻辑和动态验证确认的模式(3DES-CBC),用Python编写解密脚本。
from Crypto.Cipher import DES3 from Crypto.Util.Padding import unpad import hashlib import binascii def derive_key_iv(): # 根据逆向出的逻辑生成密钥和IV seed_str = "some_hardcoded_string_from_so" # 假设是 seed_str 经过一些变换和MD5得到key_material md5 = hashlib.md5() md5.update(seed_str.encode('utf-8')) key_material = md5.digest() # 16字节 # 假设密钥是 key_material 重复拼接而成,IV是 key_material 的前8字节 key = (key_material * 2)[:24] # 3DES密钥需要24字节 iv = key_material[:8] # CBC IV需要8字节 return key, iv def decrypt_packet(ciphertext_hex): key, iv = derive_key_iv() cipher = DES3.new(key, DES3.MODE_CBC, iv) ciphertext = binascii.unhexlify(ciphertext_hex) # 注意:OpenSSL的CBC模式默认使用PKCS#7填充 plaintext_padded = cipher.decrypt(ciphertext) plaintext = unpad(plaintext_padded, DES3.block_size) return plaintext.decode('utf-8') # 使用Frida抓取到的密文进行测试 captured_cipher = "a1b2c3d4e5f6..." # 替换为实际密文 try: print(decrypt_packet(captured_cipher)) except Exception as e: print("解密失败:", e)运行脚本,成功解密出可读的JSON或协议数据,逆向成功。
5. 进阶技巧与工具链:提升逆向效率
掌握了基础识别和分析方法后,一些工具和技巧能让你事半功倍。
5.1 特征码与常量搜索这是定位加密/哈希函数最快的方法。在IDA中:
- 搜索字符串:算法名、API名、错误信息。
- 搜索字节序列:AES的S-Box、SM4的FK/CK常量、MD5/SHA256的初始化魔数(如MD5的
0x67452301,0xEFCDAB89等)。 - 搜索指令模式:某些算法有独特的操作序列。
5.2 动态调试与内存dump静态分析有时会陷入复杂的数据流。动态调试可以直接看到运行时状态。
- 下断点:在标准库函数(如
OpenSSL的AES_encrypt)或自定义加密函数入口下断。 - 观察参数:查看传入的密钥、IV、明文/密文缓冲区。
- 内存搜索:在程序内存中直接搜索可能的密钥(如搜索固定字符串、或特定长度的随机数据)。
Cheat Engine、x64dbg的内存搜索功能很好用。
5.3 使用Frida进行高级Hook对于Android/iOS/Windows应用,Frida是无敌的。
- Hook加密函数:不仅能打印输入输出,还能替换密钥或修改返回值。
- 跟踪密钥生命周期:Hook密钥生成、存储、传输的所有相关函数。
- 主动调用:直接调用so库中的解密函数,对抓到的密文进行解密,无需重写算法。
5.4 密码学算法识别工具
- PEiD/Krypto ANALyzer (KANAL):老牌PE文件分析工具,插件可以识别一些常见的加密常量。
- IDA的FindCrypt插件:IDA的神器,能自动扫描二进制文件中的多种加密算法(AES, DES, MD5, SHA, RSA等)的常量,并高亮显示。
- Binwalk:常用于固件分析,也能识别一些加密/压缩算法的特征。
5.5 编写自己的识别脚本针对特定项目,可以结合已知特征,用Python写脚本批量分析。
import re import binascii def scan_for_crypto_constants(binary_data): # AES S-Box 开头部分特征 aes_sbox_start = bytes([ 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76 ]) # MD5 初始化常量 (A, B, C, D) md5_init = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476] md5_bytes = b''.join([i.to_bytes(4, 'little') for i in md5_init]) findings = [] if aes_sbox_start in binary_data: findings.append("发现AES S-Box特征") if md5_bytes in binary_data: findings.append("发现MD5初始化常量特征") # ... 添加更多算法特征 return findings with open('target.bin', 'rb') as f: data = f.read() print(scan_for_crypto_constants(data))6. 总结与心法:逆向思维养成
最后,分享几点在逆向编码和加密时的心得:
- 大胆假设,小心求证:看到一个字符串,先猜它可能是Base64或Hex。看到一个固定长度的数据块,先猜它可能是密钥或IV。然后用工具或脚本去验证你的猜想。
- 上下文是关键:数据出现在哪里?是网络包、配置文件、还是内存字符串?它周围有什么函数调用?这些上下文信息是判断算法类型的重要线索。
- 从易到难,先编码后加密:先尝试Base64、URLDecode、HexDecode这些简单的解码操作。把数据还原成原始二进制形式后,再分析其加密特征。
- 善用已知信息:如果程序调用了
libcrypto-1.1.so的EVP_aes_256_cbc,那几乎可以断定是AES-256-CBC。如果字符串里有SM4_ECB,那方向就非常明确了。 - 动态与静态结合:静态分析给你蓝图,动态调试给你实景。两者结合,才能高效定位关键点。不要只盯着IDA反汇编的代码看,一定要让程序跑起来。
- 工具只是延伸,思路才是根本:再好的工具也无法替代你对算法原理和程序逻辑的理解。花时间弄清楚一个算法的流程,比你盲目尝试十个工具更有用。
逆向编码和加密就像侦探破案,你需要仔细观察所有线索(数据特征、字符串、函数调用),并利用你的知识(编码、加密算法原理)进行推理和验证。这个过程充满挑战,但每一次成功解密,都像是解开一道精妙的谜题,那种成就感正是逆向工程的魅力所在。希望这篇总结能帮你少走些弯路,更快地找到“开锁”的那把钥匙。