逆向工程中编码与加密算法的识别、分析与实战应用 1. 从“菜鸡”到入门为什么逆向工程绕不开编码与加密刚接触逆向工程的朋友常常会卡在一个看似基础实则至关重要的环节面对程序里一堆“乱码”或者经过变换的数据完全无从下手。你兴致勃勃地打开调试器跟到一个关键函数发现它传进去的是一串“5L2g5aW977yM5oiR5Lus5LiA5Liq5a2X56ym5Liy”传出来的却是“你好这是一段中文”。或者你拦截到一个网络包里面的关键参数是一长串毫无规律的十六进制字符经过某个算法处理后才变成可读的明文。没错这就是编码和加密算法在“作祟”。对于逆向分析来说它们就像是锁住宝藏的第一道也是最常见的一道门。不理解这些算法你的逆向之路就永远在门口打转。我刚开始学逆向的时候也吃过不少亏。曾经为了分析一个软件的注册机制对着一段经过Base64编码后又用自定义XOR处理的数据折腾了一整天最后才发现如果我先认出Base64的特征五分钟就能搞定。所以今天我就把自己在逆向过程中对常见编码和加密算法的识别、分析与逆向经验总结下来。这不仅仅是CTF比赛中的常客更是商业软件、移动应用、网络协议分析中每天都会遇到的“老朋友”。我们会重点聊聊Base64、SM4这些高频出现的算法以及如何从二进制代码或脚本中快速识别它们并找到破解或绕过的思路。无论你是分析一个安卓APK的so库还是解密一段网页的JavaScript这些知识都是你的必备工具箱。2. 逆向思维下的编码算法不只是“转码”那么简单在正向开发中编码Encoding主要用于数据表示、传输或存储比如把二进制数据变成可打印的文本。但在逆向工程师眼里编码算法常常被用作一种轻量级的“混淆”或“伪装”手段。识别它们是还原数据真实面貌的第一步。2.1 Base64逆向分析中的“老熟人”与它的变种们Base64恐怕是逆向中最最常见的编码了。它的核心特征非常明显字符集通常由A-Z, a-z, 0-9, , /以及填充符组成输出字符串的长度通常是4的倍数。如何在逆向中快速识别Base64静态字符串扫描在IDA Pro、Ghidra等反编译工具中直接搜索上述字符集构成的字符串。如果发现一段较长的、仅由这些字符组成的字符串大概率是Base64编码的数据。动态行为观察在调试时如果发现程序调用了诸如base64_encode、base64_decode或类似命名的函数或者数据流经过一个函数后从二进制/十六进制变成了可打印的ASCII字符串或反之就要高度怀疑。特征码识别标准的Base64编码/解码函数有其固定的操作流程如按6位分组、查表替换。在一些简单的程序中可能会直接内联实现。你可以通过识别其常量表比如一个包含64个字符的数组来定位。逆向实战中的Base64“花活”单纯的Base64解码很简单但实际软件中不会这么直接。多层嵌套编码这是CTF和某些软件中常用的技巧。比如一段数据先被Base64编码结果再被Base64编码一次甚至多次。在逆向时你需要观察数据解码后的结果是否仍然是Base64特征字符串。我常用的方法是写个小脚本循环尝试解码直到结果不再符合Base64字符集为止。import base64 data “5L2g5aW977yM5oiR5Lus5LiA5Liq5a2X56ym5Liy” # 示例数据 while True: try: # 尝试解码并检查是否为ascii可打印或utf-8 decoded base64.b64decode(data) # 尝试转换为字符串如果失败则可能仍是二进制或另一层base64 try: text decoded.decode(‘utf-8’) print(f“解码成功: {text}”) # 可以进一步检查text是否还是base64字符串特征 if all(c in ‘ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789/’ for c in text): print(“解码后结果仍具有Base64特征可能为多层嵌套。”) data text # 继续循环 else: break except UnicodeDecodeError: print(“解码结果为二进制数据:”, decoded.hex()) break except Exception as e: print(“解码失败:”, e) break自定义码表这是增强隐蔽性的常用手段。程序不使用标准的ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789/码表而是将其打乱。例如某些游戏或软件使用自定义的64个字符作为码表。逆向时关键就是找到这个码表。你需要在反汇编代码中寻找一个长度为64的常量字符数组它就是解密的钥匙。结合其他简单变换Base64编码后可能再进行一次简单的XOR异或或加减操作。这需要你在动态调试中观察Base64解码函数输出后数据是否立即被另一个小函数处理。注意Base64只是一种编码不是加密它没有密钥其“安全性”完全依赖于算法的隐蔽性如自定义码表。一旦识别出来还原就是分分钟的事。不要被它的外表唬住。2.2 十六进制Hex与URL编码网络数据抓包的好伙伴这两种编码在逆向网络协议、分析HTTP/HTTPS请求响应时极为常见。十六进制编码将每个字节转换为两个字符的0-9, a-f表示。例如字节0xAB变成字符串”AB”或”ab”。在逆向中如果你看到字符串长度是偶数且字符仅在0-9a-fA-F范围内那很可能就是Hex编码。Python的binascii.hexlify()和unhexlify()是处理它的利器。URL编码Percent-Encoding为了在URL中安全传输特殊字符将其转换为%后跟两个十六进制数字的形式如空格变成%20中文常用UTF-8编码后再进行此操作如“中”可能变成%E4%B8%AD。在逆向爬虫或分析API时经常需要解码URL参数。识别特征就是包含大量的%符号。逆向技巧很多网络库如libcurl或语言内置库如Java的URLEncoder会直接调用相关函数。在反编译代码中搜索%字符串处理逻辑或定位hex、urlencode、percent等关键字可以帮助你快速找到相关代码段。2.3 Unicode与字符串编码解决乱码问题的钥匙在逆向跨平台或国际化软件时字符串编码问题会让你头疼。程序内部可能使用UTF-8、UTF-16LE、UTF-16BE或GBK等。识别特征UTF-8英文字符单字节中文字符通常为3字节。在内存中ASCII部分正常中文部分每个字节都大于0x80。UTF-16LE在x86 Windows环境下非常常见。每个字符占2字节或4字节。英文字符ASCII会在内存中呈现为类似a\x00b\x00c\x00的形式ASCII码后跟一个零字节。在IDA中你可能会看到字符串被识别为aWideString。GBK中文Windows传统编码。一个中文占2字节。逆向中的应用当你从内存或文件中dump出一段字符串数据显示为乱码时第一反应就是尝试不同的编码解码。在Python中你可以简单地尝试data b’\xd6\xd0\xce\xc4’ # 示例字节数据 encodings [‘gbk’, ‘utf-8’, ‘utf-16-le’, ‘utf-16-be’] for enc in encodings: try: print(f“{enc}: {data.decode(enc)}”) except UnicodeDecodeError: pass在静态分析中注意观察程序调用的字符串处理API如Windows的MultiByteToWideChar/WideCharToMultiByte或Linux下的iconv相关函数它们指明了编码转换的发生点。3. 对称加密算法逆向找到那个“唯一的钥匙”对称加密算法在软件保护、通信加密、本地数据存储中应用极广。逆向的目标往往不是破解算法本身现代加密算法在数学上是安全的而是找到那个被隐藏或硬编码在程序中的密钥Key或者理解其使用模式如ECB、CBC从而能够模拟加密解密过程。3.1 国密SM4算法识别与分析SM4是我国商用密码标准中的分组加密算法在金融、政务及一些国内软件中越来越常见。它和AES类似是分组密码分组长度128位密钥长度128位。如何在二进制代码中识别SM4查找常量表SM4算法内部使用一个固定的S盒Substitution Box和FK、CK常量。这是最可靠的指纹。SM4的S盒是一个256字节的固定表。如果你在反汇编代码的数据段.data或.rdata中发现一个256字节的、内容固定的数组可以将其与标准SM4 S盒进行比对。标准SM4 S盒的前几个字节是{0xd6, 0x90, 0xe9, 0xfe, 0xcc, 0xe1, 0x3d, 0xb7, …}。函数特征SM4的轮函数操作涉及S盒查表、循环移位和异或。在反编译代码中你可能会看到大量的查表操作byte ptr [table_base index]、32位整数的循环左移rol指令或((x n) | (x (32-n)))的代码模式以及异或xor操作。字符串与导入表如果程序使用了开源密码库如GMSSLBouncyCastle的国密Provider可能会在字符串或导入函数中留下SM4、GMSSL、BC等痕迹。对于Java程序可以查找SM4Engine、SM4Cipher等类名。SM4工作模式ECB vs CBC识别出算法后确定其工作模式同样关键它直接影响到逆向和攻击的难度。ECB模式最简单的模式相同的明文块加密后得到相同的密文块。在代码中它通常表现为直接对每个128位分组调用加密函数没有额外的初始化向量IV处理。安全性较低在逆向中如果你能获得一段已知明文及其对应的密文可能有助于分析或验证密钥。CBC模式更常用的模式每个明文块先与前一个密文块异或后再加密。需要一个初始化向量IV。在代码中你会看到在加密/解密循环开始前有一个IV被加载或生成并且在处理每个分组时有一个与上一分组密文或解密中间结果进行异或的操作。逆向时必须同时找到Key和IV才能正确解密。逆向实战心得 对于像SM4、AES这类标准强加密算法直接通过逆向分析从算法中推导出密钥几乎不可能除非实现有严重漏洞。我们的主要思路是密钥硬编码在程序的字符串、常量数组或资源文件中搜索可能的密钥。密钥可能是16字节128位的十六进制字符串或可打印字符。使用findcrypt-yara等IDA插件可以帮助识别常见算法的常量。密钥派生密钥可能不是直接存储而是通过一个密码Password和盐值Salt经过KDF密钥派生函数如PBKDF2计算得出。你需要找到生成密钥的那个函数并尝试输入已知的测试密码观察生成的密钥是否与加密所用的一致。内存dump在程序运行时密钥必然会被加载到内存中。如果加密/解密操作发生在你的调试会话期间你可以在调用加密函数如SM4_Encrypt时通过调试器查看传入的密钥参数或者在该函数入口处设置断点从寄存器或栈中提取密钥。白盒密码在一些高保护场景中可能会使用白盒密码技术将密钥与算法混淆在一起。逆向这种实现极其复杂通常需要深厚的密码学和程序分析功底。遇到这种情况可能需要考虑其他攻击面如协议层、输入验证等。3.2 其他常见对称加密算法特征速查除了SM4逆向中还常遇到AES识别方法与SM4类似查找其S盒256字节和轮常量Rcon数组。AES的S盒是公开的前几个字节为{0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, …}。同样需要注意ECB、CBC等模式。DES/3DESDES的S盒是8个4x16的查找表共512位。3DES是DES的三次应用。在较老的金融系统或遗留软件中常见。RC4流密码。特征包括一个256字节的S盒初始化过程for i in range(256): S[i]i和伪随机子密码生成算法。代码中通常有两个索引变量i和j在循环更新。TEA/XTEA非常简单的小型分组密码核心操作是循环、加法、异或和移位通常直接以内联代码实现没有明显的S盒。其魔数常数0x9e3779b9黄金比例的倒数是一个关键识别特征。4. 非对称加密与哈希算法逆向中的验证与签名在逆向中非对称加密如RSA和哈希算法如SM3、MD5、SHA系列通常不用于直接加密大量数据而是用于签名验证、密钥交换或完整性校验。4.1 哈希算法识别与对抗哈希算法将任意长度数据映射为固定长度的摘要。在软件中常用于验证文件完整性、密码存储加盐哈希或生成挑战码。识别特征常量每种哈希算法都有固定的初始化向量IV。例如MD5的IV是0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476。SHA-1、SHA-256也都有各自的IV常量。在数据段找到这些常量基本就能确定算法。函数名/字符串动态链接库可能导出MD5_Init、SHA256_Update等函数。字符串中也可能出现算法名。输出长度MD5输出128位16字节SHA-1输出160位20字节SHA-256输出256位32字节SM3输出256位32字节。如果你看到程序在比较一个固定长度的二进制串可以猜测其算法。逆向场景与应对注册码/许可证验证软件可能用哈希算法计算机器特征如硬盘序列号、MAC地址生成一个“机器码”然后要求用户输入对应的“注册码”。这个注册码可能是机器码经过某种变换如与一个固定密钥拼接后再哈希或使用非对称算法签名的结果。你需要找到生成机器码和验证注册码的代码逻辑。API请求签名很多网络应用如akamai盾、hcaptcha的后端交互或一些APP的API会对请求参数进行哈希通常用HMAC或签名防止篡改。逆向的目标是找到生成签名的密钥和算法以便能自己构造合法请求。这通常需要分析JavaScriptWeb逆向或移动端的Native代码Android/iOS逆向。密码验证服务器不存储明文密码存储的是加盐哈希值。客户端登录时对用户输入的密码进行同样的哈希运算后发送。在逆向客户端时你需要找到哈希函数和盐值这可能用于制作离线密码破解工具或进行撞库测试需在法律允许范围内。实操心得对于哈希由于其单向性逆向的目标几乎从不可能是“解密”哈希值而是识别算法、找到盐值、并能够复现计算过程。例如如果你发现一个游戏客户端将用户密码与一个硬编码的字符串拼接后做MD5那么你就可以用同样的方式生成任意账号的密码哈希可能用于本地模拟登录测试。4.2 非对称加密以RSA为例在逆向中的角色RSA算法在逆向中常见于软件激活、许可证验证、通信密钥交换等场景。识别点大数运算代码中会出现非常大的整数几十上百字节以及模幂运算a^b mod n。可能会调用大数库如OpenSSL的BN函数。密钥数据在程序数据段可能会发现PEM格式的证书字符串以—–BEGIN PUBLIC KEY—–开头或者直接存储着模数n和公钥指数e通常为65537的二进制大整数。功能上下文代码在验证一个签名使用公钥解密一段数据与计算出的哈希值对比或者用公钥加密一小段数据如会话密钥。逆向策略 对于RSA私钥通常不会出现在客户端。因此逆向的目标通常是提取或绕过公钥验证如果你只是想让程序认为签名有效可以尝试修改验证函数的跳转指令Patch或者找到一个有效的签名并硬编码到程序中。分析密钥交换过程在通信协议中客户端可能用服务器的公钥加密一个随机生成的对称密钥。你需要理解这个流程以便在中间人攻击或模拟客户端时能够生成合法的加密数据。寻找弱密钥或旧版本漏洞极少数情况下程序可能使用强度不足的RSA密钥如512位或者使用的随机数生成器有缺陷。但这需要专业的密码学分析工具和知识。5. 实战逆向流程与工具链配合理论说了这么多我们来看一个综合性的简化实战流程假设我们要分析一个Android Native So库中的加密函数。5.1 静态分析定位入口使用IDA Pro/Ghidra加载So文件首先进行反编译等待自动分析完成。字符串搜索在字符串窗口中搜索base64、encrypt、decrypt、AES、SM4、MD5、SHA等关键词。注意中英文。查找密码学常量使用FindCrypt等插件或手动在数据段浏览寻找大的、看起来随机的常量数组可能是S盒、IV、魔数。交叉引用分析找到上述字符串或常量的引用位置跳转到相关函数。这很可能就是加密/解密/编码/哈希的入口函数。5.2 动态调试验证猜想静态分析只能给出可能的位置动态调试才能看清具体的数据流和参数。使用Frida进行Hook这是移动端逆向的神器。你可以写一个Frida脚本Hook你怀疑的加密函数。// 示例Hook一个名为 native_encrypt 的JNI函数 Java.perform(function() { var targetClass Java.use(“com.example.app.CryptoHelper”); // 假设有一个native方法叫 encryptData targetClass.encryptData.overload(‘[B’, ‘[B’).implementation function(input, key) { console.log(“[] encryptData called!”); console.log(“Input (hex):”, bytesToHex(input)); console.log(“Key (hex):”, bytesToHex(key)); var result this.encryptData(input, key); // 调用原函数 console.log(“Result (hex):”, bytesToHex(result)); return result; }; function bytesToHex(bytes) { /* 转换函数 */ } });通过Hook你可以直接看到函数传入的明文、密钥和输出的密文从而100%确认该函数的功能。使用IDA Pro远程调试对于更底层的Native代码逻辑分析可以将IDA连接到手机上的调试服务器如android_server在关键函数地址下断点单步跟踪寄存器、内存和栈的变化观察算法每一步的执行细节。这对于分析自定义或混淆过的算法尤其有用。5.3 算法还原与模拟一旦通过动态调试确认了算法、密钥、IV和模式下一步就是用自己的代码复现这个过程。提取关键参数从内存或代码中提取出密钥、IV、S盒如果是自定义的、工作模式。选择对应库在Python中可以使用pycryptodome或cryptography库来实现标准算法AES, SM4等。对于自定义编码或简单变换自己实现即可。编写模拟代码from Crypto.Cipher import AES from Crypto.Util.Padding import pad, unpad import base64 # 假设从逆向中获取到的信息 key b’your_16_byte_key!!!’ # 16字节密钥 iv b’initial_vector_16b’ # 16字节IV (CBC模式需要) cipher_text_hex “加密后的十六进制字符串” cipher AES.new(key, AES.MODE_CBC, iv) cipher_text_bytes bytes.fromhex(cipher_text_hex) plain_text_padded cipher.decrypt(cipher_text_bytes) plain_text unpad(plain_text_padded, AES.block_size) # 去除填充 print(“Decrypted:”, plain_text.decode())验证用你的模拟代码去解密一段已知的密文看是否能得到正确的明文。或者用你的代码加密一段数据看是否与目标程序产生的结果一致。6. 常见问题与排查技巧实录在实际逆向过程中你会遇到各种奇怪的问题。这里记录一些我踩过的坑和解决思路。问题1明明找到了加密函数Hook时却捕获不到调用可能原因1函数名混淆。So库中的函数名可能被混淆成a、b、c或无意义字符串。此时静态分析找到的函数地址可能不准或者Hook脚本使用的函数签名不对。解决使用函数地址进行Hook而不是函数名。在IDA中查看函数的起始地址在Frida中使用Module.findExportByName(null, offset)或绝对地址进行Hook。可能原因2调用时机过早。加密函数可能在APP启动或某个初始化阶段就被调用你的Frida脚本附着上去时已经晚了。解决使用frida -U -f com.package.name –no-pause在APP启动早期就注入脚本或者修改APP的启动方式确保脚本最先加载。可能原因3多线程调用。函数可能在非主线程调用导致Frida默认的Java.perform上下文不对。解决确保Hook代码在Java.perform内并且考虑使用setImmediate或检查线程。问题2解密出来的数据开头或结尾有乱码可能原因1填充问题。分组加密需要填充。常见的填充方式有PKCS#7、ZeroPadding等。如果解密时使用的填充方式与加密时不一致就会导致最后一块数据错误。解决尝试不同的填充方式。观察解密后数据的最后几个字节它们可能就是填充值。例如PKCS#7填充的字节值就是填充的长度。可能原因2编码问题。解密出的数据可能是二进制数据你直接当成UTF-8解码就会乱码。解决先不要解码打印十六进制形式hex()查看。它可能是图片数据、序列化对象如Protobuf或其他结构化二进制数据。可能原因3IV错误。CBC模式需要正确的IV。如果IV获取错误第一个解密块会是乱码但后续块可能正确错误传播。解决仔细检查动态调试中传入的IV值。问题3算法似乎是标准的但用自己的库实现结果总是不对可能原因1工作模式或参数细节。除了ECB、CBC还有CFB、OFB、CTR等模式。确认模式是否正确。另外一些实现可能使用“CBC with no padding”而你的库默认使用了填充。可能原因2密钥或IV处理。程序可能对原始的密钥字符串进行了预处理比如先做一次MD5哈希取前16字节作为AES密钥。解决在调试器中在加密函数入口处不仅打印传入的“密钥”参数还要打印实际参与加密运算的密钥数据即函数内部处理后的结果进行对比。可能原因3字节序问题。特别是在处理多字节整数如AES的轮密钥或从文件中读取密钥时大端序和小端序可能会搞错。问题4遇到未知的、看起来像自定义的加密/编码怎么办第一步动态跟踪。这是最有效的方法。在调试器中单步跟踪记录下输入数据经过每一个操作异或、加减、移位、查表后的变化。用纸笔或文本编辑器记下每一步的中间状态。第二步寻找规律。观察这些操作是否可逆异或、加减同一个数是可逆的查表如果有反向表也是可逆的。尝试用穷举小数据如单个字节0x00, 0x01, 0xFF输入观察输出来推断查表的内容或运算的性质。第三步尝试模拟。根据记录的步骤用高级语言Python复现这个过程。先从最简单的部分开始逐步增加复杂度并与调试结果对比。第四步利用符号执行或污点分析高级技巧。对于复杂的混淆可以使用如Angr、Triton等框架进行自动化分析但这需要较高的学习成本。逆向编码和加密算法就像是在解一个设计者留下的谜题。它考验的不仅是你的技术知识更是耐心、观察力和逻辑推理能力。从最基础的Base64识别开始逐步深入到复杂的自定义算法这个过程本身就是极大的乐趣和成就感来源。记住没有无法分析的程序只有还没找到的突破口。每次成功还原一个算法你的工具箱里就多了一件利器下次再遇到类似的保护就能更快地直击要害。