首页
社区
课程
招聘
[原创] X-Perseus AI初窥
发表于: 4天前 2387

[原创] X-Perseus AI初窥

4天前
2387

这个文档用于按时间顺序记录 X-Perseus 的逆向过程。

当前分析不是直接对原始 trace 文本逐行做,而是先把执行日志转换进 DuckDB,再围绕统一的 step 号做联查。

这里最重要的三张表是:

三张表之间的关系非常简单:

所以后文里反复出现的分析方式,本质上都是:

读这份文档时,可以把它理解成下面这个映射:

后文如果提到:

还有两个查询习惯需要先说明:

这一节只记录第一次把 X-Perseus 从最终 7 神输出里准确摘出来,并建立首版拷贝链的过程。此时的目标还不是解释算法,而是先把“字符串在哪、从哪搬过来”这两个问题钉死。

先在最终输出缓冲区里确认 X-Perseus 参数的准确位置,再建立最早一版“它从哪里拷贝过来”的上游链路。

最开始不是从单独的 X-Perseus 字符串入手,而是先重建最终整块参数输出缓冲区:

当时直接确认到的基础信息是:

当时重建结果里最关键的 hexdump 片段是:

重建 0x40642000 后,先看到的不是单独一个参数,而是一整块已经拼接好的最终输出,其中包含:

在这块总输出里,X-Perseus 的定位结果是:

当时摘出的 X-Perseus 区间 hexdump 开头是:

并且当时已经直接得到完整参数值:

在完成最终输出定位后,最早的直觉是:

基于这个偏移,最早建立的链路猜测是:

随后沿着 0x40642000 逆着 trace 看调用参数和内存流向,做了三类验证。

第一类是整块最终拷贝:

第二类是子区间偏移验证:

第三类是前缀源块验证:

同时又用 mem-trace 做了交叉验证:

当时按 trace 得到的最早版拷贝链证据是:

这里 0x206 很关键,因为:

也就是说,最开始就是用这个偏移把 X-Perseus 在最终输出里的位置,和 0x408be206 这段子区间连起来的。

到这一步,最早稳定下来的定位结果有两部分。

第一部分是参数定位本身:

第二部分是最初版本的上游链路:

这一步虽然还没进入算法细节,但已经完成了两个关键工作:

这一步后面有一个重要修正。

最初我们一度把 0x40625800 看得太“靠前”了,像是在把它当作 X-Perseus 的主要上游结果区。但后续用户补充并确认:

这带来两个修正:

这个修正当时依赖的关键事实是:

也正是因为这一段外部函数体没有被当前 trace 展开,所以当时出现了一个重要方法论修正:

在把 0x40625800 修正为“中间缓冲区”之后,Step 1 实际上还多完成了一件事:确认了真正的成品字符串区0x40624c00

这仍然属于“参数定位”而不是“算法拆解”,因为这里解决的问题依然是:

当时支撑这个判断的直接证据有三组。

第一组证据是重建结果本身:

在这个 step 重建 0x40624c00 后,可以直接得到完整 X-Perseus 字符串,而不是某种二进制中间态。这一点和 0x40624400 那种主体输入块的形态完全不同。

第二组证据是调用参数:

这说明在后续外部函数调用前,0x40624c00 已经作为源地址放进 x1,长度 0x2e4 也与成品字符串长度一致。换句话说,这一步看到的不是“正在生成字符串”,而是“已经拿着成品字符串去做下一步处理”。

第三组证据是生成方式:

这带来一个当时很重要的定位结论:

也就是说,Step 1 最终不只是把 X-Perseus 在最终大缓冲区中的位置找出来,还把“最早可确认的成品字符串落点”一起钉死了。

Step 1 本身不是算法分析,而是“定位 + 切片 + 首版链路建立”。因此这里给出的 Python 更偏向于把最终输出里 X-Perseus 摘出来,并把首版拷贝链表达成结构化数据:

Step 1 最终固定下来的内容有四点:

参数定位已经明确:

当时能直接从 trace 看见的,是一条后段搬运链

以及与前缀区相关的:

真正的成品字符串区也已经定位出来:

后续修正也在这一步埋下了边界:

在已经定位到成品字符串区 0x40624c00 之后,下一步要判断这串 740 字节文本到底是什么编码产物。这里的目标不是继续追更早输入,而是先回答两个问题:

Step 2 的起点不再是最终输出缓冲区,而是已经确认的成品字符串区 0x40624c00

当时最直接的观察对象就是它的开头:

第一眼看上去,它的字符集非常像 Base64:

但它又明显不是标准 Base64 输出,因为标准 Base64 里更常见的开头不会是这种 LpLLLF... 形态,而且同样输入如果按标准表编码,开头字符并不对应这些结果。

因此当时的起点判断是:

围绕 0x40624c00 的逐字节写入继续看 trace 后,很快出现了几组关键现象。

第一组现象是写出方式:

第二组现象是字符来源:

把开头四个字符抽出来看,证据链是:

这说明至少开头四个字符已经满足:

第三组现象是索引模式本身很像标准 Base64。

当时拿 step 13305695 附近的输入字节去对照,看到 0x40639280 开头是:

其中前 3 个字节 01 00 00 如果按标准 Base64 分组拆成 6-bit 索引,得到的正好是:

而这 4 个索引映射出来的字符正好就是:

也就是开头的 LpLL

到这一步,当时的猜测基本收敛成了两条:

换句话说,当时最可能的模型已经变成:

如果这个猜测成立,那么继续向前应该还能看到:

随后做了三层验证。

第一层验证是开头分组逐组对齐。

0x40639280 开头几组输入按 3 字节切开后,前 5 组结果分别是:

这一步的意义很直接:

第二层验证是自定义字符表本身。

后续把当前样本中用到的字符表整理出来,得到:

这张表和标准 Base64 字符表:

的关系很明确:

第三层验证是完整输入与完整输出的闭合。

当时已经把输入缓冲区定位到:

而成品字符串区是:

随后对 0x40639280..0x406394a9 整段输入做完整编码校验,结果是:

到这一步,Step 2 实际上完成了三件关键事。

第一,最后一层编码算法已经识别出来:

第二,输入缓冲区已经定位出来:

第三,继续向前追的入口也明确了:

也就是说,Step 2 的结束点是:

Step 2 对应的 Python 可以直接写成一个最小可运行的“魔改 Base64”编码器。它保留标准 Base64 的 3-byte -> 4-index 分组,只替换索引到字符的映射表:

这一步里也有一个很重要的修正。

最初如果只看字符串长相,很容易把它理解成:

但 Step 2 做完之后,这两种模糊说法都需要收敛成更精确的表述:

它不是“完全不同于 Base64 的编码”。

对后续逆向来说,真正有价值的入口不是“继续找标准 Base64 函数”。

Step 2 最终固定下来的内容有四点:

Step 2 已经确认:

因此 Step 3 的目标就变成了:

这一节的起点是:既然 0x40624c00 的开头 LpLL 已经能对上 0x40639280 的前 3 个字节,那么下一步就不再只看开头几组,而是重建整段输入缓冲区,看看它的完整形态。

当时最先固定下来的边界是:

也就是说,从这一步开始,0x40639280..0x406394a9 被整体视为:

把整段输入缓冲区重建出来之后,最明显的观察不是“它像一段随机连续二进制”,而是它内部其实分成了几段角色不同的区域。

先看开头 32 字节:

这段数据很快暴露出一个结构特征:

当时按偏移整理出来的结构是:

这里还有一个非常关键的观察:

也就是说,这时已经能感觉到:

为了让这一节可直接阅读,完整输入缓冲区的 hexdump 也贴在这里:

基于这份结构,当时的猜测开始收敛成下面这个模型:

如果这个模型成立,那么继续看 trace 时应该能看到两类不同来源:

也就是说,这一步的关键不再是“它是不是 Base64 输入”,而是“这块输入是不是由多个来源拼出来的”。

随后做了三类验证。

第一类验证是完整编码闭合。

0x40639280..0x406394a9 整段作为输入,用 Step 2 已确认的“标准 Base64 分组 + 自定义字符表”去编码,结果与 0x40624c00 中的 740 字节成品字符串完全一致

这一步的意义是:

第二类验证是来源分层。

当时把输入区按来源整理后,得到的分层结果是:

其中最关键的是第三段,因为它对应了后续真正的算法主体。

这里还有一条很适合直接写进文档的来源细节:

第三类验证是主体搬运调用本身。

step 13300144,可以直接看到一条把主体块搬进输入缓冲区的调用:

这条调用的含义非常直接:

这就把两层关系第一次明确连起来了:

Step 3 对应的 Python 更适合写成“输入缓冲区组装器”。因为这一节的核心发现就是:完整输入并不是单源,而是由头部、中间字段和主体三段拼起来的。

到这一步,Step 3 固定下来的结论主要有三条。

第一,完整 Base64 输入缓冲区已经明确:

第二,这块输入不是单源数据,而是分层拼出来的:

第三,主体输入块的上游入口已经明确切到:

因为从 0x40639296 往前追,第一条最稳定、最明确的连接就是 step 13300144 这次整块搬运。

这一步里最重要的修正是:不能把 0x40639280 整段都当成“同一种来源”的数据。

如果只看它最终被整体编码的样子,很容易误以为:

但 Step 3 做完之后,这种理解必须拆开:

0x40639280..0x40639295 并不是主体算法本身。

真正需要继续深挖的,是从 0x40639296 开始的 0x214 字节主体。

Step 3 最终固定下来的内容有四点:

它不是单一来源,而是由三段角色不同的数据组成:

主体输入块的稳定上游入口已经明确:

从 Step 3 开始,真正需要解释的对象已经收缩到:

这段 0x214 字节就是后面搬到 0x40639296 的主体输入块。问题在于,继续往前追以后会发现:

所以 Step 4 的任务不再是“继续平铺直叙地沿时间往前追”,而是要把这块主体区按层次拆开,回答下面几个问题:

这一步的起点仍然是 Step 3 已经确认的稳定入口:

也就是说,只要解释清楚 0x40624400..0x406246130x214 字节,最后一层 Base64 输入主体就解释清楚了。

在这一阶段,文档里开始统一给这块区域起一个名字:

这里的含义不是“它在 trace 里天然就叫这个名字”,而是为了后续讨论方便,把“Base64 主体输入对应的二进制原像缓冲区”统一记作 BODY_PREOBF

不过这里要先把一个容易混淆的点说清楚:

后面 Step 4 默认主要使用的是 分析别名 BODY_PREOBF,因为这样读者在同一份 hexdump 上就能同时看懂:

到这里之后,如果还按单线时间写,会很快失去可读性。原因有三个:

同一地址区间上叠了多层状态。

有些层是真实落地缓冲区,有些层只是为了分析引入的视图。

这部分已经天然分成了几层稳定对象:

因此从 Step 4 开始,虽然总体仍然属于“逆向过程记录”,但这一步内部改按层次组织。

先固定最后真正参与 Base64 编码的主体层。

这一层的基本事实是:

这层最重要的意义不是“已经知道它怎么来的”,而是:

这一层一开始最明显的特征有两个:

前两组 64 位值很特殊。

整段并不表现得像静态常量。

这说明:

为了让后面的分层讨论有一个可以直接对照的内存基线,这里先把“运行时最终层”和“分析别名层”都说明清楚。

运行时最终层 BODY_PREOBF_FINAL 的开头 16 字节,实际是:

而为了分析方便,下面这份默认对照 hexdump 使用的是分析别名 BODY_PREOBF

继续观察整段 0x40624400..0x40624613 的写入历史后,首先分离出来的是“早期整段主体”和“后续块级修补”这两层。

关键发现是:

这意味着一个很重要的修正:

于是这一层被拆成:

而这些后续修补最稳定的特征是:

这又把 19 个块继续拆成了两类:

19 个块里,最早被单独闭合的是前两组 64 位值,也就是:

这两组之所以先被单独拿出来,是因为它们的行为明显不同于后面主体:

最终确认下来的事实是:

这里最重要的不是把每个 helper 细节都铺开,而是先固定它的层角色:

这一层解释了为什么:

为了让这一层更直观,先把“位混淆前原始 16 字节”直接贴出来:

对应的小端 64 位值就是:

而真正经过两次特殊位混淆之后,进入 Base64 主体输入的前两组值会变成:

这一层对应的 Python 可以直接写成两组特殊块的位补丁函数:

把前两组特殊位混淆放到一边后,接下来最重要的一层,是整段字节级主体在更早阶段的演化。

这里最终分离出了两个关键中间层:

这两层的关系之所以重要,是因为后来已经能把它们之间的整层变换写成统一算法。

当前可稳定确认的结论是:

也就是说,这层已经可以表达为:

这一步的价值在于:

为了先让读者看到这条链里更靠后的那一层,下面先贴 17 的完整 hexdump。它非常重要,因为后面的首字节最终补丁就是在这层基础上完成的,而 BODY_PREOBF_FINAL 也几乎可以视为“17 再叠一层首字节最终补丁”:

而再往前一层、作为统一递推输入的 17.1,其尾部旧状态是:

也就是:

这组对照很关键,因为它直接对应了后面尾部统一递推和最后一个字节 xor 收尾的分析。

17 这一层里,真正最特殊的边界是首字节和最后一个字节。

尾字节 0xad 的规律先被单独闭合:

也就是:

而首字节则更特别,因为它不是直接来自 17.1 -> 17 这一层,而是更晚又叠了一次最终补丁

当前稳定结论是:

这就解释了为什么最终 BODY_PREOBF_FINAL[0] = 0x93,而更早层里却会看到 0xb90xa0

如果只看边界区,最值得直接对照的是这几个窗口:

17 尾部:

更早一层 17.1 的对应旧尾部:

首字节最终补丁后,真正进入最终主体层的开头:

这也是为什么 Step 4 必须把“整段递推层”和“最终首字节补丁层”拆开写。

这一层对应的 Python 可以直接分成三部分:

Step 4 最终把 0x40624400 这一大块复杂主体整理成了一个更清晰的主体层框架:

这里并不是说每一层都通过同一种算法相连,而是说:

这一步里最大的修正,是放弃把 0x40624400 当成“单层缓冲区”来理解。

最初如果不分层,很容易把所有现象混成一团:

但 Step 4 之后,这些现象被重新归位了:

也正因为做了这个修正,后面才有可能继续稳定往上游写。

Step 4 最终固定下来的内容有四点:

Step 4 已经把主体区内部的大框架拆出来了,但还剩下一个更上游的问题:

也就是说,Step 5 的目标是把下面这条更上游链单独讲清楚:

继续往前追时,最先遇到的问题是:

于是当时先做了一个很小、但很关键的分析修正:

也就是说:

这一步的意义不是“伪造一层不存在的数据”,而是把首字节上的后补丁噪音先剥掉,好继续观察更上游是否存在统一模板。

定义出 APPLE 之后,最先看到的现象是:它并不是只在个别字节上有规律,而是很快出现了一套可以连续扩展的统一模式。

APPLE 的完整 hexdump 如下:

从直观上看,这一层和 17.1 的区别其实很小:

但从 trace 回溯上看,APPLE[1..] 却开始落入一套比 17.1 -> 17 更统一的公式里。

到这一步,当时的猜测收敛成:

随后做了三轮验证。

第一轮是小样本扩展。

最开始先验证 APPLE[1..15],很快得到统一关系:

这里最关键的不是每个中间变量名字,而是:

第二轮是范围扩展。

APPLE[1..15] 成立之后,继续扩到 APPLE[1..31],结果仍然全部命中。再继续程序化验证,最终得到:

也就是说,到这里已经不是“样本看起来像有规律”,而是:

第三轮是 root 源窗口定位。

APPLE[1..0x212] 整段公式跑通之后,更上游 root 源窗口也被固定下来了:

它的映射关系是:

也就是说:

为了让这一步能被直接阅读,ROOT530 的完整 hexdump 也放在这里:

为了把之前主文档里没展开的 protobuf 前缀也明确补出来,这里再把 ROOT530 按可读性拆成两段:

这一段也解释了为什么前面会说“ROOT530 前半段像 protobuf、后半段像 JSON”:

这一步的核心观察非常关键:

这说明主体区终于不再只是在“算法层”里打转,而是第一次和真实业务参数连接起来了。

Step 5 对应的 Python,就是把 APPLE 的统一桥接公式和 ROOT530 的逆序取值关系整理出来:

Step 5 最终把更上游这条链固定成了:

其中最关键的结论有两个:

这一步里最重要的修正有两个。

APPLE 不是 trace 中真实存在的一层。

ROOT530 也不是“又一个运行时生成层”。

这两个修正都很重要,因为如果把它们误当成“运行时实际缓冲区层”,后面的推理就会混乱。

Step 5 最终固定下来的内容有四点:

表名 作用 关键字段 说明
instructions 指令主表 step, address, offset, instruction, semantic 每个 step 对应一条执行指令
registers 寄存器快照表 step, reg_name, reg_value, timing 同一 step 下会记录执行前后的寄存器值,timing 分为 before/after
memory 内存访问表 step, mem_addr, mem_value, mem_type, size 记录该 step 触发的读写内存行为
输出地址 字符 关键 step 查表地址 索引
0x40624c00 L 13305555 0x4065e7ad 0x00
0x40624c01 p 13305623 -> 13305697 0x4065e7bd 0x10
0x40624c02 L 13305799 0x4065e7ad 0x00
0x40624c03 L 13305466 0x4065e7ad 0x00
分组 输入字节 标准 Base64 索引 实际输出
0 01 00 00 [0, 16, 0, 0] LpLL
1 00 13 77 [0, 1, 13, 55] LFjK
2 95 46 0d [37, 20, 24, 13] +2Nj
3 25 0e 59 [9, 16, 57, 25] cpJq
4 00 21 a2 [0, 2, 6, 34] LOS1
偏移 地址 内容角色
+0x00 0x40639280 固定头部 8 字节
+0x08 0x40639288 分隔字节 0x0d
+0x09 .. +0x15 0x40639289..0x40639295 固定中间字段 13 字节
+0x16 0x40639296..0x406394a9 主体输入区
输入区间 长度 来源判断
0x40639280..0x40639288 9 字节 固定头部 + 分隔字节
0x40639289..0x40639295 13 字节 固定中间字段
0x40639296..0x406394a9 0x214 字节 主体输入块
名称 含义
BODY_PREOBF_FINAL 严格指 step 13300144 时、真正参与搬运到 0x40639296 的运行时最终主体层
BODY_PREOBF 分析别名;在 BODY_PREOBF_FINAL 基础上,把开头前 16 字节恢复成位混淆前原值,方便读懂前两组特殊处理
层名 含义
BODY_PREOBF_EARLY 更早已经整段成形、以 STRB 为主的主体基线
BODY_PREOBF_FINAL 在早期主体之上继续修补后的最终主体
分组 位混淆前原值 控制字节 位混淆后值
第 1 组 0x16e94327f08af993 0x06 0x06e94323b088d913
第 2 组 0x5d664519ede73657 0x43 0x5d664519ede51657
层名 含义
17.1 更早的字节级主体基线
17 17.1 基础上再做统一递推后的结果
分段 地址范围 长度 说明
ROOT530_PROTO_PREFIX 0x40639001..0x40639148 0x148 = 328 字节 前半段 protobuf/二进制字段区
ROOT530_JSON_TAIL 0x40639149..0x40639212 0x0ca = 202 字节 后半段可直接读出的 JSON 参数串
instructions.step = registers.step = memory.step
一条原始执行日志
  -> 归并成一个全局 step
  -> 该 step 下拆成:
     指令信息(instructions)
     寄存器快照(registers)
     内存访问(memory)
uv run python scripts/query_trace_db.py mem-reconstruct 13501921 0x40642000 0x40642500 --write-limit 0
40642000  58 2d 41 72 67 75 73 0d 0a 4a 61 57 42 61 51 3d  |X-Argus..JaWBaQ=|
40642010  3d 0d 0a 58 2d 47 6f 72 67 6f 6e 0d 0a 38 34 30  |=..X-Gorgon..840|
...
406421f0  65 70 74 75 6e 65 0d 0a 2d 31 31 7c 35 30 3a 35  |eptune..-11|50:5|
40642200  31 3a 35 39 0d 0a 58 2d 50 65 72 73 65 75 73 0d  |1:59..X-Perseus.|
40642210  0a 4c 70 4c 4c 4c 46 6a 4b 2b 32 4e 6a 63 70 4a  |.LpLLLFjK+2NjcpJ|
40642200  31 3a 35 39 0d 0a 58 2d 50 65 72 73 65 75 73 0d  |1:59..X-Perseus.|
40642210  0a 4c 70 4c 4c 4c 46 6a 4b 2b 32 4e 6a 63 70 4a  |.LpLLLFjK+2NjcpJ|
40642220  71 4c 4f 53 31 48 6c 49 6f 72 41 75 44 6f 66 57  |qLOS1HlIorAuDofW|
40642230  71 31 61 4c 2f 70 6f 51 53 49 34 73 2b 74 66 2b  |q1aL/poQSI4s+tf+|
LpLLLFjK+2NjcpJqLOS1HlIorAuDofWq1aL/poQSI4s+tf+gq+8Mbb+vUoDRJ3Fs7W5ZQ3aXT9zqBzHC84DhCMkAJn5iCQuiEZvDPZbtlwHPuOPXZBoqAs1jHOfpEMXZ+oC0tJZu6PBrgVonka6qZk5ZL0rmJiHRMmUKNTdXU4898AC5squ6Vscm4QzITlCH1LVRpLZk4NbK+Vkm615gppA3I0Xy0I3joroLsPFXO2ynGAzdUflnSFWgEv5PJTzXbgnHmlI6C5fi8yrdbPOpWxU4ftBXoH8AVx6yIuRWqW7OBR9qq1K2XKbxM0iDMKd0KafUxooDPFm8EcaLzV0WlVSqqA2JqjTRTL08j0bqHRom54563/64EDxopjfE/48sXCuSrFOBAjYLKF5BaoXgLcqQHrrxWCYafdwlY6GgUKSFe3rnD0aOHxN2RY5qD2ZGAs7Kuy1xbNBiJxrI3wRiKugx+m97izex/zlBwVQjtg5/NPtJWY6t0Tu9XG3/Bq+Liz32oKIv9PUOphulUEjbrfYy9Lhlcs1l6Ik91rFnURmhIMuqBvdZDnRCVknREVppeY44lVIlI2ISk/ldrgstpD/1MvhmbNXBXF3p3d7af79f+yq3FXjVI16C3l+D1/nKeRRjmdBdg8ZXayXk9kcreGqP72fGGNcfmxaAgMCsUZgnGgGGgAlBPGI94BXBKeXEIrHny5nHNgD/y7okXwIyCOqSW3H0xfUdsX8=
step 13501708:
  x0 = 0x40642000
  x1 = 0x408be000
  x2 = 0x4f7

step 13501392:
  x0 = 0x408be000
  x1 = 0x40624400
  x2 = 0x206

step 13501424:
  x0 = 0x408be206
  x1 = 0x40625800
  x2 = 0x2f1
0x40624400 --(0x206 bytes)--> 0x408be000
0x40625800 --(0x2f1 bytes)--> 0x408be206
0x408be000 --(0x4f7 bytes)--> 0x40642000
0x40624c00 -> 0x40625800 / 0x408be206 / 0x40642000
step 13500742:
  x0 = 0x4062540b
  x1 = 0x40624c00
  x2 = 0x2e4
  target = 0x40281600 -> br x17 -> 0x4046c300
uv run python scripts/query_trace_db.py mem-reconstruct 13433848 0x40624c00 0x40624ee5
step 13500742:
  x0 = 0x4062540b
  x1 = 0x40624c00
  x2 = 0x2e4
  target = 0x40281600 -> br x17 -> 0x4046c300
0x40624c00
  = 已经完成字符映射后的成品字符串区

0x40625800 / 0x408be206 / 0x408be000
  = 后续搬运、拼接、封装过程中的中间缓冲区
from dataclasses import dataclass


@dataclass(frozen=True)
class PerseusSlice:
    header_addr: int
    value_addr: int
    value_end: int
    value: bytes


def locate_x_perseus_from_final_output(
    full_output: bytes,
    base_addr: int = 0x40642000,
) -> PerseusSlice:
    marker = b"X-Perseus\r\n"
    header_off = full_output.index(marker)
    value_off = header_off + len(marker)
    value_end_off = full_output.index(b"\r\n", value_off)
    return PerseusSlice(
        header_addr=base_addr + header_off,
        value_addr=base_addr + value_off,
        value_end=base_addr + value_end_off - 1,
        value=full_output[value_off:value_end_off],
    )


def first_copy_chain() -> list[tuple[int, int, int]]:
    """
    返回 Step 1 最早稳定下来的首版搬运链:
      (src, dst, size)
    """
    return [
        (0x40624400, 0x408BE000, 0x206),
        (0x40625800, 0x408BE206, 0x2F1),
        (0x408BE000, 0x40642000, 0x4F7),
    ]
0x40625800 -> 0x408be206 -> 0x40642000
0x40624400 -> 0x408be000
LpLLLFjK+2NjcpJqLOS1HlIorAuDofWq...
输出字符 = 字符表[某个 6-bit 索引]
01 00 00 00 13 77 95 46 0d
[0, 16, 0, 0]
0  -> L
16 -> p
0  -> L
0  -> L
输入字节流
  -> 标准 Base64 的 6-bit 分组
  -> 不走标准字符表
  -> 改走一张自定义 64 字符表
  -> 逐字节写到 0x40624c00
LFOH7gSlYcbaijkWpfR62ICGNqesUBxVMm1/Q+0ZrhwnAz9TD4yv8udK3JXt5EoP
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
不是标准 Base64 文本
而是“标准 Base64 分组 + 自定义 64 字符表”
0x40639280
  --(标准 Base64 分组 + 自定义字符表)-->
0x40624c00
CUSTOM_B64_TABLE = b"LFOH7gSlYcbaijkWpfR62ICGNqesUBxVMm1/Q+0ZrhwnAz9TD4yv8udK3JXt5EoP"


def encode_perseus_custom_base64(data: bytes) -> str:
    out: list[str] = []
    for i in range(0, len(data), 3):
        chunk = data[i:i + 3]
        pad = 3 - len(chunk)
        chunk = chunk + b"\x00" * pad

        word = (chunk[0] << 16) | (chunk[1] << 8) | chunk[2]
        idx = [
            (word >> 18) & 0x3F,
            (word >> 12) & 0x3F,
            (word >> 6) & 0x3F,
            word & 0x3F,
        ]
        chars = [chr(CUSTOM_B64_TABLE[j]) for j in idx]

        for j in range(1, pad + 1):
            chars[-j] = "="

        out.extend(chars)
    return "".join(out)


assert encode_perseus_custom_base64(bytes.fromhex("01 00 00")) == "LpLL"
输入字节流
  -> 标准 Base64 6-bit 分组
  -> 自定义 64 字符表映射
  -> 逐字节写到 0x40624c00
LFOH7gSlYcbaijkWpfR62ICGNqesUBxVMm1/Q+0ZrhwnAz9TD4yv8udK3JXt5EoP
0x40639280
  --(标准 Base64 分组 + 自定义字符表)-->
0x40624c00
X-Perseus 最后一层字符串编码的完整输入字节流
40639280  01 00 00 00 13 77 95 46 0d 25 0e 59 00 21 a2 0c
40639290  75 7e a2 cd 70 f9 13 d9 88 b0 23 43 e9 06 57 16
40639280  01 00 00 00 13 77 95 46 0d 25 0e 59 00 21 a2 0c  |.....w.F.%.Y.!..|
40639290  75 7e a2 cd 70 f9 13 d9 88 b0 23 43 e9 06 57 16  |u~..p.....#C..W.|
406392a0  e5 ed 19 45 66 5d 20 28 a9 73 73 ec 12 e7 80 5b  |...Ef] (.ss....[|
406392b0  10 ff 27 93 82 fa be eb 59 76 d0 d6 d3 1c 29 5a  |..'.....Yv....)Z|
406392c0  03 ac e6 bf 0c 5a 4d 4c f6 7c f0 fe 72 bb 1e a0  |.....ZML.|..r...|
406392d0  ff d4 2f fa 9d df 99 b1 b8 8d 0c 24 50 f6 0e a7  |../........$P...|
406392e0  97 e5 a6 ef 99 f5 4f f7 68 15 ff ab 38 b4 d9 9c  |......O.h...8...|
406392f0  ef 27 02 6a 21 e4 c0 d2 82 17 37 62 fd ba 73 1d  |.'.j!.....7b..s.|
40639300  2e d2 c5 bc 6d 9d 53 7d b2 61 c6 4b 55 bc 75 83  |....m.S}.a.KU.u.|
40639310  88 07 d2 40 09 ce c5 82 b7 95 f3 a1 4e 2f 05 41  |...@........N/.A|
40639320  0b 38 56 6e b2 99 5e 0d fa 8f 80 6f f0 7a 09 4c  |.8Vn..^....o.z.L|
40639330  ab 5e cb 76 71 11 eb 18 13 c5 f7 3f 3f e6 fb 7a  |.^.vq......??..z|
40639340  28 5a c3 84 75 53 5b c4 4c d3 2a 36 2b f0 90 3d  |(Z..uS[.L.*6+..=|
40639350  e7 31 47 b7 7a f8 3d 2c 7d e4 f2 57 54 8f 64 f1  |.1G.z.=,}..WT.d.|
40639360  02 75 2b 99 66 2d d4 eb 72 9e 82 63 30 83 7d a6  |.u+.f-..r..c0.}.|
40639370  dc b4 5c 7b ef b0 fc 18 74 f4 92 c0 b5 f9 8f 1d  |..\\{....t.......|
40639380  f1 99 66 c5 39 64 db d2 bc 09 b4 36 62 99 0d 2f  |..f.9d.....6b../|
40639390  a1 f3 1f 13 e2 34 f1 f7 07 be 40 d4 7d 8f 1d 1b  |.....4....@.}...|
406393a0  e9 6d 46 a0 10 9d b0 d2 00 dc 1f 1d 2f ee 85 00  |.mF........./...|
406393b0  96 64 0e 8a 1e 3d 62 0b 47 6a 87 21 35 c5 73 71  |.d...=b.Gj.!5.sq|
406393c0  81 6b 8a 2b c2 62 c2 0d e6 14 48 8f 19 c1 49 d7  |.k.+.b....H...I.|
406393d0  b1 b1 37 d7 28 9e 29 87 4c e5 ea 15 e2 a4 8c df  |..7.(.).L.......|
406393e0  51 5e 96 1b 84 32 d6 9e 8e d1 dd a9 f9 0d ec 5f  |Q^...2........._|
406393f0  23 63 fe f9 3c 84 fb 9a fd 6e e9 7e 23 75 99 40  |#c..<....n.~#u.@|
40639400  32 de 14 fb 75 73 bb f7 02 42 9d 47 73 d3 4a a1  |2...us...B.Gs.J.|
40639410  12 32 b8 0a 47 25 b8 87 4d 53 ae 8a 80 6b 71 28  |.2..G%..MS...kq(|
40639420  69 56 0d 59 77 3d a7 c2 b4 96 7c ea d2 f5 f4 10  |iV.Yw=....|.....|
40639430  68 8c 71 1d f5 47 55 45 46 3a 31 f6 a0 56 fb 43  |h.q..GUEF:1..V.C|
40639440  08 e2 83 3a 61 29 8e 9d e8 1e 10 e3 61 0b 44 4b  |...:a)......a.DK|
40639450  91 97 26 78 07 a3 5f 56 24 d6 e0 79 70 8a 3a f7  |..&x.._V$..yp.:.|
40639460  69 24 8d 87 67 76 17 49 fa 2f 2e 8e b8 e2 68 69  |i$..gv.I./....hi|
40639470  76 7f 11 44 57 5d 82 51 85 e2 ec 16 05 9b 72 71  |v..DW].Q......rq|
40639480  6b 5c 55 d7 16 c1 dd fd 75 6e c5 de 9d dd ae bd  |k\\U.....un......|
40639490  56 80 eb cb ca c3 60 5c 23 c8 4f 8e ea a5 72 58  |V.....`\\#.O...rX|
406394a0  17 36 6f ad 00 00 00 00 00 00                    |.6o.......|
0x40639280..0x406394a9
  = 固定头部
  + 固定中间字段
  + 一整块更早生成的二进制主体
step 13300144:
  x0 = 0x40639296
  x1 = 0x40624400
  x2 = 0x214
  target = 0x401e3040
0x40624400..0x40624613
  --(整块搬运 0x214 bytes)-->
0x40639296..0x406394a9
PERSEUS_INPUT_HEAD_9 = bytes.fromhex(
    "01 00 00 00 13 77 95 46 0d"
)

PERSEUS_INPUT_MID_13 = bytes.fromhex(
    "25 0e 59 00 21 a2 0c 75 7e a2 cd 70 f9"
)


def build_perseus_base64_input(body_preobf_final: bytes) -> bytes:
    assert len(body_preobf_final) == 0x214
    return PERSEUS_INPUT_HEAD_9 + PERSEUS_INPUT_MID_13 + body_preobf_final


def split_perseus_base64_input(buf: bytes) -> tuple[bytes, bytes, bytes]:
    assert len(buf) == 0x22A
    head = buf[:0x09]
    middle = buf[0x09:0x16]
    body = buf[0x16:]
    return head, middle, body
固定头部/分隔字节
  + 固定中间字段
  + 主体输入块
0x40624400
0x40639280..0x406394a9
0x40624400..0x40624613
  -> 0x40639296..0x406394a9
0x40624400..0x40624613
step 13300144:
  x0 = 0x40639296
  x1 = 0x40624400
  x2 = 0x214
BODY_PREOBF = 0x40624400..0x40624613
40624400  13 d9 88 b0 23 43 e9 06 57 16 e5 ed 19 45 66 5d  |....#C..W....Ef]|
40624400  93 f9 8a f0 27 43 e9 16 57 36 e7 ed 19 45 66 5d  |....'C..W6...Ef]|
40624410  20 28 a9 73 73 ec 12 e7 80 5b 10 ff 27 93 82 fa  | (.ss....[..'...|
40624420  be eb 59 76 d0 d6 d3 1c 29 5a 03 ac e6 bf 0c 5a  |..Yv....)Z.....Z|
40624430  4d 4c f6 7c f0 fe 72 bb 1e a0 ff d4 2f fa 9d df  |ML.|..r...../...|
40624440  99 b1 b8 8d 0c 24 50 f6 0e a7 97 e5 a6 ef 99 f5  |.....$P.........|
40624450  4f f7 68 15 ff ab 38 b4 d9 9c ef 27 02 6a 21 e4  |O.h...8....'.j!.|
40624460  c0 d2 82 17 37 62 fd ba 73 1d 2e d2 c5 bc 6d 9d  |....7b..s.....m.|
40624470  53 7d b2 61 c6 4b 55 bc 75 83 88 07 d2 40 09 ce  |S}.a.KU.u....@..|
40624480  c5 82 b7 95 f3 a1 4e 2f 05 41 0b 38 56 6e b2 99  |......N/.A.8Vn..|
40624490  5e 0d fa 8f 80 6f f0 7a 09 4c ab 5e cb 76 71 11  |^....o.z.L.^.vq.|
406244a0  eb 18 13 c5 f7 3f 3f e6 fb 7a 28 5a c3 84 75 53  |.....??..z(Z..uS|
406244b0  5b c4 4c d3 2a 36 2b f0 90 3d e7 31 47 b7 7a f8  |[.L.*6+..=.1G.z.|
406244c0  3d 2c 7d e4 f2 57 54 8f 64 f1 02 75 2b 99 66 2d  |=,}..WT.d..u+.f-|
406244d0  d4 eb 72 9e 82 63 30 83 7d a6 dc b4 5c 7b ef b0  |..r..c0.}...\{..|
406244e0  fc 18 74 f4 92 c0 b5 f9 8f 1d f1 99 66 c5 39 64  |..t.........f.9d|
406244f0  db d2 bc 09 b4 36 62 99 0d 2f a1 f3 1f 13 e2 34  |.....6b../.....4|
40624500  f1 f7 07 be 40 d4 7d 8f 1d 1b e9 6d 46 a0 10 9d  |....@.}....mF...|
40624510  b0 d2 00 dc 1f 1d 2f ee 85 00 96 64 0e 8a 1e 3d  |....../....d...=|
40624520  62 0b 47 6a 87 21 35 c5 73 71 81 6b 8a 2b c2 62  |b.Gj.!5.sq.k.+.b|
40624530  c2 0d e6 14 48 8f 19 c1 49 d7 b1 b1 37 d7 28 9e  |....H...I...7.(.|
40624540  29 87 4c e5 ea 15 e2 a4 8c df 51 5e 96 1b 84 32  |).L.......Q^...2|
40624550  d6 9e 8e d1 dd a9 f9 0d ec 5f 23 63 fe f9 3c 84  |........._#c..<.|
40624560  fb 9a fd 6e e9 7e 23 75 99 40 32 de 14 fb 75 73  |...n.~#u.@2...us|
40624570  bb f7 02 42 9d 47 73 d3 4a a1 12 32 b8 0a 47 25  |...B.Gs.J..2..G%|
40624580  b8 87 4d 53 ae 8a 80 6b 71 28 69 56 0d 59 77 3d  |..MS...kq(iV.Yw=|
40624590  a7 c2 b4 96 7c ea d2 f5 f4 10 68 8c 71 1d f5 47  |....|.....h.q..G|
406245a0  55 45 46 3a 31 f6 a0 56 fb 43 08 e2 83 3a 61 29  |UEF:1..V.C...:a)|
406245b0  8e 9d e8 1e 10 e3 61 0b 44 4b 91 97 26 78 07 a3  |......a.DK..&x..|
406245c0  5f 56 24 d6 e0 79 70 8a 3a f7 69 24 8d 87 67 76  |_V$..yp.:.i$..gv|
406245d0  17 49 fa 2f 2e 8e b8 e2 68 69 76 7f 11 44 57 5d  |.I./....hiv..DW]|
406245e0  82 51 85 e2 ec 16 05 9b 72 71 6b 5c 55 d7 16 c1  |.Q......rqk\U...|
406245f0  dd fd 75 6e c5 de 9d dd ae bd 56 80 eb cb ca c3  |..un......V.....|
40624600  60 5c 23 c8 4f 8e ea a5 72 58 26 46 3f 80 e6 79  |`\\#.O...rX&F?..y|
40624610  17 36 6f ad                                      |.6o.|
不是一开始就直接生成了 BODY_PREOBF_FINAL
而是先有一份完整的早期主体
后面再叠加块级修补,才得到最终主体
前两组 64 位值
  = 先按字节形成原值
  -> 再由控制字节驱动做表式位补丁
  -> 写回到 BODY_PREOBF_FINAL 的开头 16 字节
40624400  93 f9 8a f0 27 43 e9 16 57 36 e7 ed 19 45 66 5d  |....'C..W6...Ef]|
0x40624400 -> 0x16e94327f08af993
0x40624408 -> 0x5d664519ede73657
0x40624400 -> 0x06e94323b088d913
0x40624408 -> 0x5d664519ede51657
def obfuscate_group1(value: int) -> int:
    # control_byte = 0x06
    result = value
    result |= (1 << 40)
    result &= ~(1 << 17)
    result |= (1 << 51)
    result &= ~(1 << 7)
    result &= ~(1 << 30)
    result &= ~(1 << 13)
    result &= ~(1 << 60)
    result &= ~(1 << 34)
    return result & 0xFFFFFFFFFFFFFFFF


def obfuscate_group2(value: int) -> int:
    # control_byte = 0x43
    result = value
    result |= (1 << 40)
    result &= ~(1 << 17)
    result |= (1 << 60)
    result &= ~(1 << 13)
    result |= (1 << 30)
    result &= ~(1 << 7)
    result &= ~(1 << 51)
    result &= ~(1 << 34)
    return result & 0xFFFFFFFFFFFFFFFF


assert obfuscate_group1(0x16E94327F08AF993) == 0x06E94323B088D913
assert obfuscate_group2(0x5D664519EDE73657) == 0x5D664519EDE51657
输入:
  17.1 整段旧状态

算法:
  byte[0] 直接沿用
  byte[1] 单独补丁
  byte[2..0x212] 统一递推
  byte[0x213] = old[-1] ^ byte[-2]

输出:
  17
40624400  b9 f9 8a f0 27 43 e9 16 57 36 e7 ed 19 45 66 5d  |....'C..W6...Ef]|
40624410  a0 28 a9 73 73 ed 1a f7 00 7b 12 ff 27 92 8a fa  |.(.ss....{..'...|
40624420  3e cb 5b 36 d0 d7 db 1c 29 7a 03 ac e6 bf 0c 5a  |>.[6....)z.....Z|
40624430  cd 6c f4 7c f4 ff 7a bb 1e 80 fd 94 2b fa 95 df  |.l.|..z.....+...|
40624440  19 b1 ba cd 08 25 50 f6 8e a7 97 a5 a2 ef 91 f5  |.....%P.........|
40624450  cf f7 68 15 ff ab 30 a4 59 bc ed 67 06 6b 21 e4  |..h...0.Y..g.k!.|
40624460  c0 d2 82 57 37 62 f5 aa 73 1d 2c d2 c1 bc 6d 9d  |...W7b..s.,...m.|
40624470  53 7d b2 61 c6 4a 55 bc 75 83 8a 47 d2 40 09 de  |S}.a.JU.u..G.@..|
40624480  c5 82 b7 d5 f3 a1 46 3f 85 61 0b 38 56 6e ba 99  |......F?.a.8Vn..|
40624490  5e 0d f8 8f 80 6f f0 6a 09 4c ab 5e cb 76 71 11  |^....o.j.L.^.vq.|
406244a0  eb 18 13 c5 f7 3f 3f e6 fb 7a 28 5a c3 84 75 53  |.....??..z(Z..uS|
406244b0  5b c4 4c d3 2a 36 2b f0 90 3d e7 31 47 b7 7a f8  |[.L.*6+..=.1G.z.|
406244c0  3d 2c 7d e4 f2 57 54 8f 64 f1 02 75 2b 99 66 2d  |=,}..WT.d..u+.f-|
406244d0  d4 eb 72 9e 82 63 30 83 7d a6 dc b4 5c 7b ef b0  |..r..c0.}...\{..|
406244e0  fc 18 74 f4 92 c0 b5 f9 8f 1d f1 99 66 c5 39 64  |..t.........f.9d|
406244f0  db d2 bc 09 b4 36 62 99 0d 2f a1 f3 1f 13 e2 34  |.....6b../.....4|
40624500  f1 f7 07 be 40 d4 7d 8f 1d 1b e9 6d 46 a0 10 9d  |....@.}....mF...|
40624510  b0 d2 00 dc 1f 1d 2f ee 85 00 96 64 0e 8a 1e 3d  |....../....d...=|
40624520  62 0b 47 6a 87 21 35 c5 73 71 81 6b 8a 2b c2 62  |b.Gj.!5.sq.k.+.b|
40624530  c2 0d e6 14 48 8f 19 c1 49 d7 b1 b1 37 d7 28 9e  |....H...I...7.(.|
40624540  29 87 4c e5 ea 15 e2 a4 8c df 51 5e 96 1b 84 32  |).L.......Q^...2|
40624550  d6 9e 8e d1 dd a9 f9 0d ec 5f 23 63 fe f9 3c 84  |........._#c..<.|
40624560  fb 9a fd 6e e9 7e 23 75 99 40 32 de 14 fb 75 73  |...n.~#u.@2...us|
40624570  bb f7 02 42 9d 47 73 d3 4a a1 12 32 b8 0a 47 25  |...B.Gs.J..2..G%|
40624580  b8 87 4d 53 ae 8a 80 6b 71 28 69 56 0d 59 77 3d  |..MS...kq(iV.Yw=|
40624590  a7 c2 b4 96 7c ea d2 f5 f4 10 68 8c 71 1d f5 47  |....|.....h.q..G|
406245a0  55 45 46 3a 31 f6 a0 56 fb 43 08 e2 83 3a 61 29  |UEF:1..V.C...:a)|
406245b0  8e 9d e8 1e 10 e3 61 0b 44 4b 91 97 26 78 07 a3  |......a.DK..&x..|
406245c0  5f 56 24 d6 e0 79 70 8a 3a f7 69 24 8d 87 67 76  |_V$..yp.:.i$..gv|
406245d0  17 49 fa 2f 2e 8e b8 e2 68 69 76 7f 11 44 57 5d  |.I./....hiv..DW]|
406245e0  82 51 85 e2 ec 16 05 9b 72 71 6b 5c 55 d7 16 c1  |.Q......rqk\\U...|
406245f0  dd fd 75 6e c5 de 9d dd ae bd 56 80 eb cb ca c3  |..un......V.....|
40624600  60 5c 23 c8 4f 8e ea a5 72 58 26 46 3f 80 e6 79  |`\\#.O...rX&F?..y|
40624610  17 36 6f ad                                      |.6o.|
4062460c  58 33 1c 32 55 07 24 c2                           |X3.2U.$.|
0xad = 0xc2 ^ 0x6f
0x10a93 = 0x10a53 + 0x40
0x93 = low8(0x10a93)
4062460c  3f 80 e6 79 17 36 6f ad                           |?..y.6o.|
4062460c  58 33 1c 32 55 07 24 c2                           |X3.2U.$.|
40624400  93 f9 8a f0 27 43 e9 16                           |....'C..|
def calc_layer17_body_full(prev_full: int, prev2_byte: int, old_byte: int, idx: int) -> int:
    flag = ((prev_full & 0xE0) >> 5) & 0xFFFFFFFF
    base = (prev_full << 3) & 0xFFFFFFFF

    inv1 = (~(base | flag)) & 0xFFFFFFFF
    tmp2 = (~(inv1 | ((~idx) & 0xFFFFFFFF))) & 0xFFFFFFFF
    inv2 = (~(base | (idx | flag))) & 0xFFFFFFFF
    state = (~(inv2 | tmp2)) & 0xFFFFFFFF

    mixed = state ^ (prev2_byte & 0xFF)
    temp = (~mixed) & 0xFFFFFFFF
    return (temp + (old_byte & 0xFF)) & 0xFFFFFFFF


def calc_layer17_second_byte_patch(first_byte: int, last_old_byte: int) -> int:
    return ((((~first_byte) & 0xFF) ^ (last_old_byte & 0xFF)) | 0x01) & 0xFF


def calc_layer17_from_layer17_1(layer17_1: bytes) -> bytes:
    if len(layer17_1) < 3:
        raise ValueError("layer17_1 buffer too short")

    out = bytearray(len(layer17_1))
    out[0] = layer17_1[0]

    prev_full = (
        (layer17_1[1] & 0xFF)
        + calc_layer17_second_byte_patch(layer17_1[0], layer17_1[-1])
    ) & 0xFFFFFFFF
    out[1] = prev_full & 0xFF

    for idx in range(2, len(layer17_1) - 1):
        prev_full = calc_layer17_body_full(
            prev_full=prev_full,
            prev2_byte=out[idx - 2],
            old_byte=layer17_1[idx],
            idx=idx,
        )
        out[idx] = prev_full & 0xFF

    out[-1] = (layer17_1[-1] ^ out[-2]) & 0xFF
    return bytes(out)


def calc_first_byte_mask_patch(previous_head: int, second_byte: int) -> int:
    return ((~previous_head) & second_byte) & 0xFF


def calc_first_byte_rolling_sum_from_layer17(layer17: bytes) -> int:
    acc = 0
    for b in layer17[1:]:
        acc = (acc + (b & 0xFF)) & 0xFFFFFFFF
    return acc


def calc_first_byte_from_full_layer17(layer17: bytes) -> int:
    rolling_sum = calc_first_byte_rolling_sum_from_layer17(layer17)
    final_value = (rolling_sum + calc_first_byte_mask_patch(layer17[0], layer17[1])) & 0xFFFFFFFF
    return final_value & 0xFF


def calc_last_byte_final_patch(previous_last_byte: int, previous_byte: int) -> int:
    return (previous_last_byte ^ previous_byte) & 0xFF
17.1
  -> 17
  -> BODY_PREOBF_EARLY
  -> BODY_PREOBF_FINAL
  -> 0x40639296
ROOT530
  -> APPLE
  -> 17.1
APPLE = 17.1 with byte[0] restored from 0xb9 to 0xa0
40624400  a0 74 ff 9f 31 10 25 22 a9 dc d4 f0 9e 6e a4 d7  |.t..1.%".....n..|
40624410  3d 72 9d ea 9a eb 25 22 be 6a d4 f0 19 6e 38 d4  |=r....%".j...n8.|
40624420  bc f6 9e 69 9f 6d 24 4b 3d f7 d4 f6 31 76 42 4b  |...i.m$K=...1vBK|
40624430  bc 72 91 75 18 ee b8 d7 be f3 1e e9 91 eb 58 49  |.r.u..........XI|
40624440  45 08 91 f5 99 ee b8 d7 3e 73 91 f6 99 ed 25 22  |E.......>s....%"|
40624450  3e d2 8b fd 94 6b 25 22 a7 f4 d4 f0 91 76 25 22  |>....k%".....v%"|
40624460  27 56 b7 fd 94 ee 38 55 3c 76 1e ea 98 6e b8 56  |'V....8U&lt;v...n.V|
40624470  45 f4 7d fd 94 6d 38 54 3e f2 9e f4 9f ed b8 56  |E.}..m8T&gt;......V|
40624480  45 f4 0b 92 91 71 bf 54 bd fc 10 eb 9d 76 66 ce  |E....q.T.....vf.|
40624490  45 08 9d e9 91 8e 5e ea 45 08 9d e9 91 14 58 74  |E.....^.E.....Xt|
406244a0  45 08 9d e9 91 1f e8 48 9c 0b ab eb 1a 6e bf d4  |E......H.....n..|
406244b0  bb fd 1c e9 91 6e 59 c9 a5 0b ab eb 1a 6e bf d4  |.....nY......n..|
406244c0  bb fd 1c e9 91 0e e3 c2 45 57 24 a0 91 7b 90 93  |........EW$..{..|
406244d0  b4 46 cc 4a d5 c3 80 b9 72 58 6f 50 02 07 49 8c  |.F.J....rXoP..I.|
406244e0  87 cf 53 4d ca 46 28 84 22 8a 7f 13 38 0d e0 48  |..SM.F(."...8..H|
406244f0  c6 1e 7c ce b5 90 56 6c 64 38 30 cd e2 0a 5c 30  |..|...Vld80...\0|
40624500  35 b2 54 8b 37 8e 5e c8 27 8a b9 cd d5 f3 5e cd  |5.T.7.^.'.....^.|
40624510  9d dc 35 9e 12 4f 11 62 c6 dc 36 14 b8 94 63 a3  |..5..O.b..6...c.|
40624520  3a 1b 60 7d b8 95 de 75 a7 e9 54 c2 81 3e c0 75  |:.`}...u..T..>.u|
40624530  a4 53 7f 1e bb f3 1c 32 79 8a 7f 13 38 0d e0 48  |.S.....2y...8..H|
40624540  c6 1e a4 8c 12 0d e7 ea 1c 69 8a 7d 86 c3 85 a3  |.........i.}....|
40624550  1c 74 ff 8b 32 14 c0 3f b6 93 70 81 63 c3 ac cc  |.t..2..?..p.c...|
40624560  74 d5 4b 85 d4 c3 a0 76 7a 11 24 99 bd 0f 1b b3  |t.K....vz.$.....|
40624570  5a d7 79 d7 02 23 45 7f e0 9a 50 7d b8 95 de 75  |Z.y..#E...P}...u|
40624580  a7 e9 54 c2 02 2e fd 75 c5 92 24 3f 65 0d c1 ab  |..T....u..$?e...|
40624590  b6 54 d8 8b 11 cb 90 e1 da dc 57 55 02 b8 fd 75  |.T........WU...u|
406245a0  c5 92 24 0f 65 0d c1 ab b6 64 54 8b 37 8e 5e c8  |..$.e....dT.7.^.|
406245b0  27 8a b9 88 bd 0f 1b 6b c6 dc 36 14 b8 94 63 a3  |'......k..6...c.|
406245c0  3a ef 54 8b 37 8e 5e c8 27 8a b9 9c 12 0d e7 ea  |:.T.7.^.'.......|
406245d0  1c 69 8a 7d 86 1a 45 7f e0 e2 70 81 63 6f 45 7f  |.i.}..E...p.coE.|
406245e0  e0 fa 70 81 63 77 c0 75 a4 53 7f 1e bb f3 1c 4a  |..p.cw.u.S.....J|
406245f0  c6 dc 36 14 b8 94 63 a3 3a 0f 10 75 9a ee 16 26  |..6...c.:..u...&|
40624600  35 1e a4 0f b1 46 28 84 42 98 e1 a9 58 33 1c 32  |5....F(.B...X3.2|
40624610  55 07 24 c2                                      |U.$.|
root_i
  -> state_i
  -> seed_i
  -> full_i = seed_i ^ mask_i
  -> byte_i = low8(full_i)
APPLE[1..0x212]
  全部遵循同一套 root_i -> state_i -> seed_i -> byte_i 模板
ROOT530 = 0x40639001..0x40639212
root_addr(i) = 0x40639213 - i
40639001  01 18 c2 97 08 a0 b3 30 97 0d 38 d6 df 60 62 08   |.......0..8..`b.|
40639011  02 18 02 1a 04 33 30 28 39 2a 08 21 6e 6f 74 74   |.....30(9*.!nott|
40639021  65 30 21 32 08 21 6e 73 74 73 65 74 21 38 fd 40   |e0!2.!nstset!8.@|
40639031  7a 40 fd 88 7a 48 fd 50 7a 50 fd 58 7a 5a 08 6f   |z@..zH.PzP.XzZ.o|
40639041  6e 6f 74 73 65 60 21 21 08 21 6e 6f 74 73 65 68   |notse`!!.!notseh|
40639051  21 6a 08 21 6e 6f 74 74 65 74 21 70 fd 78 7a 21   |!j.!nottet!p.xz!|
40639061  08 21 6e 6f 74 73 65 80 21 85 01 f0 23 88 c9 f0   |.!notse.!...#...|
40639071  01 f0 23 90 c9 95 01 74 23 98 c9 9d 01 f0 23 a0   |..#....t#.....#.|
40639081  c9 a5 01 f0 23 a8 c9 f0 01 f0 23 b0 c9 b2 01 6e   |....#.....#....n|
40639091  21 6e 6f 74 73 65 74 01 b8 01 fd 88 7a c0 01 7a   |!notset.....z..z|
406390a1  88 7a c8 01 fd 88 7a fd 01 fd 88 7a e0 01 ac a6   |.z....z....z....|
406390b1  ea a6 84 67 e8 01 fd f0 7a f2 01 08 21 6e 6f 65   |...g....z...!noe|
406390c1  73 65 f8 21 fa 01 08 6f 6e 6f 74 73 65 80 21 08   |se.!...onotse.!.|
406390d1  02 08 21 6e 6f 74 73 21 88 21 8a 02 08 21 6e 73   |..!nots!.!...!ns|
406390e1  74 73 65 90 21 92 02 6e 21 6e 6f 74 73 65 98 02   |tse.!..n!notse..|
406390f1  9a 02 08 21 6e 6f 74 74 65 a0 21 a2 02 08 21 74   |...!notte.!...!t|
40639101  6f 74 73 65 a8 21 aa 21 08 21 6e 6f 74 73 65 b0   |otse.!.!.!notse.|
40639111  c0 b0 02 fd 1d 7a c0 f8 f8 cb e4 a6 84 68 6a 70   |.....z.......hjp|
40639121  72 08 21 6e 6f 74 73 74 74 21 78 d6 df 02 90 ba   |r.!notstt!x.....|
40639131  ba aa ae e6 12 98 01 94 94 8d 98 0d a0 01 aa d1   |................|
40639141  d1 46 05 c0 01 10 20 ca 01 7b 22 63 6d 72 22 3a   |.F.... ..{"cmr":|
40639151  31 36 37 37 37 32 31 36 2c 22 63 6d 72 32 22 3a   |16777216,"cmr2":|
40639161  31 36 37 37 37 32 31 36 2c 22 75 6e 5f 68 22 3a   |16777216,"un_h":|
40639171  30 2c 22 76 70 6e 22 3a 30 2c 22 73 74 73 22 3a   |0,"vpn":0,"sts":|
40639181  30 2c 22 6b 64 22 3a 36 39 34 33 36 37 2c 22 66   |0,"kd":694367,"f|
40639191  6b 64 22 3a 31 35 36 34 36 30 30 36 30 34 2c 22   |kd":1564600604,"|
406391a1  70 64 22 3a 31 32 34 38 35 39 34 34 30 33 2c 22   |pd":1248594403,"|
406391b1  64 79 6e 22 3a 22 22 2c 22 64 6f 22 3a 30 2c 22   |dyn":"","do":0,"|
406391c1  6c 70 30 22 3a 35 32 30 38 33 30 39 31 33 32 32   |lp0":52083091322|
406391d1  38 2c 22 6c 70 31 22 3a 35 32 31 39 31 33 35 31   |8,"lp1":52191351|
406391e1  38 31 35 30 2c 22 61 30 22 3a 32 30 38 34 36 39   |8150,"a0":208469|
406391f1  36 38 35 37 30 32 33 2c 22 61 31 22 3a 31 30 38   |6857023,"a1":108|
40639201  30 31 32 39 39 32 38 2c 22 74 6b 22 3a 66 61 6c   |0129928,"tk":fal|
40639211  73 65                                             |se|
APPLE_MASK_BY_MOD8 = {
    0: 0x00,
    1: 0x05,
    2: 0x15,
    3: 0x3A,
    4: 0x8C,
    5: 0x27,
    6: 0x0B,
    7: 0x05,
}

APPLE_ADDEND_BY_MOD8 = {
    0: 0x75,
    1: 0xBE,
    2: 0x9C,
    3: 0x3C,
    4: 0x61,
    5: 0x5F,
    6: 0xB2,
    7: 0x02,
}


def calc_root_addr_for_apple(offset: int) -> int:
    assert 1 <= offset <= 0x212
    return 0x40639213 - offset


def calc_apple_root(root530: bytes, offset: int) -> int:
    assert len(root530) == 0x212
    return root530[-offset]


def calc_apple_state_from_root(root_i: int, mask_i: int, addend_i: int) -> int:
    step1 = (root_i << 4) & 0xFFFFFFFF
    step2 = (step1 + ((root_i >> 4) & 0xFF)) & 0xFFFFFFFF
    step3 = (step2 + (mask_i & 0xFF)) & 0xFFFFFFFF
    return (step3 ^ (addend_i & 0xFF)) & 0xFFFFFFFF


def calc_apple_seed(state_i: int, addend_i: int) -> int:
    neg_state = (~state_i) & 0xFFFFFFFF
    mixed = ((neg_state << 3) & 0xFFFFFFFF) | ((neg_state & 0xE0) >> 5)
    return (~((mixed + (addend_i & 0xFF)) & 0xFFFFFFFF)) & 0xFFFFFFFF


def calc_apple_byte_from_root530(root530: bytes, offset: int) -> int:
    root_i = calc_apple_root(root530, offset)
    mask_i = APPLE_MASK_BY_MOD8[offset & 7]
    addend_i = APPLE_ADDEND_BY_MOD8[offset & 7]
    state_i = calc_apple_state_from_root(root_i, mask_i, addend_i)
    seed_i = calc_apple_seed(state_i, addend_i)
    return (seed_i ^ mask_i) & 0xFF
ROOT530
  -> APPLE
  -> 17.1
表名 作用 关键字段 说明
instructions 指令主表 step, address, offset, instruction, semantic 每个 step 对应一条执行指令
registers 寄存器快照表 step, reg_name, reg_value, timing 同一 step 下会记录执行前后的寄存器值,timing 分为 before/after
memory 内存访问表 step, mem_addr, mem_value, mem_type, size 记录该 step 触发的读写内存行为
输出地址 字符 关键 step 查表地址 索引
0x40624c00 L 13305555 0x4065e7ad 0x00
0x40624c01 p 13305623 -> 13305697 0x4065e7bd 0x10
0x40624c02 L 13305799 0x4065e7ad 0x00
0x40624c03 L 13305466 0x4065e7ad 0x00
分组 输入字节 标准 Base64 索引 实际输出
0 01 00 00 [0, 16, 0, 0] LpLL
1 00 13 77 [0, 1, 13, 55] LFjK
2 95 46 0d [37, 20, 24, 13] +2Nj
3 25 0e 59 [9, 16, 57, 25] cpJq
4 00 21 a2 [0, 2, 6, 34] LOS1
偏移 地址 内容角色
+0x00 0x40639280 固定头部 8 字节
+0x08 0x40639288 分隔字节 0x0d
+0x09 .. +0x15 0x40639289..0x40639295 固定中间字段 13 字节
+0x16 0x40639296..0x406394a9 主体输入区
输入区间 长度 来源判断
0x40639280..0x40639288 9 字节 固定头部 + 分隔字节
0x40639289..0x40639295 13 字节 固定中间字段
0x40639296..0x406394a9 0x214 字节 主体输入块
名称 含义
BODY_PREOBF_FINAL 严格指 step 13300144 时、真正参与搬运到 0x40639296 的运行时最终主体层
BODY_PREOBF 分析别名;在 BODY_PREOBF_FINAL 基础上,把开头前 16 字节恢复成位混淆前原值,方便读懂前两组特殊处理
层名 含义
BODY_PREOBF_EARLY 更早已经整段成形、以 STRB 为主的主体基线
BODY_PREOBF_FINAL 在早期主体之上继续修补后的最终主体
分组 位混淆前原值 控制字节 位混淆后值
第 1 组 0x16e94327f08af993 0x06 0x06e94323b088d913
第 2 组 0x5d664519ede73657 0x43 0x5d664519ede51657
层名 含义
17.1 更早的字节级主体基线
17 17.1 基础上再做统一递推后的结果
分段 地址范围 长度 说明
ROOT530_PROTO_PREFIX 0x40639001..0x40639148 0x148 = 328 字节 前半段 protobuf/二进制字段区
ROOT530_JSON_TAIL 0x40639149..0x40639212 0x0ca = 202 字节 后半段可直接读出的 JSON 参数串
  • 先锁定某个关键 step
  • 再同时查看该步对应的指令、寄存器和内存访问
  • 必要时再沿 step 向前或向后回溯
  • “某一步调用参数是什么”,通常来自 instructions + registers(before)
  • “某一步最终把什么写进了哪里”,通常来自 instructions + registers(after) + memory
  • “重建某块缓冲区内容”,通常来自按地址范围汇总 memory 表里的历史 write
  • offset 在 SQL 里建议写成 "offset",避免和保留字冲突。
  • 很多逆向结论都直接依赖 semantic 字段,因为它已经把寄存器/内存结果解析成了可搜索文本。
  • 目标地址:0x40642000
  • 重建 step:13501921
  • 重建范围:0x40642000 - 0x40642500
  • 输出性质:不是单独的 X-Perseus,而是最终拼接好的整块参数输出
  • 已见参数头:X-ArgusX-GorgonX-HeliosX-KhronosX-LadonX-MedusaX-NeptuneX-Perseus
  • 重建方法:不能只按“每个起始地址的最新值”恢复,而是要按时间顺序回放覆盖该区间的 write
  • X-Argus
  • X-Gorgon
  • X-Helios
  • X-Khronos
  • X-Ladon
  • X-Medusa
  • X-Neptune
  • X-Perseus
  • 参数头起始地址:0x40642206
  • 参数值起始地址:0x40642211
  • 参数值结束地址:0x406424f4
  • 参数值长度:740 字节
  • 整块输出来自 0x408be000
  • X-Perseus 子区间来自 0x40625800
  • 更前面还会有一个与算法主体相关的源块 0x40624400
  • step 13501708
  • 调用参数:x0 = 0x40642000x1 = 0x408be000x2 = 0x4f7
  • 说明整块最终参数输出是从 0x408be000 拷贝到 0x40642000
  • step 13501424
  • 调用参数:x0 = 0x408be206x1 = 0x40625800x2 = 0x2f1
  • 因为 0x206 正好等于 X-Perseus 在最终大缓冲区中的起始偏移,所以最早就把 0x40625800 -> 0x408be206 识别成 X-Perseus 子区间搬运链
  • step 13501392
  • 调用参数:x0 = 0x408be000x1 = 0x40624400x2 = 0x206
  • 这一步把 0x40624400 和最终拼接缓冲区的前缀区联系起来
  • 0x40642000 <- 0x408be000
  • 0x40642210 <- 0x408be210
  • 0x406424e7 <- 0x408be4e7
  • 0x40642000 + 0x206 = 0x40642206
  • 0x408be000 + 0x206 = 0x408be206
  • 已确认 X-Perseus 在最终总输出缓冲区中的准确范围
  • 已确认它不是独立缓冲区,而是整块参数输出中的一个子区间
  • X-Perseus 从整块输出里准确摘出来
  • 给后续逆向明确了最早的追踪方向
  • 真正更早的成品字符串区是 0x40624c00
  • 用户确认的更前一步地址:0x40624c00
  • step 13433848 重建 0x40624c00 可以直接得到完整 X-Perseus 字符串
  • step 13500742 调用时,已经把 x1 = 0x40624c00 传给外部函数,参数为:
  • 不能把 0x40625800 当成最早成品区
  • 遇到 bl -> 跳板 -> br x17 这类外部路径时,不能只靠内存溯源结果本身,必须同时分析调用前参数
  • 完整 X-Perseus 字符串最早稳定落在哪块内存
  • 后续拼接链里看到的几个地址,谁是成品,谁只是搬运中转
  • 0x40624c00 不是通过一次性整块 memcpy 得到
  • 当前 trace 中能看到它主要由 strb w10, [x8, x9] 逐字节写入
  • 主要写入点落在 offset 0x1e865coffset 0x1f9d10
  • 写入前还能看到 ldrb 从自定义字符表地址取单字节字符
  • X-Perseus 头部从 0x40642206 开始
  • 参数值从 0x40642211 开始,到 0x406424f4 结束
  • 参数值长度是 740 字节
  • 0x40624c00 可以直接重建出完整 X-Perseus
  • 后续调用里它作为 x1 被传给外部函数
  • 所以它应视为“成品字符串区”,而不是普通中间缓冲区
  • 这条链足够完成“参数定位”和“首版上游链路”分析
  • 但它还不是最早的成品生成链
  • 真正更前面的分析入口,应转向 0x40624c00
  • 0x40624c00 这串文本是不是 Base64 类编码
  • 如果是,它和标准 Base64 的差异到底在哪里
  • 大部分字符都落在大小写字母、数字、+/
  • 总长度是 740
  • 末尾带有 =
  • 它大概率属于“Base64 分组不变,但字符映射被替换”的变种
  • 如果这个判断成立,那么应该能在 trace 里看到:
    • 先算出 6-bit 索引
    • 再按 base + index 去某个固定字符表取字节
    • 最后 strb 写入 0x40624c00
  • 先算出 6-bit 索引
  • 再按 base + index 去某个固定字符表取字节
  • 最后 strb 写入 0x40624c00
  • 0x40624c00 不是一次性 memcpy 出来的
  • 主要写入点在 offset 0x1e865coffset 0x1f9d10
  • 最终都是 strb w10, [x8, x9] 这种逐字节写回
  • 写入前先有 ldrb
  • 读取地址不是输出缓冲区附近,而是某个稳定的表地址
  • 首字符 L 命中 0x4065e7ad
  • 第二字符 p 命中 0x4065e7bd
  • 一块完整的输入缓冲区
  • 它的长度应满足 554 bytes -> 740 chars
  • 拿这块输入用“标准 Base64 分组 + 自定义表”编码,应该能完整还原 0x40624c00
  • 如果它不是 Base64 分组,而是别的查表算法,那么这 5 组很难这么整齐地全部命中
  • 连续 5 组都命中后,“分组公式就是标准 Base64” 这一点已经基本坐实
  • 长度仍然是 64
  • 分组公式不变
  • 唯一变化是索引映射表被整体替换
  • 基址:0x40639280
  • 最后一个有效字节:0x406394a9
  • 总长度:554 字节
  • 输出地址:0x40624c00
  • 字符串长度:740 字节
  • 按标准 Base64 的分组方式切 6-bit
  • 按自定义字符表映射
  • 得到的 740 字节输出与 0x40624c00 中的完整 X-Perseus 完全一致
  • 输入基址:0x40639280
  • 输入长度:554 字节
  • 输出地址:0x40624c00
  • 输出长度:740 字节
  • 如果要继续解释 X-Perseus,下一步就不该再盯着 0x40624c00
  • 应该转去分析 0x40639280 这块 Base64 输入缓冲区是如何形成的
  • 一个“完全自定义的文本编码”
  • 或者“普通 Base64 函数的某个变体实现”
  • 3 字节输入拆 4 个 6-bit 索引这一层没有变
  • 变化只发生在字符映射表
  • 因为最后一层规律已经闭合
  • 后续更应该关注的是输入缓冲区 0x40639280 的来源
  • 0x40639280 是下一步最该分析的对象
  • 更早的算法问题,应围绕这块输入缓冲区继续展开

[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!

收藏
免费 10
支持
分享
最新回复 (5)
雪    币: 96
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
666
4天前
0
雪    币: 211
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
3
666
3天前
0
雪    币: 3866
活跃值: (2694)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
看不懂怎么办
3天前
0
雪    币: 104
活跃值: (8062)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
tql
2天前
0
雪    币: 201
活跃值: (380)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
xp算法流程比xm短
23小时前
0
游客
登录 | 注册 方可回帖
返回