论坛首页 逆向工程技术区 阅读主题

[原创]某盾设备指纹算法分析

89 浏览 9 回复
#1 楼主 2026-06-01 21:09:13
我们开启抓包进入APP,发现第一个请求便是指纹数据。数据包如下:查看 Content-Type 发现使用的二进制格式发送的数据包,因此我们排除这是 Base64 编码的数据包。现在我们需要知道,数据包是从哪里发出的。我们 HOOK 一下 RegisterNatives,看看发包前加载了哪些 SO 以及注册了哪些函数。观察得知在发包前 SO 注册了上图这个函数,我们 HOOK 一下这个函数看看调用情况。这里看到一共调用了三百多次,这是典型的传入不同编号走不同处理分支的分发函数,各大厂的算法与设备信息收集都是这样设计的。因此,我们可以大胆下判断,发包的请求就藏在这三百多个调用中。那么我们怎么确定是哪个编号发出的请求呢?我们可以 HOOK 一下 memcpy 函数,因为在 Native 层要进行数据传递通过 memcpy 来复制数据十分便捷高效,而数据包就极有可能在 memcpy 中被复制过。这里插播一个知识点:为什么各大厂商都知道 memcpy 可能是是暴露数据的库函数,为什么还要调用它,而不自行实现?因为 libc 中的 memcpy 使用汇编实现,同时针对不同的 cpu 架构会使用特定的高级指令来加速数据的拷贝。我们可以查看 AOSP 中的 common/arch/arm64/lib/memcpy.S 文件源码来验证:可以看到这里判断了是否支持 MOPS ,如果支持则使用特定指令完成拷贝。而且部分编译器在编译期间会将 memcpy 替换为 memcpy_chk 函数,memcpy_chk 是增加了 src 缓冲区长度校验的版本,防止缓冲区溢出从而导致程序崩溃。这也相当于为程序的健壮性做了兜底保证。当然,如果厂商想硬实现 memcpy 也是可以的。但是这仍不能避免被我们分析,因为最终 APP 要运行在我们的设备上,而设备是我们能完全控制的。因此,在性能与实现复杂度的双重考量下,大部分厂商仍选择使用库函数的 memcpy。下面,我们回到正题。我们 HOOK memcpy 的同时将函数的调用编号也打印出来,观察目标数据包前后的函数编号,以此来确定是在哪个编号中发出的数据包。从上图中我们可以看出在目标数据包前后,有两个编号:83、12。我们怎么判定是在哪个编号中发出的请求?可以分别搜索一下这两个编号,看看哪个编号是第一次被传入的,如果有编号是第一次被传入的那么大概率便是这个编号发出的数据包。经过搜索发现编号 83 出现了两次,而 12 只出现了一次,那么我们可以初步判定编号 12 为发包编号。那么我们怎么判定这就是最终的发包编号?我们可以在编号 12 被传入时,将进程暂停,如果暂停了仍然发包出去了,那么证明 12 不是最终的发包编号,反之则实锤这是发包编号。下面我们验证:在编号 12 被传入时我们暂停进程 5 秒,观察抓包发现并没有请求发出,那么至此我们可以确定数据包就是在编号为 12 的函数调用中发出。知道了发包的位置,接下来我们看看包数据是怎么组装加密的。在我们HOOK RegisterNatives 时得到了分发函数的偏移 0x82f14。现在我们进入SO中看看这个函数。这个函数只是做了些初始化引导工作,我稍微跟了下初始化函数发现是有 OLLVM 混淆与 VMP 防护的。这种情况下如果我们不深究其虚拟机实现的话,可以使用 trace 方案拿到执行指令流展开算法的分析。这里我用自己写的 trace 跟踪了编号 12 函数调用时的指令流,最终得到一个 8.3G,8000W 行的指令流文件。现在我们已知加密密文如下:既然我们得到了完整的指令流文件,那么密文数据一定会在某条指令中出现。因此,我们可以在文件中搜索对应的字节来找到密文生成的位置。我以四个字节为一组尝试在文件中搜索,直至搜索到 33-36 字节时,也就是 0751AC3F,才第一次在文件中搜索到对应数据。我们可以看到,这组数据第一次生成的位置在 0xa59c8 ,因此我们在IDA中跳转到目标地址查看伪代码看其是如何生成的。跳转到 0xa59c8 后我们看到这个函数,函数中有大量的查表与异或操作,因此我们可以判断这是一个加密函数。既然是加密函数,那么我们就要识别这是个什么加密算法。我们可以看看这四个表的数据分别是什么,以此来判断这是什么加密算法。我们可以看到表1与表2分别有大量的 63 7C 77 99 等字节出现,这些字节一般会出现在 AES 加密算法中的 S 盒中,因此我们初步判断这是个类 AES 算法。我们再观察下加密函数发现,a1 一进来便与 a3 进行了异或。而我们通过伪代码可知 a1 是个长度为 16 的字节数组,a3 是个长度为 4 的字节数组。将 a1 中的数据以四个字节为一组,通过 << 位操作组成一个 32 位的数据以对应 a3 的数据格式,随后与 a3 进行异或。我们再观察下函数的结尾部分,对 a2 中的每个元素进行赋值,赋值长度为 16,并且每个赋值语句的最后一步都是为 v19 进行异或,而 v19 是由 a3 赋值而来。因此我们得出如下结论:将输入数据 a1 经过加密得到输出数据 a2,每一轮循环中都有与 a3 异或的步骤。每轮异或的字节长度为 16,并且输入输出数据长度也均为 16 。我们回忆下 AES 128 算法实现:1byte = 8bit 16byte * 8 = 128bit输入明文 128 bit,输出密文 128 bit。加密开始时输入明文与扩展密钥进行异或,随后进行 10 轮加密,最后一轮的最后一步是与扩展密钥进行异或得到最终密文。现在我们将 AES 128 与我们得到的结论进行对比发现三个相似点:并且结合表数据判断,我们可以确定这是个魔改 AES 128 查表实现。查表实现是标准实现的性能优化版,节省列混淆的计算过程达到提升性能的目的。并且在逆向中不易被还原。现在我们确定了加密算法为 AES 128,那么我们就可以根据 AES 的实现步骤来找到密钥。现在我们已知的信息有:我们知道 aes 最后一步加密为 state ^ key = ciphertext。因此,我们可以在指令流中找到最后一步密文生成的位置,看看是哪些数据与 state 异或后得到了密文,与 state 异或的便是密钥。 我们可以看到这行指令便是密文最终生成的位置。我们将其还原成伪 C 代码:state = state ^ key 也就是说左边的 w18 是 state,右边的 w2 为密钥。至此我们就得到了最后一轮的第一组密钥:0xbacdd4b4。那么我们怎么得到最后一轮的全部密钥呢?我们可以以四字节为一组依次向后搜索密文生成的位置,将所有的被异或寄存器拼接起来便是最后一轮的密钥。搜索后得到如下四条指令:将被异或寄存器的值拼接起来,我们得到了最后一轮的密钥:ba cd d4 b4 ad 65 51 1d 3c d4 b0 f6 43 26 b3 a4现在我们验证一下我们的密钥是不是正确的。根据 AES 的密钥扩展算法可知,我们只需要得到任意一轮中的密钥,就能逆推出初始密钥。这里我们使用 Stack 提供的实现来逆推出初始密钥。我们看到 Stack 逆推出了每一轮的密钥,现在我们以四个字节为一组在指令流中搜索密钥。如果能搜到则

...(已截断)

---
来源: 看雪论坛
原文链接: https://bbs.kanxue.com/thread-288695.htm
#2 2026-06-01 21:09:13
学习学习
#3 2026-06-01 21:09:13
学习学习
#4 2026-06-01 21:09:13
大佬 牛逼
#5 2026-06-01 21:09:13
666 tql
#6 2026-06-01 21:09:13
过来看看
#7 2026-06-01 21:09:13
blackbox如何还原哈~
#8 2026-06-01 21:09:13
blackbox如何还原哈~
#9 2026-06-01 21:09:13
学习中
#10 2026-06-01 21:09:13
测试

请登录后参与讨论

立即登录 注册账号