车联网应用白盒AES逆向实战:从定位到模拟加密器构建
1. 项目概述从“车智赢”到白盒AES的挑战最近在分析五菱汽车相关的车机应用特别是“车智赢”这类官方APP时遇到了一个典型的移动端安全加固场景。这类应用在与车机或后台服务器通信时核心的业务数据、控制指令往往都经过了加密处理。逆向分析的目标很明确找到加密算法还原密钥最终能够自主构造合法的请求或解密响应数据。在初步的静态分析中很快就定位到了AES加密的痕迹但事情并没有那么简单——常规的Hook密钥生成函数或者搜索常量表的方法失效了。这是因为开发者采用了AES白盒加密技术。简单来说白盒AES将标准的AES算法与密钥深度融合并进行了复杂的混淆和编码变换使得在逆向环境中你无法像传统黑盒分析那样直接“看到”或“导出”一个明文的密钥。整个加密过程被伪装成一系列查表和异或操作密钥信息被“打散”并隐藏在这些操作中。这对于车联网、物联网设备这类对本地安全有一定要求的场景来说是一种提升攻击门槛的有效手段。本次分享我就以一个从业者的角度拆解针对此类白盒AES的逆向分析思路、实操步骤以及那些容易踩坑的细节目标是让有Android逆向基础的朋友能有一套清晰的路径去应对这个挑战。2. 核心思路与技术选型为何静态分析与动态调试需双管齐下面对白盒AES单一的分析手段往往力不从心。我的核心思路是“动静结合由外及内”。2.1 为何选择动静结合首先纯静态分析直接阅读反编译的Smali或Java代码在面对高度混淆、甚至核心逻辑下沉到Native层so库的白盒实现时效率极低。你可能会看到大量难以理解的变量名和复杂的控制流。其次纯动态调试如Frida Hook虽然能捕获输入输出但对于白盒这种将算法过程本身混淆的技术仅仅知道输入输出对还原内部结构帮助有限。因此必须结合两者用动态调试快速定位加密函数入口、验证猜想用静态分析深入理解混淆后的算法结构寻找可能的还原模式。2.2 工具链选型与理由逆向工程平台Jadx-GUI与IDA Pro或Ghidra是黄金组合。Jadx用于快速分析Java层代码寻找JNI调用或可疑的加密类。一旦发现核心逻辑在Native层通常文件名为libxxx.so或包含whitebox、crypto等字样立即使用IDA Pro或Ghidra进行深度静态分析。Ghidra的开源和强大的反编译能力对于分析复杂控制流很有帮助而IDA的交互调试功能在动态分析时不可或缺。动态调试与Hook框架Frida是首选。它脚本灵活能够快速Hook Java和Native函数打印参数、返回值以及堆栈信息对于定位加密调用链、验证加密函数黑白名单判断是标准AES还是白盒AES至关重要。在需要更底层调试时会用到IDA Pro的远程调试功能附加到Android进程直接调试so库。密码学分析辅助CyberChef或自写的Python脚本。用于快速验证加密模式ECB/CBC、填充方式PKCS#7等以及对比输入输出推算可能的中间状态。在白盒分析中经常需要对比标准AES的中间轮输出与白盒实现的中间某步输出来定位混淆映射关系。注意在开始前请确保你分析的APP来自合法渠道用于安全研究目的并遵守相关法律法规。对商业软件进行逆向工程可能涉及法律风险务必在授权范围内进行。2.3 分析入口的寻找策略从哪里开始通常的入口点是网络请求。使用HttpCanary、Charles或Fiddler抓包观察APP的请求体和响应体。如果数据明显被加密表现为Base64编码后的乱码或直接的二进制数据记下这些请求的URL和可能的触发场景如登录、车辆状态查询。然后在Java层搜索这些URL字符串或者搜索常见的加密类名如Cipher,AES,Encrypt,Crypto等。一个更高效的方法是直接搜索JNI函数名特征如Java_com_xxx_encrypt或使用Frida Stalker对libc的fopen等函数进行Hook寻找加载的特定so库。3. 实操步骤拆解定位、识别与还原白盒AES假设我们已经通过抓包确认了数据加密并通过字符串搜索定位到了一个名为com.wuling.secure.WhiteBoxAESCrypto的类。下面展开具体步骤。3.1 第一步Java层动态Hook确认加密行为首先编写Frida脚本Hook这个可疑类的加密方法。目标不是直接获取密钥白盒中不存在独立密钥而是确认其输入输出与我们抓包的数据是否关联。Java.perform(function() { var WhiteBoxAESCrypto Java.use(com.wuling.secure.WhiteBoxAESCrypto); if (WhiteBoxAESCrypto) { // 假设有一个encrypt方法参数为byte[] WhiteBoxAESCrypto.encrypt.overload([B).implementation function(inputData) { console.log([] WhiteBoxAESCrypto.encrypt called!); console.log( Input (hex): bytesToHex(inputData)); console.log( Input (str): bytesToString(inputData)); var result this.encrypt(inputData); // 调用原方法 console.log( Output (hex): bytesToHex(result)); console.log( Output (b64): base64Encode(result)); // 可以将输入输出保存下来用于后续分析 send({input: bytesToHex(inputData), output: bytesToHex(result)}); return result; }; } }); // 辅助函数字节数组转十六进制字符串 function bytesToHex(bytes) { /* ... */ } function bytesToString(bytes) { /* ... */ } function base64Encode(bytes) { /* ... */ }运行脚本并触发一个网络请求比如点击登录。如果控制台打印的Output (b64)与抓包中请求体的数据一致那么恭喜你找到了加密的入口点。同时记录下多组不同的明文输入和对应的密文输出这对后续分析白盒结构至关重要。3.2 第二步深入Native层静态分析so库在Java层的WhiteBoxAESCrypto.encrypt方法中几乎肯定会通过JNI调用Native方法。使用Jadx查看该方法的代码找到类似native byte[] encryptNative(byte[]);的声明。对应的Native函数名通常为Java_com_wuling_secure_WhiteBoxAESCrypto_encryptNative。定位so文件在APK的lib/目录下通常是armeabi-v7a或arm64-v8a找到包含该函数名的so库。用IDA Pro或Ghidra加载它。识别白盒特征在反编译的C/C代码中白盒AES通常不具备标准的AES轮函数结构如SubBytes,ShiftRows,MixColumns,AddRoundKey。相反你会看到大量的静态数组查找.rodata段这些数组很大通常每个几KB到几十KB并且看起来是随机数据。这些就是白盒查找表T-boxes, Tyi tables等。主要的加密函数是一个循环循环体内主要是基于这些大数组的查表操作配合一些异或(XOR)和移位操作。几乎没有明显的密钥调度Key Schedule过程因为密钥已经编码在查找表里了。函数识别搜索函数名或字符串引用找到类似WBACRAES_EncryptOneBlock正如网络资料中提到的、whitebox_encrypt、wbaes_enc这样的函数。这很可能就是核心的单个分块加密函数。3.3 第三步理解白盒AES结构与还原思路标准的AES-128加密有10轮操作。在白盒实现中每一轮的SubBytes、ShiftRows、MixColumns和AddRoundKey会被合并并预先计算生成若干张查找表。加密时明文或中间状态被拆分成字节作为索引去查这些表查表结果再经过异或组合得到下一轮的状态。还原白盒AES的目标是逆向推导出这些查找表所对应的等效AES密钥。一个经典的方法是“差分功耗分析(DPA)攻击”的白盒变种或“代数分析”。但对于大多数逆向场景我们更实用的目标是“提取白盒查找表并构建一个功能等效的加密函数”而不必求出原始密钥。实操方法提取与模拟提取查找表在IDA的静态视图中定位到那些大的常量数组。记录它们的起始地址和大小。可以使用IDA的Python脚本或手动计算将这些数组的二进制数据导出到文件。例如你可能导出10组对应10轮每组4个对应状态矩阵的4列大小为256*4字节的表。分析查表逻辑仔细分析WBACRAES_EncryptOneBlock函数的汇编或反编译代码。跟踪一个明文字节的“旅程”它是如何被用作索引查了哪张表结果又和谁异或再作为下一张表的索引。用Python或C写一个模拟器尝试复现这个过程。输入一个已知的明文块比如全零用你提取的查找表一步步模拟计算看输出是否与Hook得到的密文一致。验证与调整这个过程需要极大的耐心和细心。你可能需要调整对查表顺序、异或顺序的理解。一个常见的技巧是使用多组已知的明文密文对作为测试向量不断修正你的模拟器逻辑直到对所有测试向量都能正确加密。实操心得白盒实现的细节千差万别。有的会将ShiftRows合并到查表里有的会使用不同的编码输入/输出编码来增加分析难度。如果遇到输出编码即白盒输出的密文还不是标准的AES密文还需要经过一个额外的逆变换才能得到标准密文这通常需要分析加密函数最后的几段操作。关键是多组数据对比寻找规律。4. 核心环节实现构建等效加密器假设经过艰苦的静态分析和动态跟踪我们终于理解了so库中白盒加密的完整流程并成功提取了所有必要的查找表。接下来就是构建一个独立的、不依赖原so库的加密器。这里以Python为例说明关键步骤。4.1 数据结构定义首先将提取的二进制表数据加载到Python中。表的结构取决于你的分析结果。例如可能是一个三维列表tables[round][table_index][byte_value]。import struct def load_tables_from_bin(file_path, table_layout): 从二进制文件加载白盒查找表。 table_layout: 一个元组描述表的结构如 (10, 4, 256) 表示10轮每轮4张表每张表256个4字节项。 with open(file_path, rb) as f: data f.read() # 假设每个表项是uint32_t table_size table_layout[0] * table_layout[1] * table_layout[2] * 4 if len(data) ! table_size: print(f文件大小{len(data)}与布局预期{table_size}不符) return None tables [] offset 0 for r in range(table_layout[0]): # 轮数 round_tables [] for t in range(table_layout[1]): # 每轮表数量 table [] for i in range(table_layout[2]): # 每张表大小256 value struct.unpack(I, data[offset:offset4])[0] # 小端序 table.append(value) offset 4 round_tables.append(table) tables.append(round_tables) return tables # 假设分析得出的布局是10轮每轮有4张256大小的表 whitebox_tables load_tables_from_bin(extracted_wb_tables.bin, (10, 4, 256))4.2 加密流程模拟根据逆向出的算法实现加密函数。以下是一个高度简化的伪代码框架真实情况复杂得多def whitebox_encrypt_one_block(plaintext_block, tables): 模拟白盒AES加密一个16字节的分块。 plaintext_block: bytes, 长度16 tables: 加载的白盒查找表结构 # 1. 初始轮操作可能包含输入编码或与第一轮查表合并 state plaintext_block # 可能要先做某种变换 # 2. 主轮循环 (假设9轮完整轮 1轮最终轮) for round_idx in range(10): round_tables tables[round_idx] # 获取当前轮的表 new_state bytearray(16) # 这里需要根据具体的查表-异或网络来编写 # 例如一种可能的结构是每个输出字节由4个查表结果异或得到 for i in range(16): # 遍历状态矩阵的每个字节位置 # 计算这个位置需要查哪几张表以及索引是什么 # 这完全取决于逆向出的逻辑图 # table_idx_0, table_idx_1, table_idx_2, table_idx_3 get_table_indices_for_position(i, state) # index_0, index_1, index_2, index_3 get_indices_for_position(i, state) # val (round_tables[table_idx_0][index_0] ^ # round_tables[table_idx_1][index_1] ^ # round_tables[table_idx_2][index_2] ^ # round_tables[table_idx_3][index_3]) # new_state[i] val 0xFF # 取最低字节 pass # 实际代码需替换 state bytes(new_state) # 最后一轮可能不同没有MixColumns对应白盒中可能用不同的表 if round_idx 9: # 最后一轮 # 可能还有输出解码操作 state final_output_transform(state) return state def encrypt_data(data, tables, modeECB): 封装分块和模式处理 # PKCS#7填充 pad_len 16 - len(data) % 16 data bytes([pad_len] * pad_len) cipher_blocks [] for i in range(0, len(data), 16): block data[i:i16] if mode ECB: cipher_block whitebox_encrypt_one_block(block, tables) elif mode CBC: # 需要处理初始向量IV这里简化 # block xor(block, previous_cipher_block or IV) # cipher_block whitebox_encrypt_one_block(block, tables) pass cipher_blocks.append(cipher_block) return b.join(cipher_blocks)4.3 测试验证使用之前Frida Hook收集的多组明文密文测试向量对你的whitebox_encrypt_one_block函数进行测试。# 测试用例 test_vectors [ (b\x00*16, bytes.fromhex(对应密文十六进制)), (b1234567890abcdef, bytes.fromhex(...)), # ... 更多组 ] for plain, expected_cipher in test_vectors: cipher whitebox_encrypt_one_block(plain, whitebox_tables) if cipher expected_cipher: print(f测试通过: {plain.hex()} - {cipher.hex()}) else: print(f测试失败: {plain.hex()}) print(f 预期: {expected_cipher.hex()}) print(f 实际: {cipher.hex()}) # 这里就需要回头检查查表逻辑、索引计算或轮次处理是否正确只有通过所有测试向量才能说明你的白盒模拟器是基本正确的。这个过程可能需要反复迭代几十次。5. 常见问题与排查技巧实录在实际操作中你会遇到各种各样的问题。下面记录一些典型场景和解决思路。5.1 问题Hook到了加密函数但输入输出对不上号现象Frida脚本打印的输入明文看起来是正常的比如JSON字符串但输出的密文与抓包数据完全不同甚至长度都不对。排查检查调用时机确认你Hook的函数是否真的是用于网络请求加密的那个。可能APP有多个加密函数用于不同场景。尝试Hook所有疑似加密的方法并对比触发网络请求时的调用栈。检查输入预处理明文在传入Native函数前可能已经被处理过比如进行了压缩、编码如UTF-16LE或添加了固定前缀/后缀。在Hook的Java层函数入口处多打印一些上下文信息或者向上追溯调用栈。检查输出后处理Native函数返回后Java层可能对结果进行了二次处理如Base64编码、Hex编码或者拼接了其他字段。查看encrypt方法的返回值被用在了哪里。5.2 问题静态分析so库时查找表数量或结构与预期不符现象AES-128应该有10轮但你只找到了8组或12组表或者每轮的表不是4张。排查算法变体确认是否是AES-192或AES-256它们的轮数分别是12和14。检查常量中是否有线索或者通过测试不同密钥长度的标准AES与白盒输出对比来推断。合并与优化白盒实现为了效率经常将多轮操作甚至所有轮操作合并到更少的查表阶段中。你可能找到了“超级表”。需要更仔细地分析数据流看一个明文字节是否通过一次查表就直接走到了好几轮之后的状态。编码表分离输入编码和输出编码可能使用独立的表这些表可能不在主加密循环附近需要全局搜索大数组。5.3 问题模拟器加密结果与Hook结果不一致现象你已经尽力还原了查表逻辑但加密结果总是错几个字节或者完全不对。排查字节序问题x86/PC和ARM的字节序可能都是小端但表中存储的32位整数你在提取和加载时使用的字节序struct.unpack的格式字符I小端I大端必须与目标平台一致。通常ARM是小端但最好验证。索引计算错误这是最常见的问题。白盒算法中用于查表的索引计算可能非常绕涉及位掩码、移位和复杂的异或。用单个测试用例在IDA中单步调试或通过Frida Trace Native指令记录下每一轮每一个操作的实际索引值和查表结果与你模拟器中的计算逻辑逐条对比。遗漏了初始/最终变换输入编码和输出编码可能不仅仅是简单的查表可能是与某个常量异或或进行字节替换。对比标准AES加密中间某一轮的状态如果你能通过其他方式推算的话与白盒模拟对应步骤的状态可以帮助发现这些编码。表数据提取错误确认你从IDA中导出表数据的起始地址和大小完全正确。有时候表与表之间可能有填充对齐或者IDA的段视图划分不准确。可以尝试导出整个数据段.rodata然后在你的模拟器中用偏移量去读取。5.4 问题性能与集成现象Python模拟器太慢无法用于实际的重放攻击或批量解密。解决关键路径用C/C重写将核心的whitebox_encrypt_one_block函数用C实现并编译为Python扩展模块如使用ctypes或Cython速度可以有百倍提升。查找表内存化确保所有查找表在进程生命周期内只加载一次放在全局变量或类静态变量中。考虑移植到其他语言如果需要集成到其他系统如服务器端可以用Go、Rust或直接C语言重写整个逻辑。5.5 高级技巧利用符号执行或中间表示IR分析对于极其复杂的白盒实现手动逆向查表逻辑如同大海捞针。可以尝试使用更高级的分析工具Ghidra的脚本化分析编写Ghidra脚本尝试对加密函数进行数据流分析自动推导查表网络。Triton或angr框架这些符号执行引擎可以帮助你构建输入输出之间的约束关系理论上可以自动求解出等效的加密函数。但这需要较高的学习成本且对路径爆炸问题处理起来比较棘手。最后白盒AES逆向是一场耐心和细心的较量。它没有通用的“一键解密”工具每一个目标都需要定制化的分析。成功的标志不仅仅是能解密数据更是彻底理解了这个特定白盒实现的设计思路这份理解本身的价值远超过破解一个具体的APP。在整个过程中保持详细的笔记和记录至关重要因为回溯和验证是家常便饭。当你构建的模拟器终于吐出一串与服务器响应完全匹配的解密数据时那种成就感就是逆向工程最大的乐趣所在。