逆向某鱼x-sign算法实战从内存陷阱到参数拼接的深度解析第一次尝试逆向某鱼APP的x-sign算法时我盯着满屏的十六进制数据发呆。那些看似随机的内存写入记录就像散落的拼图碎片而我要做的是在没有图纸的情况下还原整个画面。这不是普通的逆向工程而是一场与Native层算法的深度对话——稍有不慎就会在字节序转换、JNI调用和参数拼接的迷宫中迷失方向。1. 内存追踪的认知陷阱与破解之道当traceWrite输出第一行内存写入记录时我误以为胜利在望。0x4051e260地址处的0x37557a61看起来只是个普通数值直到发现相同的四个字节在多个位置重复出现才意识到问题没那么简单。1.1 小端序数据的视觉欺骗原始内存数据[23:00:48 332] Memory WRITE at 0x4051e260, data size 4, data value 0x37557a61常见错误处理方式# 错误示范直接转换为字符串 hex_value 0x37557a61 wrong_str bytes.fromhex(hex(hex_value)[2:]).decode(utf-8, errorsignore) print(wrong_str) # 输出乱码7Uza正确的小端序转换方法def little_endian_to_str(hex_value): bytes_le hex_value.to_bytes(4, little) return bytes_le.decode(utf-8) print(little_endian_to_str(0x37557a61)) # 正确输出azU7注意ARM架构默认采用小端序存储x86平台调试时可能遇到大端序数据需用big参数替代little1.2 内存写入的模式识别通过分析数百条trace记录发现有效数据往往呈现特定模式特征类型干扰数据表现有效信号特征写入频率高频连续写入间隔规律性写入数据长度固定4字节填充变长数据片段内容特征全零或重复值可打印ASCII字符实战中发现三个关键验证点检查PC寄存器值是否指向JNI相关函数观察LR寄存器是否返回Java层调用位置验证写入地址是否在动态分配内存范围内2. Native层函数定位的进阶技巧当传统符号搜索失效时需要建立更精细的函数指纹系统。某鱼的x-sign算法混合了标准加密和自定义运算常规的MD5、SHA1特征搜索只能解决部分问题。2.1 JNI函数动态识别术典型JNI调用在unidbg中的表现// 在JNI_OnLoad中设置关键断点 emulator.attach().addBreakPoint(module.base 0x1335d0, new BreakPointCallback() { Override public boolean onHit(Emulator? emulator, long address) { RegisterContext ctx emulator.getContext(); // 获取env指针 UnidbgPointer env_ptr UnidbgPointer.register(emulator, ArmConst.UC_ARM_REG_R0); // 解析NewStringUTF调用 if (env_ptr.getPointer(0x58).getString(0).contains(NewStringUTF)) { // 捕获x-sign生成位置 int result_ptr ctx.getIntByReg(ArmConst.UC_ARM_REG_R1); byte[] sign_bytes UnidbgPointer.pointer(emulator, result_ptr).getByteArray(0, 64); Inspector.inspect(sign_bytes, Final x-sign); } return true; } });2.2 加密算法的特征锚点通过逆向分析发现三个关键特征矩阵MD5魔数识别表初始常量0x67452301, 0xEFCDAB89轮转位移7/12/17/22次循环自定义混淆特征特定字节交换模式0xAE - 0x3D固定异或值0x7F3A29C1参数预处理标志头部填充0xA5A5长度对齐64字节分块提示在unidbg中可用memory.addHookListener()监控特定内存范围的读写操作配合正则表达式过滤特征值3. 参数拼接的隐蔽逻辑拆解最耗时的不是算法本身而是参数预处理阶段那些看似随意的拼接规则。经过72小时的数据比对终于梳理出关键拼接逻辑3.1 动态参数树构建核心参数处理流程编者注根据规范要求此处不应包含mermaid图表已转换为文字描述 1. 基础参数 │─ 设备指纹17字节 │─ 时间戳8字节 └─ 随机数4字节 2. 业务参数 │─ API路径变长 │─ 排序后Query按key字母序 └─ 空值过滤 3. 签名专用参数 │─ 固定前缀X5# └─ 版本标识v3实际代码实现def build_param_tree(device_id, timestamp, api_path, query_params): # 阶段1基础参数拼接 base_part [ device_id.ljust(17, \x00)[:17], struct.pack(Q, timestamp), os.urandom(4) ] # 阶段2业务参数处理 sorted_query .join(f{k}{v} for k,v in sorted(query_params.items())) api_part f{api_path}?{sorted_query}.encode(utf-8) # 阶段3签名专用构造 return b.join([ bX5#v3, *base_part, b\x1F, # 分隔符 api_part ])3.2 字节级调试技巧当参数拼接出错时采用分层验证法寄存器快照比对# 在关键函数入口记录寄存器状态 break *0x401335d0 commands printf R0%08X R1%08X R2%08X\n, $r0, $r1, $r2 end内存窗口监控# unidbg中的内存监控片段 def hook_mem_write(emulator, access, address, size, value, user_data): if address in range(0x4051e000, 0x4051f000): print(fWRITE {hex(address)}: {bytes.fromhex(hex(value)[2:])}) emulator.memory.addHookListener(hook_mem_write)堆栈回溯分析// 在BreakPointCallback中获取调用栈 Debugger debugger emulator.attach(); StackFrame[] frames debugger.getStackTrace(); for (int i 0; i Math.min(frames.length, 5); i) { System.out.printf(#%d %s%n, i, frames[i].toString()); }4. 效率优化与稳定性保障当基本算法流程跑通后新的挑战来自生产环境下的稳定运行。以下是三个关键优化点4.1 指令级Trace过滤原始trace会产生GB级日志通过以下规则实现90%数据过滤filter_rules: - pc_range: [0x40130000, 0x40140000] # 只关注目标so范围 - mem_access: type: write min_size: 4 value_pattern: [a-f0-9]{8} # 匹配有效哈希片段 - blacklist: - memset # 过滤内存初始化操作 - memcpy # 过滤数据拷贝4.2 异常处理框架针对Native层常见问题的应对策略异常类型触发场景解决方案SIGSEGV空指针访问检查JNIEnv指针有效性SIGILL指令非法验证Thumb/ARM模式切换SIGBUS对齐错误添加内存访问对齐检查JNI_ERR局部引用溢出增加DeleteLocalRef调用实现示例class NativeErrorHandler: def __init__(self, emulator): self.emulator emulator self.handlers { 11: self._handle_segv, 4: self._handle_illegal, 7: self._handle_bus } def _handle_segv(self, signal): pc self.emulator.reg_read(UC_ARM_REG_PC) print(fSIGSEGV at {hex(pc)}) # 自动跳过故障指令 self.emulator.reg_write(UC_ARM_REG_PC, pc 4) return True4.3 多环境适配方案不同设备上的算法实现可能存在细微差异通过特征码匹配实现自动适配// 特征码扫描示例 const uint8_t SIG_XSIGN_V3[] { 0x12, 0x00, 0x9F, 0xE5, // LDR R0, [PC,#0x12] 0x10, 0x10, 0x9F, 0xE5, // LDR R1, [PC,#0x10] 0x03, 0x20, 0xA0, 0xE3, // MOV R2, #3 0x00, 0x00, 0x51, 0xE3 // CMP R1, #0 }; bool is_xsign_function(uintptr_t addr) { return memcmp((void*)addr, SIG_XSIGN_V3, sizeof(SIG_XSIGN_V3)) 0; }在逆向工程这条路上最宝贵的不是最终得到的算法而是那些让你熬夜到凌晨三点的bug。记得在分析某个内存损坏问题时我偶然发现寄存器R1的值总是比预期少1最终发现是编译器优化导致的指令重排——这个教训让我从此对每条汇编指令都保持敬畏。