JS逆向之某头条jsvmp逻辑层算法分析

今天我们来研究下,某头条的jsvmp逻辑层加密算法,其主要的目的是想在大家在接触此类算法时,给出点实质性的建议和思路。

0x01 分析加密

进入到目标网站通过分析请求会发现一个动态的 _signature 加密参数
JS逆向之某头条jsvmp逻辑层算法分析_第1张图片
0x02 定位加密

不同于以往,我们这次使用 ajax 请求拦截方式,来对这个参数进行拦截
JS逆向之某头条jsvmp逻辑层算法分析_第2张图片
通过堆栈的调用信息,得出以下结果

JS逆向之某头条jsvmp逻辑层算法分析_第3张图片
打上断点实现最终定位

JS逆向之某头条jsvmp逻辑层算法分析_第4张图片
换一种形式调用

在这里插入图片描述
0x03 window被赋予的属性

我们分析得知: _signature 参数由 window.byted_arawler.sign 方法实现,然而这个方法指向 acrawler.js 文件中一个莫名其妙的位置
JS逆向之某头条jsvmp逻辑层算法分析_第5张图片
所以我们目的也有了初步的明确:只要让 window 对象拥有 byted_acrawler 属性,那么整套算法就会被我们完全击破

JS逆向之某头条jsvmp逻辑层算法分析_第6张图片
0x04 惯用的方法

我们在本地新建一个 HTML 文件把代码整体拷贝过来,然后用浏览器打开
JS逆向之某头条jsvmp逻辑层算法分析_第7张图片
我们惊奇的发现,这个 byted_acrawler 属性已经存在于 window 对象之中,但是当我们借用 node 去执行它的时候,却秒被现实打脸

JS逆向之某头条jsvmp逻辑层算法分析_第8张图片
0x05 如何分析

我们先整体看下代码,除了 if-else 就是 else-if 活活的一个无限套娃形式,而且里面的逻辑,只用单一的字母表示,根本没有任何阅读性

JS逆向之某头条jsvmp逻辑层算法分析_第9张图片
虽然通篇都是 if-else 但它肯定也符合一定的逻辑规律,如若不然,它一定会报错。只不过官方这样做的目的,让它失去可读性,从而导致我们无法对分析。试想,我们可不可以,把它换一种形式进行表示出来,从而恢复它的可读性呢?
JS逆向之某头条jsvmp逻辑层算法分析_第10张图片
0x06 平坦化流程

这是一个全新的概念,也是对抗 jsvmp 算法必会的概念,虽然方式比较简单,但工程量比较庞大。我们需要将代码中所有的 if-else 替换成 switch-case 但问题在于 switch 哪个变量 case 那种情况?

观察一下这段代码:

var j = parseInt("" + b[O] + b[O + 1], 16);

JS逆向之某头条jsvmp逻辑层算法分析_第11张图片

验证一下,整体流程会不会因为j 变量的动态变化,进入到不同的逻辑分支?
JS逆向之某头条jsvmp逻辑层算法分析_第12张图片
不妨我们通过开篇所讲的条件断点试一下,每次 j=16 时会不会进入同一个逻辑分支中,如果能,说明 j 所有 if-else 的动态参数。如果不能,那就再去验证其他参数。

JS逆向之某头条jsvmp逻辑层算法分析_第13张图片
经验证,我们可以确定 j=16 时都会进入到上述的分支,所以我们不妨可以这样将它表示出来


 switch (j) {
   case 0:
     // todo somethind
     break;
   case 16:
      q = S[R--],
      w = S[R--],
      (A = S[R--]).x === G ? A.y >= 1 ? S[++R] = K(b, A.c, A.l, q, A.z, w, null, 1) : (S[++R] = K(b, A.c, A.l, q, A.z, w, null, 0),
          A.y++) : S[++R] = A.apply(w, q);
      break;
    default:
      //todo something
      return;  
 }

最终我们梳理的结果是这样的
JS逆向之某头条jsvmp逻辑层算法分析_第14张图片
这里注意一下:x 会有一个偏移量计算,每次运行都会 x>>4 并且 x 和 A 的值相等

0x07 环境对比
等我们对代码平坦化流程之后,我们需要考虑之前的一个问题,为什么浏览器环境就可以拥有byted_acrawler 而 node 环境却是 undefined 呢?可能的原因是,它肯定存在某种环境的检测,导致 node 环境失效

我们先从运行的开端分析一下(这也是很多人会忽略的细节)

glb = "undefined" == typeof window ? global : window

这段代码放到浏览器运行,代表的是 window 对象

JS逆向之某头条jsvmp逻辑层算法分析_第15张图片
如果把它放到 node 环境运行,它代表却是 global 对象
JS逆向之某头条jsvmp逻辑层算法分析_第16张图片
所以我们需要改变下 node 环境,这里我采用的是 jsdom 来对环境进行一下伪装,这样的话,我们就改变了初始的环境

JS逆向之某头条jsvmp逻辑层算法分析_第17张图片
0x08 逻辑对比

修改完环境之后,我们再次运行我们本地文件,我们会发现抛出一个错误

JS逆向之某头条jsvmp逻辑层算法分析_第18张图片
这是 case 26 流程所抛出的,所以我们直接在官方打条件断点进行比对

JS逆向之某头条jsvmp逻辑层算法分析_第19张图片
这是官方的结果JS逆向之某头条jsvmp逻辑层算法分析_第20张图片
JS逆向之某头条jsvmp逻辑层算法分析_第21张图片
经过对比,官方 S 中拥有Object 函数而本地却是 undefined 所以我们要给它添加这种属性

window.Object = function (){
    return ["native code"];
}

添加完之后自然有了这种属性

JS逆向之某头条jsvmp逻辑层算法分析_第22张图片

0x09 环境补全

鉴于这个对比过程太过漫长,我这里主要列出几个重要的属性,其他的请自行校验:

1.补齐 canvas 属性

window.HTMLCanvasElement.prototype.getContext = function () {
    return {
        fillText(text, x, y, maxWidth) {
            return ["native code"];
        },
        arc(x, y, radius, startAngle, endAngle, anticlockwise) {
            return ["native code"];
        },
        stroke() {
            return ["native code"]
        },
        getExtension() {
            return ["native code"]
        },
        getParameter() {
            return ["native code"]
        }
    }
}

2.补齐 localStorage(这个会影响 _signature 生成的长度)

(function () {
    let localStorage = {
        "__tea_cache_tokens_2018": "{\"user_unique_id\":\"verify_kut5p98i_BUC0aRbH_QZ5B_4lu4_BQzz_F22NI2z6snKh\",\"web_id\":\"7018918857491351074\",\"timestamp\":1634353853331}",
        "__tea_cache_tokens_24": "{\"web_id\":\"7018898163830441479\",\"ssid\":\"522d279c-ba4b-4c85-a61c-98b7f1a05b71\",\"user_unique_id\":\"7018898163830441479\",\"timestamp\":1634219409505}",
        "__tea_cache_first_24": "1",
        "tt_scid": "z1OscwP3.P6dJQt3WyFHyZ65WZCoGUYARVlW9x0SAYAXAN.mmr9m1v6FlmpA1M98f94d",
        "__tea_cache_first_2018": "1",
        "_byted_param_sw": "lmr6SespEoX9rv9+V/8="
    }
    for (var p in localStorage) {
        window.localStorage.setItem(p, localStorage[p])
    }
})()
  1. 修改 jsdom User-Agent (必须跟请求中保持一致)
resourceLoader = new jsdom.ResourceLoader({
    strictSSL: false,
    userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36",
});
const jsdomConfig = {
    resources: resourceLoader,
    url: "https://www.toutiao.com/",
}

0x10 简洁化处理

如果细心的话会发现,每个分支的代码,只运行了一小部分就跳出了所在的分支,剩下的代码形同虚设般存在。之所以会这样,是因为分支内部的 A 值会影响代码的整体走势,所以我们只需要判断满足 A的代码就可以,其他的都是在尸位素餐

精简之前

case 25:
    (A = x) > 8 ? (C = S[R--],
        S[R] = typeof C) : A > 6 ? S[R] = --S[R] : A > 4 ? S[R -= 1] = S[R][S[R + 1]] : A > 2 && (q = S[R--],
        (A = S[R]).x === G ? A.y >= 1 ? S[R] = K(b, A.c, A.l, [q], A.z, w, null, 1) : (S[R] = K(b, A.c, A.l, [q], A.z, w, null, 0),
            A.y++) : S[R] = A(q));
    break;

精简之后

case 25:
    S[R -= 1] = S[R][S[R + 1]]
    break;

0x11 总结

1.对于这种混淆的代码而言,流程平坦化是首选,但如何进行才是关键
2.代码不仅想知道你的运行环境,它更想知道这种环境是否真的适合它

最终效果

JS逆向之某头条jsvmp逻辑层算法分析_第23张图片
最后声明:

✧✧ 本片文章只用于学习交流,切勿用于商业用途,如果有侵权行为,请与作者联系,本人会在第一时间将其删除。✧✧

欢迎关注:
JS逆向之某头条jsvmp逻辑层算法分析_第24张图片

你可能感兴趣的:(算法,ajax,javascript)