ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

逆向工程中RC4流密码原理、C实现与魔改变体识别

逆向工程中RC4流密码原理、C实现与魔改变体识别 1. 逆向场景里为什么总绕不开RC4做逆向分析的人迟早会在某个加密函数里撞见RC4。它不像AES那样有一堆S盒和轮密钥的复杂结构也不像RSA那样涉及大数运算但它的出场频率高得离谱——尤其是当你分析一些年代稍早的客户端、嵌入式固件、老版本通信协议或者某些App的本地数据加密逻辑时RC4几乎是绕不过去的一道坎。RC4全称Rivest Cipher 41987年由Ron Rivest设计属于流密码stream cipher的一种。它的核心思路非常朴素用一个密钥生成一串伪随机字节流然后把这串字节流和明文逐字节做异或运算得到密文。解密时用同样的密钥生成同样的字节流再异或一次就还原了。异或运算的性质决定了加密和解密是完全对称的同一个函数既能加密也能解密。这个算法在逆向中的典型出场场景包括某些App的本地配置文件加密密钥硬编码在so或dex里网络协议中某个字段的混淆处理比如登录参数、签名串的二次加密固件升级包的校验数据加密老版本Web应用的Cookie或Token生成逻辑游戏资源文件的简单保护为什么逆向人员需要专门掌握RC4因为它的实现变体非常多。标准RC4的KSA密钥调度算法和PRGA伪随机生成算法结构固定但实际代码里经常遇到各种魔改版本S盒初始化方式不同、密钥扩展轮数变化、异或的时机调整、甚至把S盒和密钥流生成揉在一起写。如果你不能快速识别出“这就是RC4的变体”就很容易在逆向过程中卡住浪费大量时间。这篇文章面向的是有一定逆向基础、但还没系统梳理过RC4的读者。我会从RC4的核心原理讲起然后带你一步步用C语言实现一个标准版本再分析逆向中常见的变体和识别方法最后分享一些实战中排查问题的经验。目标很明确让你在十分钟内建立起对RC4的完整认知框架下次在IDA或JEB里看到类似代码时能一眼认出来。注意本文所有内容仅用于合法的安全研究、软件兼容性分析和学习目的。请勿将相关技术用于未经授权的逆向工程。2. RC4算法的核心原理拆解2.1 流密码的基本工作模式要理解RC4先得搞清楚流密码和分组密码的区别。分组密码比如AES、DES是把数据切成固定长度的块每块单独加密块与块之间可能还有链接模式。流密码则是把数据看成一串连续的字节用一个密钥流逐字节处理加密和解密都是异或操作。流密码有个天然优势不需要填充。分组密码如果数据长度不是块大小的整数倍就得填充到完整块解密后再去掉填充。流密码没这个问题来多少字节就处理多少字节非常适合处理长度不固定的数据比如网络包、配置文件、字符串参数。RC4的密钥流生成完全依赖于一个256字节的状态数组S和一个密钥调度过程。整个算法没有复杂的数学运算全是字节级的加法和交换这也是它能在资源受限的环境里广泛使用的原因。2.2 KSA密钥调度算法KSA的目标是把一个变长密钥通常5到256字节扩展成一个256字节的初始状态数组S。标准流程是这样的初始化S数组令S[i] ii从0到255用一个临时数组K存放密钥如果密钥长度小于256就循环填充K直到长度达到256用j作为索引遍历i从0到255执行j (j S[i] K[i]) mod 256交换S[i]和S[j]这个过程看起来简单但它的作用是把密钥的随机性扩散到整个S数组中。密钥的每一个字节都会影响后续所有的交换操作最终S数组的排列与密钥强相关。这里有个细节值得注意K数组的填充方式。标准实现里如果密钥长度是keylen那么K[i] key[i % keylen]。这个取模操作在逆向时经常被优化成循环赋值或者用位运算替代但逻辑是一样的。2.3 PRGA伪随机生成算法KSA完成后S数组已经是一个与密钥相关的排列。PRGA阶段负责从这个排列中不断生成密钥流字节初始化i 0, j 0每次生成一个字节时i (i 1) mod 256j (j S[i]) mod 256交换S[i]和S[j]输出S[(S[i] S[j]) mod 256]作为密钥流字节将输出的字节与明文/密文对应字节异或PRGA每生成一个字节都会修改S数组的状态所以密钥流是动态变化的。同一个密钥在不同位置生成的字节流完全不同这也是RC4不需要IV初始化向量的原因——当然这也带来了安全隐患后面会提到。2.4 加密与解密的对称性RC4的加密和解密是同一个操作用相同的密钥初始化生成相同的密钥流然后与数据异或。假设明文是P密钥流是KS密文C P XOR KS。解密时P C XOR KS因为(P XOR KS) XOR KS P。这个对称性在逆向中非常有用。当你发现一个函数对数据做了异或操作而且异或的另一边是一个动态生成的字节序列那基本可以确定是流密码。如果这个字节序列的生成逻辑里出现了256大小的数组和交换操作那大概率就是RC4或其变体。3. 用C语言手写一个标准RC4实现3.1 代码结构与函数划分为了便于理解和后续在逆向中对照我把RC4拆成三个函数KSA初始化、PRGA生成密钥流、加解密入口。这种划分方式在实际逆向中也很常见因为编译器优化后虽然可能内联但逻辑边界通常还是清晰的。#include stdio.h #include string.h typedef struct { unsigned char S[256]; int i; int j; } RC4_CTX; void rc4_init(RC4_CTX *ctx, const unsigned char *key, int keylen) { int i, j 0; unsigned char K[256]; unsigned char tmp; for (i 0; i 256; i) { ctx-S[i] (unsigned char)i; K[i] key[i % keylen]; } for (i 0; i 256; i) { j (j ctx-S[i] K[i]) % 256; tmp ctx-S[i]; ctx-S[i] ctx-S[j]; ctx-S[j] tmp; } ctx-i 0; ctx-j 0; } unsigned char rc4_next_byte(RC4_CTX *ctx) { unsigned char tmp; ctx-i (ctx-i 1) % 256; ctx-j (ctx-j ctx-S[ctx-i]) % 256; tmp ctx-S[ctx-i]; ctx-S[ctx-i] ctx-S[ctx-j]; ctx-S[ctx-j] tmp; return ctx-S[(ctx-S[ctx-i] ctx-S[ctx-j]) % 256]; } void rc4_crypt(RC4_CTX *ctx, const unsigned char *in, unsigned char *out, int len) { int k; for (k 0; k len; k) { out[k] in[k] ^ rc4_next_byte(ctx); } }这段代码是标准RC4的教科书实现。RC4_CTX结构体保存了S数组和两个索引i、j这样可以在流式处理时保持状态。rc4_init完成KSArc4_next_byte完成PRGA的单字节生成rc4_crypt负责异或。3.2 关键参数的选择与计算密钥长度在标准RC4中没有硬性限制从1字节到256字节都可以。但实际使用中常见的选择是5到16字节早期WEP协议用5或13字节一些简单应用用8或16字节16字节对应128位比较常见32字节对应256位安全性稍好但RC4本身的问题不在密钥长度密钥内容通常是ASCII字符串、MD5/SHA1哈希值、或者从其他密钥派生函数输出的字节序列。在逆向时如果你看到密钥是从某个字符串常量直接读取的那基本就是硬编码密钥如果密钥是经过某种变换生成的就需要继续追溯变换逻辑。K数组的填充方式也值得注意。标准实现用key[i % keylen]但有些实现会先把密钥复制到K的前keylen个字节然后后面的字节用0填充或者用某种模式重复。这些变体在逆向时需要根据实际代码判断。3.3 编译与测试验证写完之后必须验证正确性。我通常用已知的测试向量来检查int main() { unsigned char key[] Key; unsigned char plaintext[] Plaintext; unsigned char ciphertext[16]; unsigned char decrypted[16]; RC4_CTX ctx; rc4_init(ctx, key, 3); rc4_crypt(ctx, plaintext, ciphertext, 9); printf(Ciphertext: ); for (int i 0; i 9; i) { printf(%02X , ciphertext[i]); } printf(\n); rc4_init(ctx, key, 3); rc4_crypt(ctx, ciphertext, decrypted, 9); decrypted[9] \0; printf(Decrypted: %s\n, decrypted); return 0; }用密钥Key加密Plaintext标准RC4的输出应该是BB F3 16 E8 D9 40 AF 0A D3。如果你自己实现的输出和这个一致说明KSA和PRGA都正确。如果不一致重点检查K数组填充和交换逻辑。提示测试时一定要用已知向量验证不要凭感觉判断。RC4的变体很多差一个字节的偏移就会导致完全不同的输出。4. 逆向中RC4的常见变体与识别方法4.1 魔改KSA的几种典型方式标准KSA的循环是256次但实际逆向中经常遇到变体变体一循环次数不是256。有些实现只循环128次或者循环512次相当于两轮。循环次数减少会降低密钥扩散效果但代码看起来仍然像RC4。识别方法是看循环计数器的上限值。变体二交换条件不同。标准实现是无条件交换S[i]和S[j]但有些变体会加一个判断比如只有当i ! j时才交换或者根据某个标志位决定是否交换。这种变体在逆向时容易迷惑人因为核心的加法取模还在但交换逻辑变了。变体三K数组生成方式不同。标准是K[i] key[i % keylen]但有些实现会先把密钥做一次哈希然后用哈希值填充K或者用密钥的每个字节重复多次填充。识别时重点看K数组的来源。变体四S盒初始值不是0到255。标准是S[i] i但有些实现会用一个固定的置换表初始化S或者用密钥相关的值初始化。这种变体已经偏离RC4较远但核心的PRGA结构可能还在。4.2 PRGA阶段的调整与混淆PRGA的变体通常更隐蔽因为它的代码量小容易被忽略调整一输出字节的计算方式。标准是S[(S[i] S[j]) % 256]但有些实现会改成S[(S[i] S[j] 某个常量) % 256]或者用异或代替加法。这种改动会改变密钥流但整体结构不变。调整二i和j的更新顺序。标准是先更新i再更新j但有些实现会反过来或者同时更新。顺序不同会导致S数组的交换序列不同密钥流完全不同。调整三交换与输出的顺序。标准是先交换再输出但有些实现会先输出再交换或者输出后再做一次额外交换。这种细节在逆向时需要通过动态调试确认。调整四密钥流缓存。有些实现会一次性生成一大块密钥流缓存起来而不是每次异或时生成一个字节。这种优化在逆向时表现为一个大的循环里面连续调用PRGA逻辑。4.3 在IDA和JEB中快速定位RC4在IDA中识别RC4我通常关注这几个特征一个256字节的栈数组或全局数组初始化时用循环赋值0到255一个嵌套循环外层256次内层有加法和取模运算取模运算通常被优化成 0xFF因为256是2的幂交换操作表现为三次赋值或xchg指令异或操作出现在数据处理的循环里在JEB或jadx中分析Android应用时RC4通常出现在native层so文件或Java层的工具类里。Java实现的特征是byte[] S new byte[256]循环里出现(S[i] 0xFF)这样的写法因为Java的byte是有符号的取模用% 256或 0xFF交换用临时变量一个实用的技巧是搜索常量256和0xFF的出现位置然后看附近的代码是否有循环和交换。如果某个函数里同时出现了这两个常量而且有数组操作那大概率是RC4或类似算法。4.4 动态调试确认算法类型静态分析只能给出猜测最终确认还是要靠动态调试。我的做法是在疑似RC4初始化的地方下断点观察S数组的变化如果S数组从0到255的顺序排列变成了一个看似随机的排列说明KSA执行了继续执行观察每次异或前生成的字节是否来自S数组的动态索引修改密钥或输入数据看输出是否随之变化验证密钥流的生成逻辑动态调试的好处是可以直接看到中间状态不用完全依赖代码阅读。特别是遇到魔改变体时通过观察S数组的变化规律可以快速判断哪些部分是标准RC4哪些被改动了。5. 实战排查与常见问题速查5.1 解密结果乱码的排查思路这是逆向RC4时最常见的问题你觉得自己找到了正确的密钥和算法但解密出来的数据是乱码。排查顺序应该是第一步确认密钥是否正确。密钥可能不是你以为的那个字符串。检查密钥是否经过了编码转换比如Base64解码、十六进制转换、是否被截取了一部分、是否与其他数据拼接。第二步确认算法是否是标准RC4。用相同的密钥和密文分别用标准RC4和你的实现跑一遍对比输出。如果标准RC4能解出正确结果说明你的实现有问题如果都不行说明算法有变体。第三步确认数据边界。RC4是流密码密钥流的生成与数据位置强相关。如果你从密文的中间开始解密或者跳过了前面的字节密钥流就对不上。确认你处理的数据是从加密的起始位置开始的。第四步确认异或的数据是否正确。有时候密文可能经过了额外的编码比如Base64、十六进制字符串、或者与其他数据做了拼接。先还原到原始字节再解密。5.2 密钥流不同步的问题RC4的密钥流是位置相关的同一个密钥在不同位置生成的字节不同。如果你在解密时发现前几个字节正确后面开始乱码通常是密钥流不同步。常见原因包括加密时先异或了某个固定头部解密时没有跳过加密和解密使用的密钥相同但初始化次数不同比如加密时初始化了一次解密时初始化了两次数据中间有插入或删除的字节导致后续位置偏移解决方法是在动态调试中对比加密和解密时的S数组状态找到第一次出现差异的位置然后回溯原因。5.3 常见问题速查表问题现象可能原因排查方法解密结果完全乱码密钥错误或算法不对用标准RC4验证检查密钥来源前几字节正确后面乱码密钥流不同步检查数据起始位置和初始化次数解密结果部分正确数据中间有额外处理检查是否有分段加密或填充找不到密钥密钥是动态生成的追溯密钥生成函数可能是哈希或派生算法识别困难遇到魔改变体动态调试观察S数组变化性能异常慢实现中有额外循环检查是否有不必要的重复初始化5.4 几个实用的避坑经验经验一不要假设密钥是字符串。很多实现里密钥是字节数组可能是哈希值、随机数、或者从其他数据派生出来的。看到字符串常量不要直接当成密钥先确认它是否被用于KSA。经验二注意字节序问题。在跨平台分析时密钥和数据的字节序可能不同。特别是从网络包或文件里提取的数据要确认是大端还是小端。经验三RC4的S数组初始状态很关键。标准RC4的S数组初始是0到255的顺序排列。如果你在动态调试时发现S数组的初始状态不是这个说明要么KSA已经执行过了要么这是一个变体。经验四不要忽略取模运算的优化形式。编译器通常把% 256优化成 0xFF把% keylen优化成条件减法或乘法。在逆向时看到位运算不要懵先想想它是不是在模拟取模。经验五多版本对比。如果你手上有多个版本的App或固件对比它们之间的RC4实现差异往往能快速定位关键改动点。版本迭代时开发者可能只改了密钥或某个参数算法主体不变。6. 从RC4延伸到其他流密码的识别掌握了RC4的识别方法后你会发现其他流密码也有类似的模式。比如RC4的改进版RC4A、Spritz以及完全不同的ChaCha20、Salsa20它们在结构上各有特点但核心思路都是“密钥生成状态状态生成密钥流密钥流异或数据”。在逆向中遇到不认识的流密码时可以按这个框架去分析状态数组的大小是多少256字节通常是RC4系16或32字节可能是ChaCha/Salsa状态初始化用了什么运算加法和交换是RC4系加法和异或是ChaCha系密钥流生成是否依赖位置依赖的是同步流密码不依赖的是自同步流密码是否有额外的非线性操作RC4没有ChaCha有 quarter round 函数这个分析框架能帮你在遇到新算法时快速归类然后有针对性地搜索资料或做动态验证。我个人在实际逆向中的体会是RC4本身并不复杂复杂的是各种魔改变体和与其他算法的组合使用。有时候一个函数里前半段是RC4后半段又接了Base64或AES需要分段拆解。关键是要有耐心从已知的标准实现出发逐步对比差异利用动态调试观察中间状态最终一定能还原出完整的加密逻辑。
返回列表