资讯中心

先xor再add反运算:逆向题加密逻辑拆解与Python脚本实现

📅 2026/10/11 0:01:00
先xor再add反运算:逆向题加密逻辑拆解与Python脚本实现
简介这份docx文档聚焦数据恢复与逆向工程中的核心难点面向具备一定二进制运算基础的安全从业者与逆向爱好者系统讲解“先异或再加法”的反运算推导思路。内容围绕字节拆分、固定字节定位、二次异或消除位模式等关键技巧展开通过46 49 4C 45 30 00经XOR 32与ADD 8F变为03 0A 0D 06 91 C1的实例完整演示如何以10为基数逐次加10观察高低4位变化最终推导出ADD 71 XOR 32的反向组合并说明加减法结合性带来的等价替换。资源包内仅含1个docx文档大小约48KB以文字与推导过程为主便于随时查阅与对照练习。目前已有647人学习适合需要掌握复杂数据还原逻辑、提升逆向推理能力的读者参考可帮助理解WinHex等工具中的运算验证方法为实际数据恢复与逆向分析提供可复用的排错思路。1. 先xor再add反运算思路从一道逆向题看加密逻辑的拆解拿到一个二进制文件拖进反汇编器发现核心校验逻辑只有两行先对输入做逐字节异或再整体加上一个偏移量最后跟目标数组比对。这种「先xor再add」的组合在CTF逆向和实际固件校验里出现频率极高但很多人卡在反推环节——知道正向怎么算却不知道怎么从目标值倒推原始输入。问题出在加法进位会污染异或的可逆性直接对目标值减偏移再异或结果往往是一堆乱码。这篇笔记就围绕这个反运算思路展开把数学原理、手工推导、脚本实现和常见翻车点讲透。适合正在做逆向入门、需要写反推脚本的开发者也适合想理解混合运算可逆性边界的老手。2. 先xor再add的数学结构与可逆性分析2.1 为什么单独xor和单独add都可逆合起来却容易翻车异或运算的可逆性来自自反性a ^ b ^ b a。加法运算的可逆性来自逆元(a b) - b a。单独看两者都是完美可逆的。但组合成f(x) (x ^ k) b之后问题就来了——加法产生的进位会改变高位比特而异或操作是按位独立的不产生跨位影响。这意味着你不能简单地把加法逆运算和异或逆运算拆开独立执行。具体来说正向计算是先x ^ k得到中间值m再m b得到结果y。反推时你拿到y第一步做y - b得到m这一步没问题减法确实能还原加法。第二步做m ^ k得到x这一步也没问题异或自反。那为什么说会翻车翻车场景出现在你以为可以交换顺序的时候。有人会想既然(x ^ k) b y那我先对y ^ k再减b行不行不行。因为(x ^ k) b和(x b) ^ k是完全不同的两个函数。加法进位会改变哪些比特位参与异或顺序一旦搞反结果就全错。这是最常见的理解误区也是很多脚本跑出乱码的根因。2.2 逐字节处理与整体处理的边界在实际逆向场景中xor和add的操作粒度决定了反推策略。常见的有三种组合操作粒度正向形式反推策略典型场景逐字节xor 逐字节addy[i] (x[i] ^ k[i]) b[i]逐字节独立反推简单校验、固件头校验逐字节xor 整体addy[i] (x[i] ^ k[i]) B先整体减B再逐字节xor带全局偏移的授权码整体xor 整体addY (X ^ K) B多字节整数需按位分析进位链协议序列号、哈希混淆逐字节独立处理是最简单的情况每个字节互不影响反推就是逐个字节做减法和异或。整体加法的情况稍复杂因为加法进位会跨字节传播但如果你知道B是常数先对每个字节减去B的低字节部分再处理进位借位仍然可以还原。真正麻烦的是多字节整数整体异或后再整体加法这时候进位链和异或掩码交织在一起需要逐位推导。我一般会先判断操作粒度看反汇编里循环的步长是1还是4或8看加法指令操作的是字节寄存器还是通用寄存器。步长为1且用AL寄存器做add基本就是逐字节用EAX或RAX做add就是整体处理。这个判断决定了后续反推脚本怎么写。2.3 反推公式的推导从目标值倒推输入以逐字节xor加整体add为例正向逻辑是for i in range(len(input)): m[i] input[i] ^ key[i] y sum(m) B # 或者每个字节加B后拼接更常见的形式是每个字节独立加同一个偏移y[i] (input[i] ^ key[i]) B反推时m[i] y[i] - B input[i] m[i] ^ key[i]这里的关键约束是y[i] - B必须在0到255之间否则说明B不是全局常数或者存在模256截断。很多实际代码里加法后会做 0xFF这时候反推要加回模数m[i] (y[i] - B) 0xFF input[i] m[i] ^ key[i]如果忘了这个模运算负数结果会导致异或出错。这是第二个高频翻车点。3. 用Python复现反推脚本从硬编码到通用框架3.1 最小可复现案例8字节校验的完整反推先构造一个已知输入和密钥正向算出目标值再写反推脚本验证。这样能确保你理解每一步的对应关系。# 正向计算模拟目标程序的校验逻辑 def forward(input_bytes, key, offset): 先逐字节xor再逐字节add偏移最后模256 result [] for i in range(len(input_bytes)): m input_bytes[i] ^ key[i % len(key)] y (m offset) 0xFF result.append(y) return bytes(result) # 反推计算从目标值还原输入 def reverse(target_bytes, key, offset): 先减偏移再xor注意模256处理 result [] for i in range(len(target_bytes)): m (target_bytes[i] - offset) 0xFF x m ^ key[i % len(key)] result.append(x) return bytes(result) # 测试 key b\x12\x34\x56\x78 offset 0x1A original bABCDEFGH target forward(original, key, offset) print(f正向结果: {target.hex()}) recovered reverse(target, key, offset) print(f反推结果: {recovered}) assert recovered original, 反推失败 print(验证通过)这段代码的核心在于 0xFF的位置。正向时加法后模256反推时减法后也要模256。Python的负数取模行为和C语言不同(-5) 0xFF在Python里得到251在C里如果直接用%可能得到-5。所以反推脚本里统一用 0xFF而不是% 256避免符号问题。参数说明key是异或密钥长度可以和输入不同用i % len(key)循环使用offset是加法偏移范围0到255target_bytes是从二进制里提取的目标数组通常是硬编码在.rodata段或栈上的立即数。3.2 从反汇编提取密钥和偏移的实操步骤实际逆向时密钥和偏移不会直接告诉你需要从汇编里抠出来。以常见的x86-64为例典型校验循环长这样movzx eax, byte ptr [rsircx] ; 读输入字节 xor eax, 0x12 ; 异或密钥 add eax, 0x1A ; 加偏移 and eax, 0xFF ; 模256 cmp al, byte ptr [rdxrcx] ; 跟目标比对 jne fail inc rcx cmp rcx, 8 jl loop提取步骤找到循环体里的xor指令立即数就是密钥字节。如果密钥是数组会看到xor eax, dword ptr [rip0x...]需要去数据段读。找到add指令立即数就是偏移。注意有些编译器会用lea替代add比如lea eax, [rax0x1A]效果一样。找到and指令确认模数没有的话说明结果可能溢出到高位但比对时只取低字节效果等同模256。目标数组在cmp指令的右操作数通常是[rdxrcx]或[rip0x...]去对应地址读原始字节。如果密钥长度超过1字节循环里会有取模或索引操作。常见的是key[i % key_len]汇编里表现为movzx eax, byte ptr [key rcx % len]其中rcx % len可能用div或位运算实现。遇到2的幂长度密钥编译器会用and替代取模比如and ecx, 3对应长度4的密钥。3.3 处理多字节密钥和变长偏移的通用写法当密钥长度大于1且不是2的幂时反推脚本需要正确处理索引。下面是一个通用框架支持任意长度密钥和逐字节偏移数组def reverse_generic(target_bytes, key, offsets): target_bytes: 目标字节序列 key: 异或密钥bytes类型任意长度 offsets: 加法偏移可以是单个int或与target等长的list if isinstance(offsets, int): offsets [offsets] * len(target_bytes) result bytearray() for i, y in enumerate(target_bytes): m (y - offsets[i]) 0xFF x m ^ key[i % len(key)] result.append(x) return bytes(result) # 测试变长密钥和变长偏移 key2 b\xAA\xBB\xCC offsets2 [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08] original2 b12345678 target2 bytes([((original2[i] ^ key2[i % 3]) offsets2[i]) 0xFF for i in range(8)]) recovered2 reverse_generic(target2, key2, offsets2) print(f变长测试: {recovered2}) assert recovered2 original2这个框架的关键改进是offsets参数支持列表因为有些程序会对不同位置加不同偏移比如offset[i] i * 3 1。遇到这种情况你需要从汇编里逐条提取add指令的立即数或者找到偏移表的地址直接读。提示如果反推结果出现大量不可打印字符先检查 0xFF是否遗漏再检查密钥索引是否从0开始。有些程序密钥索引从1开始对应key[(i1) % len]这种细节在汇编里表现为循环计数器初始值为1。4. 避坑与排查反推结果不对时先查这五处4.1 现象反推结果全是乱码但正向验证能过原因最常见的是操作顺序搞反了。你以为程序是(x b) ^ k实际是(x ^ k) b。两者在b不为0时结果完全不同。另一个可能是模256的位置不对正向时先加后模还是先模后加反推时要对应。解决回到汇编逐条确认指令顺序。xor在前还是add在前and在add之后还是之前。如果and在add之前说明加法结果可能超过255但比对时只取低字节反推时仍然要模256。用一个小例子手工验证输入0x41密钥0x12偏移0x1A正向算一遍再反推算一遍看是否一致。4.2 现象部分字节正确部分字节偏差1原因加法进位或减法借位处理不当。当y[i] - offset为负数时如果没有模256Python会得到负数异或后结果错误。另一种情况是偏移量在不同字节间有进位传播比如整体加法而不是逐字节加法。解决统一用(y[i] - offset) 0xFF。如果是整体加法需要先确定字节序大端还是小端再按多字节整数做减法最后拆回字节。判断方法看汇编里add操作的是8位寄存器还是32/64位寄存器。8位寄存器就是逐字节32/64位就是整体。4.3 现象密钥长度判断错误导致循环错位原因密钥长度不是2的幂时编译器用div实现取模反汇编里可能看到div指令或乘法逆元优化。如果误判密钥长度索引就会错位从某个字节开始全部错误。解决观察循环里索引寄存器的变化规律。如果是i % 4会看到and ecx, 3如果是i % 3会看到乘法加移位序列比如mov edx, 0xAAAAAAAB; mul edx; shr edx, 1。根据这些特征反推密钥长度。另一个方法是看数据段里密钥数组的实际大小通常紧挨着其他常量。4.4 现象目标数组提取错误比对值对不上原因目标数组可能在.rodata段也可能在栈上动态生成。如果是从栈上读取需要跟踪栈帧布局确认偏移是否正确。有些程序会对目标数组做二次变换比如先解密再比对。解决在调试器里下断点直接看比对时的内存值。如果目标数组是动态生成的在比对指令前下断dump对应内存区域。不要直接从静态反汇编里抄立即数因为可能有重定位或加密。4.5 现象脚本跑通了但输入不是可打印字符原因反推结果正确但原始输入本身就不是ASCII。很多校验的输入是二进制序列号或哈希值不一定是可读字符串。如果题目要求输入可打印那说明反推公式还有遗漏比如多了一层base64或十六进制编码。解决先确认反推结果的熵。如果字节分布均匀且都在0到255之间大概率是正确的二进制数据。如果题目期望可打印检查是否有额外的编码层。常见的是先做hex解码再做xoradd反推时要先xoradd再hex编码。5. 进阶技巧用约束求解器处理复杂混合运算当运算链超过两层比如xor - add - xor - add手工推导容易出错。这时候可以用z3约束求解器把每个字节的运算建成符号表达式让求解器自动反推。from z3 import * def solve_xor_add(target_bytes, key1, offset1, key2, offset2): 求解 ( (x ^ key1) offset1 ) ^ key2 offset2 target solutions [] for i, y in enumerate(target_bytes): x BitVec(fx_{i}, 8) m1 x ^ BitVecVal(key1[i % len(key1)], 8) m2 m1 BitVecVal(offset1, 8) m3 m2 ^ BitVecVal(key2[i % len(key2)], 8) m4 m3 BitVecVal(offset2, 8) s Solver() s.add(m4 BitVecVal(y, 8)) if s.check() sat: model s.model() solutions.append(model[x].as_long()) else: return None return bytes(solutions) # 测试两层混合运算 key_a b\x11\x22 key_b b\x33\x44 target3 bytes([(((0x41 ^ key_a[i % 2]) 0x05) ^ key_b[i % 2]) 0x0A for i in range(2)]) result3 solve_xor_add(target3, key_a, 0x05, key_b, 0x0A) print(fz3求解结果: {result3})z3的优势在于不用手工推导公式只要把正向逻辑用BitVec表达出来求解器会自动处理进位和模运算。缺点是速度比直接反推慢对于长输入超过几百字节可能超时。我一般用z3验证手工推导的结果或者处理运算层数超过3层的场景。另一个技巧是分段处理。如果输入长度很大但每8字节一组独立校验可以每组单独求解并行化加速。实际固件校验里常见的是每16字节一个块块间无关联这种结构非常适合分段反推。注意z3的BitVec默认是8位加法会自动模256不需要额外 0xFF。但如果你用Int类型需要手动加模约束否则求解器可能给出超出0到255的解。最后一个习惯每次反推完一定用正向函数再算一遍确认forward(reverse(target)) target。这个后悔药能帮你抓住绝大多数符号和模运算错误。希望帮到你。本文还有配套的精品资源点击获取

看完文章,想为自己的企业也做一次专业网站诊断?

尧图顾问免费为您评估现有网站,并给出建站/改版建议与报价方案。

免费获取方案