逆向工程入门:常见编码与加密算法识别与实战分析
1. 项目概述:从“菜鸡”到入门,逆向路上的第一道门槛
刚接触逆向分析那会儿,我常常对着反编译出来的代码发懵。满屏的0xDEADBEEF、0xCAFEBABE,还有一堆sub_401000、loc_4040A0,看得人头大。但很快我就发现,真正拦住我的,往往不是复杂的控制流混淆或者花指令,而是那些看起来平平无奇,却无处不在的编码和加密算法。程序用它来保护字符串、校验数据、加密通信,如果你看不懂这些“黑话”,逆向分析就寸步难行。这就像侦探拿到了加密的日记本,不先破解密码,后面的推理全是空谈。
“菜鸡学逆向——常见编码,加密算法逆向总结”这个标题,精准地戳中了所有逆向新手的痛点。它不是一个高深的密码学课题,而是一份实战导向的“生存指南”。它的核心价值在于,将逆向工程中最高频出现的那些“障眼法”进行归类、拆解,并提供一套可复现的识别与分析方法。无论你是想分析一个安卓APK里的网络请求,还是破解一个PC游戏的存档校验,亦或是参加CTF比赛,这些知识都是你工具箱里的螺丝刀和扳手,是最基础也最不可或缺的部分。
这篇文章,我就结合自己踩过的坑和总结的经验,带你系统梳理一遍逆向分析中常见的编码与加密算法。我们会从最基础的Base64、十六进制开始,讲到古典的XOR、ROT,再到现代对称加密的AES、DES,以及国密算法SM4,最后提一下非对称加密和哈希。重点不在于数学推导,而在于:在逆向的上下文中,如何快速识别它们?识别后,又该如何验证、解密或绕过?我们的目标是,让你下次在IDA Pro、Ghidra或者Frida的动态日志里看到这些“老朋友”时,能会心一笑,然后迅速搞定它。
2. 逆向分析中的编码算法:数据的“化妆术”
在逆向中,编码(Encoding)和加密(Encryption)有本质区别。编码是为了方便数据传输或存储(如避免特殊字符),通常没有密钥,过程可逆;加密则是为了保密,必须有密钥。但很多新手容易混淆,因为它们在代码里看起来可能都是“把一串数据变成了另一串”。我们先从最简单的编码开始,它们是逆向中最常见的“第一层皮”。
2.1 Base64:网络世界的“通用语”
Base64是我在逆向中遇到频率最高的编码,没有之一。它的目的很单纯:将任意二进制数据(比如一张图片、一段密文)编码成由64个可打印ASCII字符(A-Z, a-z, 0-9, +, /,=用于填充)组成的字符串。这样,数据就能安全地在只支持文本的协议(如HTTP、XML、JSON)中传输。
逆向识别特征:
- 字符串特征:编码后的字符串长度通常是4的倍数,字符集严格限定在
A-Za-z0-9+/=之内。如果你在程序的字符串常量区或网络抓包中看到一长串这样的字符,比如“dGVzdCBzdHJpbmc=”,那大概率是Base64。 - 函数特征:在反编译的代码中,寻找包含
base64、b64字样的函数名或变量名。更隐蔽的,是寻找操作0x41(‘A’)到0x7A(‘z’)这个区间的字符映射表。标准的Base64编码表(“ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/”)如果以常量形式出现,就是铁证。 - 调用模式:常见于网络请求前对参数的包装,或收到响应后的第一步处理。在Android逆向中,
android.util.Base64类是明显的线索。
实战分析与解密:识别后,解密通常很简单。但这里有三个坑:
- 变种Base64:有些开发者会自定义编码表,比如把
“+/”换成“-_”(URL安全的Base64),或者完全打乱顺序。这时你需要找到程序中的编码表常量。在IDA中,可以搜索字符串“ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789”,看其附近是否有被引用的、长度64的字符数组。 - 多层嵌套:尤其在CTF题中,可能会遇到Base64套Base64,甚至套了五六层。解决思路就是写个循环,一直解码直到解码失败或结果不再变化。用Python的
base64.b64decode,捕获binascii.Error异常即可。 - 自动识别工具:在静态分析时,可以用
strings命令或IDA的字符串窗口配合过滤。动态分析时,可以在Frida中Hook常见的Base64编码/解码函数,直接打印输入输出。
注意:Base64编码会使数据体积膨胀约33%。如果一个字符串解码后是乱码,但它符合Base64特征,不妨考虑它是不是其他数据(如压缩后的数据、另一个加密算法的输出)的Base64形式,需要进一步分析。
2.2 十六进制(Hex)与URL编码:明文的“伪装”
十六进制编码就是把每个字节用两个十六进制数字(0-9, A-F)表示。URL编码(百分号编码)则是为了在URL中安全传输特殊字符。
逆向识别特征:
- Hex:字符串仅由
0-9和a-f(或A-F)组成,长度为偶数。例如“68656c6c6f”对应“hello”。在代码中,常看到sprintf(buf, “%02x”, data[i])这样的模式。 - URL编码:字符串中包含大量
%后跟两个十六进制数字的模式,如“%E4%B8%AD%E6%96%87”(“中文”的UTF-8 URL编码)。在HTTP请求参数中极为常见。
实战心得: 这两种编码几乎没有保密性,纯粹是为了格式兼容。逆向时,看到Hex字符串,先尝试用CyberChef或Python的bytes.fromhex()转换一下,很可能就是关键的明文信息,如密钥、初始向量(IV)或校验和。URL编码则通常在Web逆向和抓包中遇到,浏览器的开发者工具或Burp Suite通常会自动解码。
2.3 古典编码:ROT13与XOR
这些算法简单,但常被用于CTF或一些轻量级混淆。
- ROT13:字母移位13位,自逆(加密解密同一操作)。识别特征是字符串由字母组成,但看起来像乱码。
“uryyb”解密后是“hello”。在代码中可能就是一个简单的循环加减13。 - XOR(异或):这是逆向中的“常客”,既可用于简单加密,也可用于复杂加密的组成部分。核心是
data[i] ^ key[i % key_length]。
XOR的逆向关键:
- 识别模式:在反汇编代码中,寻找对数据缓冲区进行循环,并与某个密钥(或固定值,如
0xFF)进行XOR操作的指令。在IDA的图形视图里,这常表现为一个循环块内有一个XOR指令。 - 提取密钥:如果已知部分明文(比如文件头
“PK\x03\x04”是ZIP,“\x89PNG”是PNG),可以通过密文 ^ 明文 = 密钥来推测密钥。这就是所谓的“已知明文攻击”。 - 单字节XOR:密钥为单个字节。可以通过分析密文字符的频率来爆破,因为英文文本中空格(0x20)频率最高。用密文与0x20异或,常常能直接得到密钥字节。
实操技巧:遇到一段数据看不出是什么编码,可以先尝试用
0xFF、0x00、0xAA等常见魔数进行单字节XOR,看结果是否出现可读字符串。这招在分析固件或游戏资源文件时有时有奇效。
3. 现代对称加密算法逆向:寻找密钥的“狩猎”
对称加密是逆向中的硬骨头,算法本身是公开的(如AES、DES、SM4),安全完全依赖于密钥的保密。我们的目标往往不是破解算法(那几乎不可能),而是找到程序中隐藏或生成的密钥。
3.1 AES:逆向中的“霸主”
AES(高级加密标准)是当今最流行的对称加密算法,在软件中无处不在。它有多种模式,如ECB、CBC、CTR等,这直接影响我们的逆向策略。
逆向识别特征(静态):
- 常量表(S盒):AES算法内部使用一个256字节的替换表(S-Box)。在反编译的代码中,查找一个大的、256字节的常量数组。标准的AES S盒开头是
0x63, 0x7c, 0x77, 0x7b, 0xf2...。找到它,几乎就坐实了AES。它的逆S盒也常同时存在。 - 轮常数(Rcon):另一个小的常量数组,通常10个或更多个32位整数。
- 函数名/库:使用OpenSSL、Crypto++等库时,函数名可能包含
AES_encrypt、AES_set_encrypt_key等。在Android的Java层,可能是javax.crypto.Cipher.getInstance(“AES/...”)。
逆向策略与动态分析:静态识别算法只是第一步,拿到密钥才是胜利。
- 密钥硬编码:最简单的情况。在IDA的字符串窗口或代码段中搜索可能的密钥字符串(16/24/32字节的Hex或Base64形式)。用
Find Crypt等IDA插件可以自动识别算法常量。 - 密钥动态生成:密钥由设备信息(IMEI、Android ID)、用户输入、服务器下发等计算而来。这时需要动态调试(如用Frida)来捕获密钥生成过程。关键Hook点:
- 密钥生成函数:Hook
AES_set_encrypt_key或Cipher.init函数,打印其密钥参数。 - 加密/解密函数:Hook
AES_encrypt/AES_decrypt或Cipher.doFinal,打印输入(明文/密文)、输出和内部状态(对于CBC模式,还需要IV)。
- 密钥生成函数:Hook
- 模式识别与影响:
- ECB模式:相同明文块产生相同密文块。不安全,且无IV。逆向时只需找密钥。
- CBC模式:最常用。需要密钥和IV(初始化向量)。IV可能随密文一起传输(通常放在密文前),也可能是固定的。在Hook时,必须同时获取IV。
- CTR/GCM模式:流加密模式。需要密钥和Nonce。思路类似。
实战案例(Frida Hook Android AES):假设目标App使用Java的Cipher类进行AES-CBC加密。
Java.perform(function() { var Cipher = Java.use(‘javax.crypto.Cipher’); Cipher.getInstance.overload(‘java.lang.String’).implementation = function(transformation) { console.log(‘[+] Cipher.getInstance: ‘ + transformation); return this.getInstance(transformation); }; Cipher.init.overload(‘int’, ‘java.security.Key’, ‘java.security.spec.AlgorithmParameterSpec’).implementation = function(opmode, key, params) { console.log(‘[+] Cipher.init: ‘); console.log(‘ opmode: ‘ + opmode); // 1=ENCRYPT_MODE, 2=DECRYPT_MODE console.log(‘ key: ‘ + bytesToHex(key.getEncoded())); if (params) { var ivSpec = Java.cast(params, Java.use(‘javax.crypto.spec.IvParameterSpec’)); console.log(‘ IV: ‘ + bytesToHex(ivSpec.getIV())); } return this.init(opmode, key, params); }; Cipher.doFinal.overload(‘[B’).implementation = function(input) { console.log(‘[+] Cipher.doFinal input: ‘ + bytesToHex(input)); var result = this.doFinal(input); console.log(‘[+] Cipher.doFinal output: ‘ + bytesToHex(result)); return result; }; });这段脚本能帮你捕获到算法模式、密钥、IV以及完整的输入输出数据。
3.2 SM4国密算法:国内应用的“标配”
随着国密推广,SM4在金融、政务、物联网等领域的App和软件中越来越常见。它是我国官方认定的商用密码算法,分组长度和密钥长度均为128位。
逆向识别特征:
- 常量S盒:SM4也有自己的固定S盒,与AES完全不同。其S盒是一个固定的256字节数组,开头是
0xd6, 0x90, 0xe9, 0xfe, 0xcc, 0xe1, 0x3d, 0xb7...。在IDA中搜索这些字节序列是识别SM4的最可靠方法。 - 固定常数FK和CK:SM4算法在密钥扩展中使用固定的系统参数FK(4个32位字)和CK(32个32位字)。这些常量也常出现在代码中。
- 函数名/库:可能使用
GMSSL、国密算法库等,函数名可能包含sm4。
逆向要点:逆向SM4的思路与AES完全一致。因为算法固定,核心仍是找密钥和模式(ECB、CBC等)。由于SM4在国内环境使用更多,其密钥可能来源于国密硬件盾、基于SM2的非对称协商,或从服务器获取。动态Hook的方法与AES类似,关键是定位到加密函数。
重要提示:无论是AES还是SM4,在非攻击性安全测试(如授权渗透测试)中,我们的目标是验证加密实现是否正确、密钥管理是否存在风险。在CTF竞赛中,目标则是利用漏洞或侧信道获取密钥。绝对不要将其用于非法破解。
3.3 DES与3DES:逐渐退役的“老兵”
DES(数据加密标准)因其56位密钥太短已不安全,3DES是DES的加强版。现在已不推荐使用,但在一些遗留系统或特定协议中还能遇到。
逆向识别特征:
- DES:识别其8个固定的S盒(每个S盒是4x16的查找表)和初始置换IP/逆初始置换IP-1表。这些表在代码中非常显眼。
- 3DES:本质是执行三次DES,密钥长度可以是112位或168位。在代码中可能看到三次DES加密/解密调用。
逆向策略:由于DES/3DES算法强度已不足,除了寻找硬编码密钥,在CTF中有时甚至可能尝试暴力破解(如果密钥空间不大)。但在实际App逆向中,找到密钥生成和存储的逻辑仍是主要途径。
4. 非对称加密与哈希算法:逆向中的“验证者”
非对称加密(如RSA)和哈希算法(如MD5、SHA256)在逆向中通常扮演“验证”或“签名”的角色,而不是用来加密大量数据。
4.1 RSA:公钥的“公开秘密”
在逆向中,你很少需要去破解RSA(在密钥长度足够时,这不可行)。更常见的是:
- 验证签名:服务器用私钥签名,客户端用内嵌的公钥验证。我们的目标是绕过验证,而不是破解私钥。方法可能是:Patch掉验证函数,使其永远返回成功;或者用Frida Hook验证函数,直接修改返回值。
- 分析密钥管理:公钥可能硬编码在App内。用
strings或搜索“BEGIN PUBLIC KEY”/“BEGIN RSA PUBLIC KEY”等PEM格式的标头可以找到。有时公钥被编码(Base64)或拆分成多个部分存储,需要拼接还原。 - 捕获加密数据:如果App用RSA加密一个临时密钥(即“数字信封”模式),那么我们需要Hook加密函数,捕获这个被加密的临时密钥。在内存中,它可能以
BigInteger或字节数组形式存在。
逆向识别:寻找大整数运算(模幂运算a^b mod n)、大素数生成、以及PEM格式的密钥字符串。OpenSSL的RSA_public_encrypt、RSA_verify等函数是明显的目标。
4.2 哈希算法(MD5, SHA系列):完整性的“指纹”
哈希是不可逆的。在逆向中遇到哈希,通常是为了:
- 完整性校验:校验文件、数据包是否被篡改。程序会计算接收数据的哈希值,与一个预设值比较。
- 密码存储:比较用户输入密码的哈希值与存储的哈希值。
- 生成密钥或标识:用哈希将一些输入数据变成固定长度的密钥。
逆向策略:
- 识别:MD5、SHA1、SHA256等都有固定的初始常量(魔数)。例如MD5的初始A、B、C、D是
0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476。在代码中搜索这些常量可以识别算法。 - 目标:我们的目的通常不是“反推”原文(这极难),而是:
- 绕过校验:找到比较哈希值的地方(往往是
memcmp或strcmp),通过调试修改比较结果,或直接Patch跳转指令。 - 构造碰撞(仅限CTF或研究):对于MD5、SHA1这类已发现碰撞漏洞的算法,在CTF中可能需要构造两个哈希值相同的不同输入。这需要专门的工具和方法。
- 理解逻辑:明白程序用哈希来做什么,是生成会话ID,还是计算文件校验和,这有助于理解整个程序的数据流。
- 绕过校验:找到比较哈希值的地方(往往是
5. 综合逆向实战:从识别到解密的完整链条
理论说再多,不如一个实战案例。假设我们拿到一个Android App,其某个网络请求的data参数是一串加密的Base64。我们的目标是弄清其加密方式并解密。
第一步:静态探查
- 用
jadx-gui或APKTool反编译APK。 - 搜索与
data参数相关的字符串,定位到网络请求库(如OkHttp、Retrofit)或相关代码。 - 搜索
“AES”、“DES”、“Cipher”、“encrypt”、“decode”等关键词。 - 如果发现
“SM4”或国密相关字符串,重点关注。 - 查看
assets或res/raw目录下是否有包含密钥或配置的文件。
第二步:动态分析(Frida + 抓包)
- 启动App和抓包工具(Charles/Fiddler),捕获加密的请求。
- 编写Frida脚本,Hook
javax.crypto.Cipher类的所有重载方法(如前文示例)。 - 触发网络请求,观察Frida输出。理想情况下,你能直接看到算法模式、密钥、IV和明文。
- 如果密钥是动态计算的,需要向上追溯。Hook密钥生成相关的函数,或者Hook
SecretKeySpec的构造函数。
第三步:算法验证拿到疑似密钥和算法后,需要用独立脚本验证。例如,用Python的cryptography库,使用捕获的密钥和IV,对捕获的密文进行解密,看是否能得到有意义的明文。
第四步:深度追踪(如需)如果加密在Native层(so库)进行,就需要使用IDA Pro或Ghidra进行反汇编,并结合Frida的Interceptor来Hook Native函数。这时,识别算法常量的技能就派上用场了。在Native代码中搜索AES的S盒或SM4的S盒字节序列,能快速定位加密函数。
6. 常见问题与排查技巧实录
逆向编码和加密的路上,坑永远不会少。这里记录一些我踩过的坑和总结的技巧。
问题1:静态分析找到了加密函数,但密钥是“一片空白”?
- 可能情况:密钥来自服务器,在运行时通过网络请求下发。或者密钥由多个部分拼接而成(如
part1 + deviceId + part2)。 - 排查技巧:全局搜索拼接字符串的操作(
+、StringBuilder.append)。动态Hook所有网络响应解析函数,查看是否有像密钥的数据。HookSharedPreferences或SQLite的读取操作,看是否从本地存储中读取了密钥片段。
问题2:Frida Hook Cipher类什么都没打印?
- 可能情况:App使用了自定义的加密实现(自己写的算法,或对标准库进行了封装、混淆),没有走标准的
CipherAPI。 - 排查技巧:查看调用栈,找到真正执行加密的类。或者直接Hook
libc的malloc/memcpy等函数,观察大块内存的分配和操作,结合反编译代码分析。也可以尝试使用objection等工具的android hooking watch class命令来探查所有被调用的方法。
问题3:解密出来的数据是乱码?
- 可能情况:
- 密钥或IV错了。
- 加密模式判断错误(比如以为是ECB,实际是CBC)。
- 密文在传输或处理时被修改了(如去掉了Padding)。
- 解密后还需要进行其他处理(如解压缩、二次解码)。
- 排查技巧:首先确认算法、模式、Padding方式是否完全匹配。用捕获的密钥和IV,对一段已知的明文进行加密,看得到的密文是否与抓包的一致。如果不一致,说明还有未知参数。检查密文长度是否符合算法分组的整数倍。
问题4:遇到代码混淆怎么办?
- 常见混淆:类名、方法名、字段名被重命名为
a, b, c,字符串被加密,控制流被扁平化或虚假分支。 - 应对策略:
- 字符串解密:寻找字符串解密函数。通常程序启动时会调用一个初始化函数,解密所有字符串到内存。用Frida Hook内存分配函数,在解密后去内存中dump字符串。
- 关键点定位:即使混淆,加密操作最终还是要调用底层的API(如
MessageDigest、Cipher)。直接Hook这些系统API是绕过混淆的好办法。 - 动态追踪:在未混淆的版本(如果有)或早期版本中定位关键函数,记录其特征(如参数数量、调用顺序),再到混淆版本中通过特征匹配。
问题5:如何判断一个自定义算法是编码还是弱加密?
- 观察输入输出:如果输出长度与输入有固定数学关系(如Base64膨胀约33%,Hex膨胀一倍),且输出字符集有限,很可能是编码。
- 尝试频率分析:对输出数据进行字节频率统计。如果分布均匀,类似随机数,可能是强加密。如果某些字节频率异常高,可能是简单替换或XOR。
- 搜索常量:在代码中搜索大的常量数组(S盒、置换表),这是识别标准或自定义加密/编码算法的强信号。
逆向工程是一个需要耐心、经验和大量实践的技术活。对编码和加密算法的熟悉,能帮你快速拨开迷雾,触及程序逻辑的核心。记住,你的武器库不仅仅是IDA和Frida,还包括对算法特征的深刻理解、严谨的假设验证流程,以及一份永不言弃的好奇心。从识别一个简单的Base64开始,慢慢积累,你也能成为破译程序秘密的专家。
