抖音协议算法

请原谅我起了一个这样的标题博取你的眼球。与其说叫通杀,不如说按照一定的套路一定能纯算 jsvmp,我测试了部分网站 tx,x 红薯...都成功过掉了,其中最麻烦的当属某音的 a-bous,一个星期左右搞定。现在,24 年 5 月 2 日,作者也就是我,认为只要耐心一点可以纯算市面上所有的 jsvmp,以后,不好说,其实本文只是最初版本,我认为还有很大可扩展的空间,允许我先卖个关子。遗憾的是没遇到我想象中更恶心的 jsvmp,没有机会一展拳脚。以上,为了激发你的阅读兴趣,被迫装 B。

以下我将以某音的 a-bous 为例讲述如何去做,分为三个部分:流程概述,实操 a-bous,未来工作。

vmp 是什么,我就不再赘述了,相信在你动手做的时候会有自己的体会。鼓励大家动手做一遍。

一、流程概述

1.观察代码结构,观察至关重要,在第一步你需要看懂 jsvmp 的调用流程,找到“内存”数组、“指令集”数组,这些名词现在看不懂也没关系,在纯算 a-bous 时会举例说明。

2.代理 jsvmp 的“内存”数组,因为 jsvmp 是用数组模仿的汇编中的“内存”,所以找到 jsvmp 的主循环后,在“内存”数组定义下面,将其代理,然后替换网页代码,刷新载入代码。

3.copy 代理文本信息在文本编辑器中查看,找到需要逆向的密文,搜索第一次出现的位置。查看上面行代理日志初步观察生成的规律,特别要说明的是,你花费多少时间就取决于这个初步观察了,举个例子就是,有些密文的生成所带出的代理日志是有一定规律的,三个一组或四个一组;或者有些算法直接就出现特征值等等,要做的心中有数,如果只是盲目的进行第四步后续会浪费大量时间,因小失大。

4.根据代理日志的特征定位到生成密文的起点,修改代理代码,重新替换网页代码,对照代理日志,确定密文加密地点。

5.单步调试、跳出代理函数...“指令集”数组中参与运算的数据当作固定值,运算符、未知数据做好记录,对照 3 中的猜测,结合代理日志,得到密文初步生成规律,不必全部跟完,先自己构建一个算法试试能不能对上,对上就证明密文初步生成规律是正确的,反之则要多跟几步,看看哪里引入额外的运算或特殊的处理,得出算法。

6.逆向、逆向,逆着破解密文,在 5 的过程中记录的未知数据大概率是另一层加密算法,需要你结合之前的代理日志找到对应的数据,可能值已经变了但日志中对应的位置不会改变,长度也大概率不会变动。就这样“递归”下去,当所有的未知数据都逆向完毕,只剩下了些环境值、之前返回的参数、网页中的固定值、“指令集”数组中的值时,宣告纯算成功!

二、实操 a-bous

嗯,重头捋一遍某音的 a-bous 的生成流程,让我删掉本地替换,哎,好麻烦。
首先,先找一下数据接口。哦,对了,案例是评论接口,a-bous 长度为 164,没有登录。
直接搜索大法,找到,如下:


还有一件事,建议用谷歌浏览器,edge 可能会卡。

然后就是,最好找到进入 vmp 文件的最开始,以及 vmp 加密的结束,因为存在 vmp 的文件会被重复加密别的地方,这样简单筛选下,减少一些代理日志。

这个是 xhr 请求,二话不说,先来个 xhr 断点,作“尾”。什么,你问我为什么不跟栈,随你便。


发现 this 中已经有 a-bous 的值了,然后向上跟栈,最后确定进入 vmp 文件的位置,也就是“头”,如下:


但是你再去看 this 中的_url 时,发现什么鬼,这个作者绝对业余,这加密起点,所谓的“头”已经有了 a-bous,那还加密个集贸。稍安勿躁,听我解释,没有翻车,浏览器调试有个特性,跟栈显示的值会被你断点处的变量值覆盖。如果是不一样的变量名那没什么问题,如果一样,那跟栈的值就会显示成断点处的值,所以这里的 this 被之前 xhr 断点的 this 覆盖了。

好了解释完,这一点,再来说说该怎么找这种值,有个技巧,也就是“头”和“尾”,一句话就是:找两个点,“头”:加密开始前,“尾”:加密结束后,逼近加密的位置。这个需要刻意练习一下,有经验的高手,看到这种组包就会习惯性下个断点。靠,一不留神又跑题了。


言归正传,在这里下一个条件断点,因为这里是一个封装的发包函数,普通断点会一直断。


重新断下来发现,a-bous 已经没有了。


单步会进入存放 vmp 的文件了。


把代码全部拷贝下来,分析一下,这段代码的执行流程。


我记得我当时闲着无聊数了一下,它有多少带 vmp 的函数,好像是 29 个,忘记了。不过不用慌,加密只用到了其中的 4 个。


接下来就很简单了,无脑上套路!

先通过搜索定位一下最开始的 vmp:


进入流程概述-1:

i 是“指令集”数组,p 是“内存”数组,确定的方式有两种:一是分析这个函数,看主循环那个数组使用的多,怎么使用的,vmp 的解密函数解密出“指令集”数组:


二是在主循环里下断点,看看作用域种,各个数组的内容,高效,直观。


但某音特殊的地方在于,它每个 vmp 搞了两块内存,这里的 g 也是“内存”数组。

然后是流程概述-2:

代理的脚本如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

window.luan_text = "";

window.luan_open = false;

function luan_proxy(arr, name) {

  // arr 内存

  // name 名字

  // 获取数据类型

  function get_value_type(value) {

    // 'undefined', 'null', 'boolean', 'string', 'number', 'symbol', 'array', 'object', 'function'

    if (Array.isArray(value)) {

      return "array";

    }

    if (value == null) {

      return "null";

    }

    return typeof value;

  }

  // 打印函数

  function luan_print(obj) {

    let type = get_value_type(obj);

    if (["undefined""null""boolean""string""number"].includes(type)) {

      return obj;

    else if (obj[Symbol.toStringTag]) {

      return obj[Symbol.toStringTag];

    else if (type == "function") {

      return `function ${obj.name}`;

    else if (type == "object" || type == "array") {

      // JSON.stringify 会递归处理

      let temp = JSON.stringify(obj, function (k, v) {

        if (v && v[Symbol.toStringTag]) {

          return v[Symbol.toStringTag];

        else if (v && typeof v == "function") {

          return `function ${v.name}`;

        else {

          return v;

        }

      });

      return temp;

    else if (type == "symbol") {

      return obj.toString();

    else {

      // 未预料到的情况

      debugger;

    }

  }

  // 句柄

  let handle = {

    get(target, property, receiver) {

      let result;

      result = Reflect.get(target, property, receiver);

      try {

        if (window.luan_open) {

          let content = luan_print(target);

          window.luan_text += `${name}|get| 下标: ${property.toString()} 内容: ${content}\r\n`;

        }

      catch (e) {

        debugger;

      }

      return result;

    },

    set(target, property, value, receiver) {

      let result;

      result = Reflect.set(target, property, value, receiver);

      try {

        if (window.luan_open) {

          let content = luan_print(target);

          window.luan_text += `${name}|set| 下标: ${property.toString()} 内容: ${content}\r\n`;

        }

      catch (e) {

        debugger;

      }

      return result;

    },

  };

  return new Proxy(arr, handle);

//………………

p = luan_proxy(p, "p1");

封装了一下,过这些 vmp 站的时候,踩过不少坑,也逐渐完善了这个脚本,用着还可以,其实还有新的想法去进一步改进代理脚本,会在未来工作种会展示。

使用方法也很简单,下面将仔细介绍下:

前面一段扔到 vmp 文件最开始:


后面一段放在“内存”数组定义处:


因为有多个 vmp 嵌套,所以给“内存”数组搞了一个名字。然后替换代码重新载入。然后细节的地方来了,这个代理日志的存储是有一个开关的,这也就是为什么要找到“头”的一个重要原因。有了一个开关不会运行速度。来到头部打开开关。


然后流程概述-3:

来到“尾”,copy 代理日志:


第一个 vmp 很短只有 400 多行日志。


直接到网站的“尾”,找到 a-bous 的值,在代理日志中搜索:


url 编码应该不用我在提醒大家了。

找到第一项


我擦勒,啥子情况,突然出现??!是的,这种情况就预示着,这个 vmp 对于 a-bous 的加密来说,屁用没有,所以直接进入第四步了。

流程概述-4:

接下来我们要考虑的是如何定位到这一行,你可能到这还是不太懂,接下来要仔细看了。


搜索这一小段,发现只出现了两次,并且,a-bous 的长度固定为 164(长度为 164 可以直接在“头”调用进入函数测试,不是重点,这里就不在演示)。在 set 句柄中,写判断函数:


重新替换代码,再次载入,找到一个函数调用。


多次运行:


跟进去是一个新的 vmp


老规矩,重复定位:


挂上代理:


这时可以把第一个 vmp 的代理取消掉了。

重置一下 set 句柄:


重新获取代理日志:


很明显最后一行就是我们要的 a-bous,全局搜索一下:


我的发,什么鬼,又是突然出现!好吧,去 set 句柄里写定位。看到这里有一些老手,就要忍不住要拿上边的乱码、数组试一试了,你问我为什么不试试,因为我早就试过了,没这么简单。

找到第三个 vmp:


再次获取代理日志:


好家伙,这下多起来了,不过有搜索在,多少行都无所谓的。

小技巧之只搜索一部分:


这就是 a-bous 的第一层加密了,接下来我们逐渐删除加密字符串的长度,定位到开始生成的位置,为什么要这样做?这样可以让每一行的日志变短一点,方便我们观察,可以说这一步的观察是唯一体现技术的地方,敏锐的洞察,出色的判断,将决定你纯算 jsvmp 花费的时间与精力,老手与小白的差距也大概率在此拉开。不过,不用慌,什么也观察不出来,就按照套路走也可以做出来的,我保证。

OK,差不多定位到这里,可以开始观察了:这加密字符串是怎么一个个蹦出来的,几个是一轮回,遍历什么,取什么......大概心里有数。


下面我将以 a-bous 第一层加密为例,详细解释一下:
可疑点 1:


测试:


"Dkdpgh2ZmsQB80/MfvV36XI1R45-WUAlEixNLwoqYTOPuzKFjJnry79HbGcaStCe"这个东西,搜索一下,贯穿了整个日志,追溯一下第一项:


复制出来看一看:


看知道这种东西,大概率是一个固定值,先不管它,当一个未知值处理。也就是说,我们现在关心的就是 20 是哪来的,简单找一下。


这里代理日志每行都很短的好处就体现出来了,如果你的显示屏足够长的话,当我没说。

20 大概率是个什么运算,向上翻一翻:


好的,看不懂。好了,到这里我们的观察-1:“来源观察”就可以结束了,老手可能到这里又想试试这些乱码啊、数据啊,这次,我会告诉你这是在浪费时间。

然后让我们进入观察-2:“分组观察”,这个没法截图了,我大概描述下,你需要向下看一看代理日志,看看加密字符串每个字符的生成是不是大概都一样,可能每个字符的运算逻辑都一样,只不过换了数据;也可能四个一组,一组中的四个字符的运算逻辑完全不一样,但是每组的运算逻辑相同。反应到代理日志上最直观的就是代理日志的行数、每行的长度是否一样,这样大概估计一个分组,估计错了,没有影响,估计对了,大大简化纯算的流程。

当然,别的 jsvmp 可能还好,只有一个 vmp,没有几层加密,但像某音的 a-bous 这种,套了很多层 vmp,加密了很多次,每次都要观察,确实有点费眼睛,在未来工作中会给出解决办法,稍后再谈。

好了,不管你观察没观察出来它的分组,我这里都默认你没观察出来,没观察出来分组的话,就先按“每个字符的运算逻辑都一样,只不过换了数据”这种来,到时候在网站中看着改,现在再一次进入流程概述-4,找代理日志的特征,在写 set 句柄中写判断,当然,你乐意在 get 句柄中写也行。

这里有个小技巧:
我们的目的是 debugger 到一个字符开始生成之前,你当然可以看日志总结一下规律,但更方便的是找到一个字符刚生成完的地方,然后 debugger 进入就可以了。如下:


set 句柄中的判断,如下:


替换代码,重新载入。

进入流程概述-5:

在网站开始单步跟算法,记得“指令集”数组中的值如果参与运算了就当作固定值,未知的值要记录下来,每出现一个值做好笔记,否则很容易前功尽弃!指令集”数组中的值如果参与运算了就当作固定值,为什么呢?这个可以做一个思考题,自己想一想,其实你做的多了,就很容易想到了。

单步的时候小心一点,你不需要跟很多,一般来说,一层加密,跟一两个字符就能逆向了,稍后会演示,但你每一步,都要想一想它在做什么,切记不能无脑跟。

“指令集”数组算出值 258048:


现在可以去之前的代理日志中看看 258048 是不是总是有,开搜:


是不是很想说一句,臭小子让我逮到了,别急,点“查找下一个”的按钮,更惊喜的来了:


每加密四个字符出现一次,四个一组呗。别急,看看它用来做什么:


第一个运算:7835561 & 258048 = 229376

我知道你想问 7835561 是哪来的,呃,忘了截图了,7835561 是在 v 也就是第二个“内存”数组中取出来的的值,当未知值处理,先不用去搜索了,未知值做好笔记就可以了,固定值需要去以往的代理日志中搜索确定一下。

它又将结果存入 p“内存”数组中:


“指令集”数组算出值 12:


这个值估计一搜一大把,在笔记中做好记录就行了,看看 12 用来做什么,当然你也可以去代理日志中看看 258048 的下面是不是都跟着一个 12,来确定 12 是个固定且有用的值,不过我们现在已经有 80%机率确定第一层加密是四个一组,迫切的需要直到,它是怎么算出来的,所以就记录下 12,接着跟:


跟进来看一看:


第二个运算:229376 >> 12 = 56

在跟就到了这里:


哦吼,字符串变成'ckdp1h4ZKsUB80/Mfvw36XIgR25+WQAlEi7NLboqYTOPuzmFjJnryx9HVGDaStCe'了,什么情况,实际上在我们全局搜索 258048 的时候,聪明的读者可能就已经看出端倪了,让我们看来看一下这张图:


注意左边的总览图,全局都在飘橘黄色,我们跟过去看看:


最开始的代理日志 258048 前面还真是'ckdp1h4ZKsUB80/Mfvw36XIgR25+WQAlEi7NLboqYTOPuzmFjJnryx9HVGDaStCe'

到了后面才是"Dkdpgh2ZmsQB80/MfvV36XI1R45-WUAlEixNLwoqYTOPuzKFjJnry79HbGcaStCe"

我们跑到'ckdp1h4ZKsUB80/Mfvw36XIgR25+WQAlEi7NLboqYTOPuzmFjJnryx9HVGDaStCe'最后生成的位置,看看它造了一个什么东西出来:


一顿搜索,发现生成了这么一个玩意,看看长度:

别晕,想想"Dkdpgh2ZmsQB80/MfvV36XI1R45-WUAlEixNLwoqYTOPuzKFjJnry79HbGcaStCe"是怎么来的,我们搜“尾”的 a-bous 的值逆过来的,而'ckdp1h4ZKsUB80/Mfvw36XIgR25+WQAlEi7NLboqYTOPuzmFjJnryx9HVGDaStCe'呢,它是出现在代理日志的开始。好嘛,找错位置了!

其实这个长度为 148 的字符串在后续的加密中会用到,这里我们解的是第一层,就不再展开,算是留给你们的伏笔。

现在,让我们点击网站上的小三角,断到正确的位置,也就是说我们要改写一下断点的判断,有"Dkdpgh2ZmsQB80/MfvV36XI1R45-WUAlEixNLwoqYTOPuzKFjJnry79HbGcaStCe"的部分才是我们的目标。

观察代理日志:


在 get 句柄中写下判断:


此时,我们也基本可以确定,"Dkdpgh2ZmsQB80/MfvV36XI1R45-WUAlEixNLwoqYTOPuzKFjJnry79HbGcaStCe"就是固定值了。其实,在解密出的 o 中可以确定,道理与 i“指令集”数组相同:


重新断下来之后,继续单步跟。

“指令集”数组算出值 4032:


接下来到了:


又来!?

到了这里,我想我们应该去之前的代理日志中看看:


同 258048 一样,也是四个加密字符用一次,每个 4032 之后也都会有一个固定数 6,也同样会在 v(另一个“内存”数组)中取一个值。那么类比 258048,上图中的下一次运算应该是 256 >> 6 = 4,对上了!

我们找出所有的像 4032 与 6,这样的数:

也就是说 v 中取一个值是我们下一个目标,OK,现在让我们去代理日志中看看 v 中的值是从哪里来的,随便在代理日志中找一个 v 中取出来的值,全局搜索,找到这个数第一次生成的位置:


你可能会说:等等,你怎么知道那个值是从 v 中取出来的,如果你单步跟一跟的话就不会问这样的问题了。v 中取出来的值必然会和 258048 这样的值做运算,并且另一个重大发现是一组只用到了一个 v 中的值,等等等,让我们好好捋一捋,也就是说,a-bous 的第一层加密是:四个为一组,每组的运算流程相同,但每组需要一个不同的值来运算。

恭喜你想到这里,就已经破解了 a-bous 的第一层加密,接下来只要照葫芦画瓢,也就是流程概述-6,以纯算的王者之资通杀所有的 jsvmp!

因为本文的重点在于介绍纯算 jsvmp 的套路,而不是过掉 a-bous 这么一个小小的参数,所以花了很多篇幅与笔墨尽可能地去讲清这个流程的所有细节。与其说我带大家过一遍 a-bous 算法的全部逆向过程,不如大家自己动手,尝试一下。

没错,本文就写到这里了,因为实在很长了,相信能读到这里的人都会有点意犹未尽的感觉,我的建议是 have a try!

开始最好不要挑战 a-bous 这种很有难度的纯算,可以先试试 xx 音乐,等有了自己体会后,相信 jsvmp 是拦不住你的。

三、未来工作

1.其实代理脚本还可以加上“指令”也就是这里的 m:


一是在代理日志中可以有额外的信息用来筛选;二是:“指令”m 本身就蕴含着指令信息,可以帮助我们更好的分析,但我还没想好要怎么改,如果你有好的想法欢迎和我讨论。

2.相信大家实操一下就会觉得,在观察代理日志的时候会有些费眼睛,其实可以训练一个小型的神经网络来代替,输入一段加密字符串的日志,这个其实可以用但问题是用 transformer 训练的话,每一行都进行 tokenization,内存实在吃不消,其实也没必要这样做,有点杀鸡用牛刀的感觉。我现在有两个思路:一是取一些关键信息如:每行日志的长度、每个加密字符隔了多少行,get 还是 set 句柄,下标为多少,这样的边角信息去做,因为我们也是通过这些观察的;二是微调开源的 Bret,用人家已经训练好的模型抽特征,最后加一个分类头。

如果我有时间的话会继续玩一玩,或者出现更恶心的 jsvmp。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值