日志逆向分析:从执行日志中推演数据处理与编码过程
本笔记记录了一次针对日志驱动的逆向分析过程,核心结论为:程序利用一份乱码字符串作为输入,通过“按三位一组、
charAt取值、位移与位运算逻辑、再经类 Base64 码查表转换”等方式,最终生成目标值(如case 86输出结果),但目前对部分源头值(以A开头的一连串引用)的具体生成环节尚未完全追踪完毕,多处结论仍停留在推测阶段。
分析背景与目标
- 目标是理解最终结果的产生原理。
- 主要依据的材料是某次运行的日志。
- 在分析过程中使用的推断手段与一般 Debug 流程:先打印出传入的参数(入参),然后从未知结果的输出位置向上搜索,寻找转换过程,最终确定从入参到最终数据的流程。
- 涉及的主要对象:
- 某长度为 543 位左右的字符串数组(下称“乱码”)
case 86、case 19等代码中的数据处理节点- 针对一组字符进行
charCodeAt/fromCharCode、并行的位运算逻辑 - “本次日志中”特定位置的值(例如 0x0400B315)是目标关键值
日志分析时将入参记录展示出来,主要用作初始自变量。
本次用到的数据来源说明
- 分析时只用了某一份日志,第一次分析可能只针对一次运行过程(不一定覆盖其它可能变化的分支)。
- 引用到的日志:是从输入侧打断点,然后继续将浏览器环境中的
代码值与日志记录的结果进行对照确认(断点打印位置决定了case 86展开过程中产生的结果)。
分析方法概述
- 过程是“倒推法”,基本顺序为:
- 拿到最终结果(输出),观察它的产生位置(如某个
result与case 86)。 - 找到产生最终结果的关键索引(例如
63)。 - 往上继续查询索引 63 的来源。
- 逐渐剥离日志内的处理步骤,归纳整理位运算与码表操作。
- 如果遇到与常见的编码(如 Base64)类似的操作,先尝试用已有编码表去做假设测试,如果命不中,再回归日志,继续追踪具体的中间来源。
- 前提:
- 所有被分析的对象都是可信的代码实际记录。
- 日志较长时,分析方法是逐步打断点,将各处断点的中间值记录下来,再人工拼出整个链条。
- 分析期间手动修改日志或者打印内容,让阅读更方便;通过 “搜索+断点+人工对比” 去确认哪一段代码当时做了哪些操作。
分析的关键过程(日志逐步追踪)
从最终结果倒推第一个关键索引
- 在
case 86中输出结果是通过某个变量取到的,代码逻辑上“取了这个东西的第63位”。 - 通过日志中打印的位置寻找,得到的前置操作:
- 左侧是 63,右侧是 63,输出是 63(bit位相关)。
- 结果是
case 86下的最终计算结果,最终值标记过低。 - 通过往上追踪,观察到代码中此过程主要是:
- 取值逻辑:先对某个值做“右位移”或“与”操作。
- 结果指向某一位(第 63 位)。
注意点:数组中从 0 开始计数,因此取 63 位实际就是第 64 个元素。后续相关的索引取值时需要留意此基准。
- 追踪
63来源: - 来源处是
charAt(63),内容是某一个结果。 - 然后在“上面若干步骤”中,存在一个
H操作,H 是取当前值的第 8 位而来。 - 向上继续找第 8 位怎么来的:
- 过程越来越深,每层继续做同样的“先查找”操作。
28 的来源以及三位一组的规律
- 在某个位置出现值 28:
- 通过某个值(比如
charAt(542))取得一个值,随后通过“与 63”“或”等位操作得到 28。 - 分析实例中有一个关键位置:
63、192、28三个值同时参与运算:- 63:是通过
charAt(540)得到的值。 - 192:通过
charAt(541)得到。 - 28:通过
charAt(542)得到。 - 该三位是“一组”,行为是先将 63 向左移 2 位,再右移 1 位,得到 48;再取 192,向右移位再与 60 做与等运算,得到中间值;最终得到 60。
- 于是得出结论:当前处理逻辑是按“三位一组(540、541、542 连续索引)”取值,取完后按顺序进行相同的位运算和查表操作。
索引 542 与长度 543 的来源
- 出现“543 - 540 = 3”的日志记录,说明这一段的长度信息可能是通过当前位与该组起点之间的差计算出来的。
- 比如 543 本身含义与这段数据的长度有关。
- 该三位(540、541、542)可以理解成一次处理循环中的一组。
第一位字符的产生逻辑
基于字符数组的 charAt 运算
- 输入是一串“乱码”,比如类似
0x...的字符串(需要再仔细确认)。 - 在三位的分组运算中,每次先拿到“三个值”:按位数取(
charAt(540),charAt(541),charAt(542))。 - 然后进行一个同样的运算流程。
第一次出现的位运算
已知:
- 第一步:
63是charAt(540)的结果。 - 操作:第一次取到之后进行
左移两位,得到结果。 - 再经后续取余等操作,最终得到一个“code”。
- 这个“code”会被用于第二个阶段。
- 第二步:取 192,跟先前的值做“右移”或“与”等位运算。
- 第三步:取 28,然后同样参与到运算。
- 整个位运算过程类似于 Base64 的转换方式,但在开头做了一次
charCodeAt/fromCharCode操作,所以直接套常规 Base64 码表并不能全部对应上。
最后得到的第一个目标“字符”
- 通过上述位运算+查表得到 5(索引),然后再从码表中取第 5 个字符,得到某个字符。
- 位运算逻辑和 Base64 的取 6 bit 过程的思路近似。
整个流程的类别归纳
- 从 524 位开始,取三位:
60、247、54:- 三个数分别是:
charAt(522)-> 247charAt(523)-> 54charAt(524)-> 60- 组合过程:
- 247 左移两位,得到某值。
- 做类似 Base64 的 6 bit 分段取法。
- 再做类似操作得到第二个值。
- 依次循环得到第三个值。
- 随后也是一样的顺序:得到位数后,再查表,得到下一个输出字符。
- 可见:
- 从 522 位开始,一次处理三位(索引递增:522、523、524……)。
- 每次都按同样模式进行:
- 先取 3 个字符代码。
- 对这 3 个数的二进制进行拼接、移位、相加。
- 得到的结果作为索引,去某个“码表”里面取新的字符。
- 把这一个或多个结果再拼接到最终出去的那串目标字符串中。
四位一组的假设与验证失败
- 在上述流程推导到一段时,尝试以每 4 位一组去推,出现了与预期不符的情况:
- 尝试将一段“乱码”(例如 Base64 示例字符串)按 4 位一组作 Base64 解码,输出与日志中结果不匹配。
- 乱码看起来像是 Base64 字符串,但不是直接标准解码。
- 后续观察到,还需要先做其它处理,所以暂时否定了直接用 Base64 解码 4 位一组的假设。
追踪某一个组时产生的值
以一个示例组来说明:
- 示例组:
227、110、124、422? (注:此处应是 227 与后面数位做的计算)- 简记中存在一些中间结果:
- 227 左移两位得到某个数。
- 227 与 3 做与(AND 3),结果是
110(注:此 110 可能是指227 & 3,值不太合理,日志写法中可能有误写,根据上下文应属推断)。 - 另一个示例:
200、415、9、236?;- 第一次拿到 200 后与 415 按流程运算;
- 得到 60,再进行后续操作;
- 第二次 240 又做同样的“按模取 3”(AND 3),然后获得了新的分支。
- 整体来说,追踪这些值就是不断确认:
- 在索引 522 之后,同一套流程被循环执行多次。
- 每次得到 3 个字符或类似数量的输出片段值。
循环初期的确定位置
- 前面很长一段流程是统一的:
- 从某个起始索引位置开始,
charAt取三位,进行位运算。 - 得到的值再进行
charAt或fromCharCode操作。 - 一直到指定的长度为止。
- 从“最前方”也就是最开始的位置开始检查:
- 例如索引从第 0 位、第 1 位、第 2 位开始:
- 第一个三位的处理结果与后续处理节点匹配。
- 第 0 位取到 4,第 1 位取到 0,第 2 位取到 179。
- 对比我们预推的流程,发现与前面的推断吻合。
- 例如:
- 4、0、179;
- 左移 2 位后得到结果,再在某表查到字符
D。 - 409 与 3 做与,取 0,再查表,结果为
0。 - 第三步在 409、179 上做类似处理,得到
2。 - 三个字符拼起来得到一段如“D0 2”的结果(具体值仍需后续串接才能确认最终语义)。
- 由此确认:前段算法流程即:
- 准备码表(可能类似 Base64 字符集)。
- 对某个输入字符串进行三位一组的取码点计算。
- 按位拆分并做类 Base64 取码表映射。
- 输出得到的字符,继续直到整串处理完。
较长目标串的拆分
- 在后续分析中,发现某个很长的目标串是由不同片段拼接而成:
- 比如片段 1 为类似
0x0400B315...的一段: - 它来自某一段字符串的分组(例如两个一组取十六进制转字符)。
- 其它片段(例如以字母 A 开头的一串)来源尚未追踪完全。
- 两个片段:
- 第一个片段:
0400B315B3等类似内容,是通过对另外一串数据每两位转十六进制,再fromCharCode后拼接得到的。 - 实例:
04转为 0x04,得到某个控制字符; 00转为 0x00,等等;- 以这种方式构造出目标字符串的第一部分。
- 第二个片段:以
A...开头的一长串,还被继续追踪(见下文)。
第二段的生成来源追踪
从日志中定位到 539、141、145、28
- 片段 2 的源头出现的早期日志:
- 某处有两个值 141 和 145。
141是取一个“前面乱码串”第 538 位得到的值(charAt(538))。- 将 141 与 145 求与(或者某运算),得到 128;最终 28 是这样得到的?
- 如果记录成初步结果:
141与145做与(或类似运算)得到 128;- 再取
28是另一个经过处理得到的中间值; - 这 28 又被用于获取数组下标。
- 但随后又有:
141与 145 做取余后得到数字 12。- 数组的第 12 位正好为 145(如果从第 0 开始计数,第 12 个元素为 145)。
追踪 121 与 147
- 分析到某条链时:
- 取到
121、147: - 这两个数相加后再取模(如 % 156)得到
12。 - 12 是索引,指向第 12 位(某数组的下标)。
- 第 12 位对应的值是 145。
- 继续追 147 的来源:
- 可能与另一个数组索引
173有关; - 尚未确定具体生成方式。
- 这一段的分析仍未完全完成:
- “141可能是明文”,即前一次 “乱码中的某索引位置转换后得到 141”,后续再与另一个结果做异或或相与;
- “145可能是下标对应出来的明文”,后续当前组分析戛然而止。
三类值的出现:538 位的含义
- 已经确认的一段数据:
前面某个乱码串长度约为 539(即索引最大到 538)。- 在日志中
charAt(538)就是最后一位,得到 141。 - 那么 538 作为界限,后续整段处理是否也到达收尾处需再确认。
与 Base64 关联的观察
- 分析过程中发现其中的位运算规律与 Base64 算法中 24 bit 拆分为 4 个 6 bit 的设计非常接近。
- 例如:
- 分组取 3 个字符;
- “左移两位再右移四位”、 “AND 63”;
- 最终用得到的下标去一个码表中取字符。
- 因为一开始认为可能是 Base64,所以利用常规 Base64 码表做测试:
- 使用标准 Base64 码表(大 A 到 Z、小 a 到 z、0 到 9、+、/),测试结果并不是该码表直接输出。
- 因此判断可能有前置变换。
- 后来在某一特定步骤中,先将结果与某个较长的码表进行匹配时,看到结果确实有可能是合法的“自定义码表”;
- 例如通过码表
charAt(某值)取出来的是5等结果。 - 但由于当时截取到的码表内容与标准 Base64 并不同,所以判定并不是常规 Base64。
未实际验证的推断
- 当前认为:
- 码表可能是 Base64 字符集,也可能被魔改;
- 但因为前面得到的中间值已经做了一层 “fromCharCode”,因此直接测 Base64 不会命中输出;
- 正确流程是:
- 先用某种方式把原始输入分组转成若干码点值;
- 对码点值进行类 Base64 的运算;
- 输出可以是字符代码(char code),也可能是表面上的字符;
- 在进一步确认之前,这部分当作“是 Base64 类似流程,但基础表不一定为标准表”处理。
十六进制分组的发现
- 在追踪某个已有目标串片段时:
- 比如字符串
0x0400B315...的生成: - 从某一个大字符串中每两位切分一次;
- 每两位作为一个十六进制数(如
04、00、B3、15); - 该十六进制数转成十进制后,再
fromCharCode,得到字符; - 依次拼接,得到长串。
- 这可以解释“看起来是十六进制串被转储字符串”的现象。
- 结论:串
0400B315B3是十六进制按字节转字符串结果。
已确认与未确认的流程汇总
已确认的部分
- 程序从某个乱码输入出发;
- 按索引 3 个一组取值;
- 对每组进行类 Base64 运算(位移 + 与 + 查码表);
- 形成第一段结果;
- 对另一些数据按两位一组的十六进制转换后
fromCharCode形成片段。 - 用这些片段再拼接出后续要使用的长字符串(如案例中的
0x0400...段)。
尚未确认的部分
- 以字母 A 开头的那一串片段是如何生成的(只追溯到“某数组取值、与 141/145、121/147 相关操作”)。
- 上述“最后 538 位后的索引取值”具体与哪些固定偏移绑定。
- 目标案例
case 86中“最终码表”的具体表项; - 是否所有运行场景都使用同一份固定日志数据,还是会根据其它动态值改变;
- 正则中用到的
0x0400B315是否每次都是固定取值。
日志断点调试建议(来自分析过程的操作)
- 为了便于追踪,按以下方式设置断点和观察方式:
- 在最终结果生成之前打断点(例如最后输出位置的上一行)。
- 查看调用栈,并记录当前操作变量名。
- 再向上回溯(call stack 的上一帧),逐步确认取值位置。
- 每次使用
搜索日志关键字的方式定位到对应代码行,手动对照。 - 遇到的变量如果是由
charAt/charCodeAt/fromCharCode结果组成的,在控制台直接打印实际值更直观。 - 为了简化,可以把日志打印点改清晰,比如显式打印每次的分组起点。
遇到的问题
- 分析时存在大量“从上往下”看不出逻辑的情况,需改为从下往上(从输出反推到输入)。
- “有几个中间量(如 145、147)虽然在码表中看起来不像正常值,但实际是某个数组下标,或者数组项本身”。
当前阶段总结(可操作结论)
- 本日志中:
- 最终结果来自
case 86; case 86相关输出不是直接计算结果,而是先对一组三位输入做两次“64 基”式的位操作,再用表索引完成查表;- 日志中已知的部分逆推流程:
- 输入先按三位分组存入数组;
- 通过 charCodeAt/fromCharCode、左移右移、and 63 等得到若干中间码点;
- 中间码点进入类似 Base64 的查表流程;
- 其中有一步依赖 “码表”(当前不确定表来源);
- “乱码串”长度为 539(索引 538),且该长度是结算末尾的依据;
- 更完整的逻辑需要下一次再继续跑日志、并在生成关键值时打断点,尤其是要观察:
- 147 具体如何产生;
- 141 是否总是某乱码串第 538 位的值;
- 码表具体地址与内容。
附:从日志中看到的部分关键数值记录
以下为在分析过程中记录的、需要后续对照的重要数值与取值位置:
| 含义/阶段 | 关键数值 | 来源取位 | 备注 |
|---|---|---|---|
| 最终结果相关索引 | 63 | charAt(63)等 | 最终结果产生前的关键取位 |
| 第一组三位 | 63、192、28 | 第 540、541、542 位 | 用于做位移运算 |
| 中间代码 | 60 | 对前三值运算后得到 | 随后查表得到 7 |
| 连续分组示例 | 60、247、54 | 第 522、523、524 位 | 样例行 |
| 连续分组示例 | 227、... | 示例 | 用于步进验证 |
| 末尾来源 | 141 | 某串第 538 位 | 对应最后一组 |
| 潜在下标 | 121、147 | 从不同位置取 | 生成 12,再取数组第 12 位 = 145 |
| 后续拼接片段 | 0x0400B315B3... | 按位拆分后再转 | 来自两个一组十六进制转字符 |
下一步执行清单(行动方向)
- 再跑一次相同日志,在该流程起始处打断点(如输入乱码载入后)。
- 记录每次“三位一组”的三位初值。
- 将日志打印点补充到:
- 开始取值处;
- 左移/右移/与运算结果处;
- 查表结果处;
- 最终拼接处。
- 对码表部分做“dump”,得到当前执行环境的实际表结构与长度。
- 验证:
- 该表是否固定,或者是每次动态生成。
- 追踪第二个大段(A 开头片段)的生成:
- 跟踪“147”的赋值位置;
- 确定“141”是否每次是第 538 位索引值;
- 必要时打印那次
charAt(538)对应的原串字符内容。
分析时的保留结论
- 目前无法仅凭本次日志还原“完整算法”;
- 对已经走通的路径,核心结论是:
- 处理过程中存在“按字节(charCodeAt -> 位移)转码”和“查自定义码表”两个阶段;
- 且这两个阶段具体在代码中以大量相似分支实现,不适合直接“阅读源码+静态推导”,采用日志追踪的方式是可行的。