逆向工程 TikTok 混淆的 VM
Navigation Menu
LukasOgunfeitimi / **TikTok-ReverseEngineering ** Public
- Notifications You must be signed in to change notification settings
- Fork 14
- Star 99
99 stars 14 forks Branches Tags Activity
LukasOgunfeitimi/TikTok-ReverseEngineering
Folders and files
Name | Name | Last commit message | Last commit date ---|---|---|--- decompiler | decompiler | | deobfVersions | deobfVersions | | deobfuscation | deobfuscation | | output | output | | README.md | README.md | | decryptBytecode.js | decryptBytecode.js | | handleBytecode.js | handleBytecode.js | | injector.js | injector.js | | latestDeobf.js | latestDeobf.js | | vm.js | vm.js | |
TikTok VM 逆向工程 (webmssdk.js)
这个项目旨在逆向工程 TikTok 的 Virtual Machine (VM)。
概述
TikTok 使用自定义的 virtual machine (VM) 作为其混淆和安全层的一部分。本项目包含以下工具:
- 反混淆包含 virtual machine 的
webmssdk.js
。 - 反编译 TikTok 的 virtual machine 指令为可读的形式。
- 脚本注入 使用反混淆后的 VM 替换
webmssdk.js
injector。 - 签名 URL 生成签名的 URL,可以用于执行基于 auth 的请求,例如发布评论。
反混淆
查看 webmssdk.js,你会看到一个被严重混淆的文件。 混淆 JavaScript 的主要方法是利用括号表示法,它允许你使用另一个变量索引变量。
所以当你看到这样的代码时:
// Line 3391 of ./deobfVersions/raw.js
r[Gb[301]](Gb[57], e))
你完全不知道它在索引什么。
这种方法的每次使用都使用一个数组 Gb
,定义为:
var Gb = ["ydTGHdFNV", "sNxpGNHMrpLV", "xyrNMLEN Fpp rpMu", "ydWyNe", ...].map(function(a) {
return a.split("").map(function(c) {
return "LsfVNxutyOcrEMpYAGdFHneaUKRXSgoJDbhqICzPZklivTmWBwQj".indexOf(c) == -1 ? c : "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"["LsfVNxutyOcrEMpYAGdFHneaUKRXSgoJDbhqICzPZklivTmWBwQj".indexOf(c)]
}).join("")
});
正如你所看到的,我们甚至无法读取它,因为它都使用字符串 "LsfVNxutyOcrEMpYAGdFHneaUKRXSgoJDbhqICzPZklivTmWBwQj"
进行了编码。
因为这段代码会立即执行,我们可以简单地获取这段代码片段并在任何控制台中运行,并检索:
[
"isTrusted",
"beforeunload",
"filename too long",
"isView",
...
]
我们现在可以看到这些字符串中的每一个,因此我们可以使用 RegEx 遍历脚本并将数组的所有用法替换掉,如这里所示。它还会将括号表示法转换回可读的点表示法。 之后,我们就得到了 webmssdk1。 上面的例子现在看起来像这样:
r.addEventListener("abort", e),
好多了。
另一种重要的混淆方法用于伪装函数调用。
每个函数都在一个数组 Ab
中定义。
var Ab = [function(e) {
return "[object Array]" === Object.prototype.toString.call(e)
}
, function(e) {
return e && e.__esModule && Object.prototype.hasOwnProperty.call(e, "default") ? e.default : e
}
, function() {
var Ga;
Ga = [0, 1],
(je = !Ga[0],
le && (setTimeout(function() {
document.dispatchEvent(new Event(pe))
}, Ga[1]),
document.removeEventListener("DOMContentLoaded", Ab[40]),
document.removeEventListener("readystatechange", Ab[75])))
}
...]
它通过调用 Ab[index](args)
来使用,例如:
Ab[31](f[e], t, n, i)
当使用常见的 IDE 时,如果我们点击这个函数,它只会将我们带到数组的开头,使得跟踪哪个函数调用了什么函数变得困难。 我们可以通过以下方式使其可读:
- 获取数组
- 将每个函数元素替换为它自己的标准函数,并将其称为
function Abindex(args)
- 将每次调用
Ab[index](args)
替换为Abindex(args)
我们可以通过使用脚本的 AST 形式,通过 bapel
来实现这一点,如这里所示。
这给了我们这个。
脚本的 Virtual Machine 部分,特别是在执行 bytecode 时,是一个嵌套的 if else
语句,如这里所示。
它实际上只是一个普通的 switch case
,但伪装得很好。 在手动完成了一些 case
之后,AI 能够帮助我完成其余的工作。 这给了我这个,对于 bytecode VM 来说,它看起来非常标准。
在稍后调试 Virtual Machine 并查看它使用的哪些函数时,我能够知道它在做什么并更改了一些变量名称。
经过所有这些以及更多小的混淆技巧之后,这里是该文件的最新版本。
解密 Bytecode
在完全反混淆文件后,弄清楚其功能变得容易得多,我很容易找到 VM 是如何初始化的 这里。 bytecode 存储为一个长字符串,该字符串已全部使用字符串中的密钥进行 XOR 运算。
// Line 3046 of latestDeobf.js
// Getting XOR key
for (var t = atob(payload), r = 0, n = 4; n < 8; ++n) r += t.charCodeAt(n);
// Decryping bytecode
unZip(Uint8Array.from(t.slice(8), XOR, r % 256), { i: 2 }, t && t.out, t && t.dictionary),
// Extracting strings, functions and metadata for each function
for (var n = leb128(t), o = 0; o < n; ++o) strings.push(Ab27(t));
i = leb128(t);
for (o = 0; o < i; ++o) {
for (var argsLength = leb128(t), isStrictMode = Boolean(leb128(t)), exceptionHandlers = new Array(), p = leb128(t), m = 0; m < p; ++m) exceptionHandlers.push([leb128(t), leb128(t), leb128(t), leb128(t)]);
for (var instructions = new Array(), h = leb128(t), v = 0; v < h; ++v) instructions.push(leb128(t));
instructionSets.push([instructions, argsLength, isStrictMode, exceptionHandlers]);
}
注意:该字符串已进行 GZip 压缩,并且每个值都经过 leb128
编码以进行压缩
Virtual Machine 反编译
TikTok 正在使用一个成熟的 bytecode VM,如果你浏览它,它支持作用域、嵌套函数和异常处理。 这不是一个典型的 VM,表明它绝对是复杂的。 为了能够编写一种反编译形式,我简单地遍历了每种情况并为每种情况编写了适当的代码,以及任何跳转到循环的另一个位置的情况,例如:
case 2:
var a = instructions[index++];
stack[pointer] ? --pointer : index += a;
break;
我会简单地阻止它这样做:
case 2:
var a = instructions[index++];
//stack[pointer] ? --pointer : index += a;
addCode(`// if (!v${pointer}) skip ${a} to ${index + a}`, byteCodePos)
break;
在为所有情况执行此操作后,我将每个文件转储到这里。 它并不完全可读,但你应该能够大致了解每个函数的作用,例如VM223,它正在生成随机字符。
调试
由于这是一个在 Web 上执行的 JavaScript 文件,因此实际上可以将普通的 webmssdk.js
替换为反混淆的文件并正常使用 TikTok。
这可以通过使用两个浏览器扩展来实现,即 Tampermonkey 用于执行自定义代码,以及 CSP 用于禁用 CSP,这样我就可以从被阻止的来源获取文件。 这是为了我可以将 latestDeobf.js
放在我自己的文件服务器中,并让它每次都被获取,这样我就可以轻松地编辑文件并让更改每次刷新时生效。 这使得在反转函数时进行调试变得更加容易。
该脚本可以在这里找到。
请求
现在我们已经反混淆了文件并反编译了 VM,我们可以开始逆向任何我们想要的函数,并弄清楚它在做什么。 当你向服务器发出请求时,它通常包含 3 个额外的标头。
Header | Description
---|---
msToken
| 由服务器发送,并在每次请求时重新颁发。
X-Bogus
| 由 webmssdk.js
基于请求生成。
_signature
| 由 webmssdk.js
基于请求生成。
当发出不需要身份验证的请求(例如查询用户)时。 只需要生成 X-Bogus
,这可以使用 window.frontierSign
完成。 不需要 _signature
,并且可以使用任何 msToken
。
这个流行的 API 允许你发出这些请求。 它使用一个名为 playwright 的 webdriver 库,它只是设置一个浏览器实例,因此它可以轻松地调用 window.frontierSign
。
当涉及到发出基于身份验证的请求(例如发布评论)时,需要 _signature
并且它不会暴露给 window
。
签名器
每个请求的初始函数调用是 VM86,然后调用:
VM113 用于 X-bogus
VM189 用于 _signature
我能够编写 signer,它成功地对 URL 进行签名。 这是一个发布评论并使用私有浏览器检查它以确保成功的演示。
PostCommentTest1.mp4
注意:还有一些机器人保护方法,例如鼠标跟踪(VM120) 和环境检查(VM265) 在 VM86 中,但它是一个完全客户端的检查,不与服务器通信,因此在生成签名时可以忽略它。
额外信息
- 注意: TikTok VM 正在随着新版本的发布而不断变化。 主要算法很可能会改变,并且需要反编译新的 VM。
关于
没有提供描述、网站或主题。