惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Google DeepMind News
Google DeepMind News
博客园_首页
H
Help Net Security
T
Tailwind CSS Blog
S
SegmentFault 最新的问题
GbyAI
GbyAI
Scott Helme
Scott Helme
D
Docker
Hacker News: Ask HN
Hacker News: Ask HN
P
Privacy & Cybersecurity Law Blog
Jina AI
Jina AI
雷峰网
雷峰网
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Spread Privacy
Spread Privacy
G
GRAHAM CLULEY
C
Cisco Blogs
The Hacker News
The Hacker News
F
Full Disclosure
Y
Y Combinator Blog
Blog — PlanetScale
Blog — PlanetScale
Recent Announcements
Recent Announcements
G
Google Developers Blog
量子位
K
Kaspersky official blog
Cisco Talos Blog
Cisco Talos Blog
The Cloudflare Blog
A
About on SuperTechFans
C
Cybersecurity and Infrastructure Security Agency CISA
Last Week in AI
Last Week in AI
博客园 - 三生石上(FineUI控件)
Microsoft Security Blog
Microsoft Security Blog
Martin Fowler
Martin Fowler
T
Tenable Blog
P
Palo Alto Networks Blog
H
Heimdal Security Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
W
WeLiveSecurity
Schneier on Security
Schneier on Security
The Register - Security
The Register - Security
F
Fortinet All Blogs
Stack Overflow Blog
Stack Overflow Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
T
The Blog of Author Tim Ferriss
N
News and Events Feed by Topic
Hugging Face - Blog
Hugging Face - Blog
小众软件
小众软件
V
V2EX
爱范儿
爱范儿

Yesterday17's Blog

2026 新年解密红包 / Melody Flag | Yesterday17's Blog 谈谈 Iori 的设计思路(二):如何实现一个 Showroom 录制工具? | Yesterday17's Blog 谈谈 Iori 的设计思路(一):从 Nico Timeshift 说起 | Yesterday17's Blog Iori Minyami 0.1.0 发布 | Yesterday17's Blog 2025 新年解密红包 / Melody Flag | Yesterday17's Blog 使用 Cloudflare Warp 解决罗森票务的海外登录问题 | Yesterday17's Blog How To Blog 04: The Astro v5 Era | Yesterday17's Blog 谈谈 tokio::select! 的公平性 | Yesterday17's Blog Learning Pingora 05 - Connect with TLS | Yesterday17's Blog Leaving Bytedance | Yesterday17's Blog 大橋彩香 AsiaTour「Reflection」上海公演 个人向记录 & Repo | Yesterday17's Blog Recoving from burnout - What happened? | Yesterday17 Yubikey 重建手册 | Yesterday17's Blog How To Blog 03: Heimus | Yesterday17's Blog 🪧 Blog Migration Accouncement | Yesterday17's Blog Learn Your IDE - VSCode 是如何仅重启插件的? | Yesterday17's Blog How To Blog 02: Astro❤️Password | Yesterday17's Blog How To Blog 01: Why, How, and the Future | Yesterday17's Blog Learning Pingora 04 - Establish L4 Connection | Yesterday17's Blog Learning Pingora 03 - Upstreams and Peers | Yesterday17's Blog Learning Pingora 02 - A Simple HTTP Server | Yesterday17's Blog Learning Pingora 01 - Getting Started | Yesterday17's Blog 2024 新年解密红包 / Melody Flag | Yesterday17's Blog 向新的一年飞驰——记录 2023 | Yesterday17's Blog 「サクラノ刻」对话选摘(2) | Yesterday17's Blog PGP Key Revocation 注销声明 | Yesterday17's Blog 「サクラノ刻」对话选摘(1) | Yesterday17's Blog 2023 新年解密红包 / Melody Flag | Yesterday17's Blog 『蒼の彼方のフォーリズム』通关感想 | Yesterday17's Blog 单显卡直通教程 | Yesterday17's Blog 对博客与笔记的思考 | Yesterday17's Blog Project Anni 之旅(3)自动化 Flutter 应用 CI/CD 上架流程 | Yesterday17's Blog AsobiStage 直接播放链接 | Yesterday17 如何在后分P时代进行投稿——sswa使用详解 | Yesterday17's Blog JSON RPC 与 LSP 协议基础 | Yesterday17's Blog Grajapa Shueisha / BookEnd 加密方式调查 | Yesterday17's Blog 【2022篇+WriteUp】如何再收一个新年红包? | Yesterday17's Blog 如何将良心云的良心功能清理干净 | Yesterday17's Blog 【油猴脚本】bilibili 投稿页面返回旧版+旧版页面强制允许分P上传 | Yesterday17's Blog Cloudr1v1 授权方式分析 | Yesterday17 Typora 1.0.2 逆向实录 | Yesterday17's Blog Project Anni 之旅(2)ValueAfterTable——toml-rs的实现与限制 | Yesterday17's Blog IPv4透明代理+IPv6 Passthrough——树莓派单臂软路由折腾记 | Yesterday17's Blog Chaos; Child 汉化补丁 神秘编码探索 | Yesterday17's Blog 镣铐与舞蹈——个性与共性之迷思 | Yesterday17's Blog Go 学习笔记 02 - 找准 io 之道 | Yesterday17's Blog NAT Slipstreaming v1 原理浅析 | Yesterday17's Blog 绕过「9-nine-」的 CDKEY 验证——KrkrPlugin 正(?)向实录 | Yesterday17's Blog 静流的青春纪念册——「サクラノ刻 -櫻の森の下を歩む-」体验版感言 | Yesterday17's Blog Project Anni 之旅 01 - 从 clap-builder 到 derive | Yesterday17's Blog [Google CTF 2021] CPP WriteUp | Yesterday17's Blog 获取 アソビステージ 的实际播放链接 | Yesterday17's Blog 90 行 Rust 代码实现 AsyncTeeReader | Yesterday17's Blog 或许还算有价值一读的文章列表 | Yesterday17's Blog 从零开始的 Seedbox 之旅 | Yesterday17's Blog [随笔]技术型博客行文迷思(1) | Yesterday17's Blog 浅谈 git fetch 的工作方式 | Yesterday17's Blog 『ソーサレス*アライヴ! ~the World's End Fallen Star~』通关感想" | Yesterday17's Blog Rust std::fmt 格式语法简述 | Yesterday17's Blog 日亚修改居住国的解决方案 | Yesterday17's Blog [Windows/Linux] GC553 的 Switch 完美采集之路 | Yesterday17's Blog 【翻译】Subtyping and Variance / 子类型与变型 | Yesterday17's Blog Berd's Red Envelope 2021 WriteUp | Yesterday17's Blog 【中英对照】ALSA 音频 API 使用教程/A Tutorial on Using the ALSA Audio API | Yesterday17's Blog 从 cue_scanner.l 看 CUE Sheet 的词法单元 | Yesterday17's Blog Postman 历史记录导出的解决方案 | Yesterday17's Blog 《恋爱绮谭 不存在的夏天》通关感想 | Yesterday17's Blog [微机实验/TD-PITE] 微机接口综合实验 | Yesterday17's Blog [微机实验/TD-PITE] 键盘扫描及数码管显示实验 | Yesterday17's Blog [微机实验/TD-PITE] 数码管显示实验 | Yesterday17's Blog Airsonic Advanced+Google Drive+Caddy 部署纪实 | Yesterday17's Blog X-NUCA 2020 - hellowasm 题解 | Yesterday17's Blog [微机实验/TD-PITE] 8251 串行接口实验 | Yesterday17's Blog Node.js child_process.fork 与 env 污染 RCE | Yesterday17's Blog EP.01 「夜の向日葵」 | Yesterday17's Blog [微机实验/TD-PITE] 8254 定时/计数器实验+选做实验 | Yesterday17's Blog [JLU CTF/2020] babywasm WriteUp | Yesterday17's Blog PHP 反序列化与经典利用 | Yesterday17's Blog WebAssembly 逆向简述 | Yesterday17's Blog 『彼女、お借りします』一期完结点评 | Yesterday17's Blog [微机实验/TD-PITE] D/A 转换实验+选做实验 | Yesterday17's Blog [微机实验/TD-PITE] A/D 转换实验+选做实验 | Yesterday17's Blog 开源项目申请 JetBrains Open Source License 简单流程 | Yesterday17's Blog 微软拼音与 JetBrains 搜索快捷键冲突的解决方案 | Yesterday17's Blog [微机实验/TD-PITE] 8259 中断优先级实验+选做实验 | Yesterday17's Blog IFTTT 测试(续) | Yesterday17 IFTTT 测试 | Yesterday17's Blog [微机实验/TD-PITE] 存储器扩展实验+选做实验 | Yesterday17's Blog 新版 GCC 针对 -fdump-translation-unit 的替代方案 | Yesterday17's Blog 一次 HSTS 策略配置的排错之旅 | Yesterday17's Blog YukiNative 踩坑记——Windows 的消息队列 | Yesterday17's Blog 我是我自己——论获取 HTTPS 证书时的验证步骤 | Yesterday17's Blog 【设计文档】对 PUG 的大规模设计修订(1.1) | Yesterday17's Blog GS65 折腾记(2)加装固态,分区,Grub2 引导 Manjaro LiveCD | Yesterday17's Blog 「さくら、もゆ。」的空白字体列表——一次逆向问题定位过程实录 | Yesterday17's Blog GSuite 探索篇(1)使用 Service Account 向 Google Drive 传输文件 | Yesterday17's Blog 『サクラノ詩 -櫻の森の上を舞う-』通关感想 | Yesterday17's Blog 《ATRI -My Dear Moments-》通关感想 | Yesterday17's Blog [工具][VSCode 扩展] AegiKit——方便 Aegisub 使用的工具箱 | Yesterday17's Blog 贝塞尔曲线、字体矢量化与曲线运算 | Yesterday17's Blog
Shinym@s 初探 07 - 入口、脚本加载与 Webpack | Yesterday17's Blog
Yesterday17 · 2020-05-01 · via Yesterday17's Blog

enza 已抛弃本文中的 eval 脚本执行方案,本文仅作为历史文件供考古使用。


ToC

  • 前言
  • 入口
  • ASM.js
  • 入口(真)
  • Webpack
  • 结语

前言

对于 SC,或者说对任何 Webpack 打包的应用而言,最重要的一步就是加载脚本。Webpack 将目录合并成了文件,减小了整体的请求次数的同时也增加了逆向的分析难度。这种难度增加体现在多个维度:一、Webpack 通常生成单个较大文件,对浏览器开发者工具而言难以解析;二、Webpack 将模块以数字的形式加以处理,在阅读时需要将数字重新与模块对应,较为复杂。

当然了,Webpack 的流行也为逆向增加了方便之处。由于打包格式大抵一致,我们可以从任意未混淆的打包中反推出已混淆脚本的内容。并且,由于第三方库的广泛使用,大部分代码实际是开源的,我们可以在 GitHub 上根据相应的特征搜索到对应的结果。

扯了这么多,来看 SC 的吧。SC 的对应文件是 commons.chunk-9a1878f7dac7f1ff26f2.js,后面一串十六进制可能会随着版本更新改变,我们不用去管。这里我们就把它叫做 common.chunk.js 好了。

当时写的时候还是有 commons.chunk 的。时过境迁,自从 2020 年 4 月 30 日之后的版本开始,这一部分的内容已经被整合进了主代码中。这里不得不跨几句 enza,这样做是对的。

以及这次更新引入的新 wasm,都是在安全角度上的再次考量。高山你终于想到这个了(

这句当我没夸,之前版本就有了只是我没注意到(

嘛,万变不离其宗,其实本质都没什么区别。既然 Webpack 移到里面去了,那我们就先看看怎么到里面去呗(

入口

任何程序都要有入口,SC 也不例外,但作为一款网页游戏,如何保护自己就成了一门学问。据说过去 SC 的代码是不设防的,但现在显然不是这样了。

通过前几篇文章我们知道,SC 用到了 WebAssembly,但其实并不止。针对不支持 WASM 的浏览器,SC 也没有放弃治疗,而生选择了 asm.js 这一兼容方案。没错,Mozilla 的方案在 WASM 的大背景下已经只能作为兼容方案存在了(

作为兼容方案,纵使它的效率可能没有 WASM 那么高(对不支持加速的浏览器来说),但这显然是作为入口的不二之选。所以入口其实很简单,就是一个 asm.js 的模块执行的过程。

ASM.js

作为 asm.js,它和 WASM 有着高度的相似性(毕竟都是同一个工具链生成的,至少对于 SC 是如此),而相比之下有一个比较大的区别就是初始内存。WebAssembly 是在程序末,而 asm.js 则是一个 base64 字符串:

没错,就是中间这串
没错,就是中间这串

我们把它 atob,于是发现了熟悉的身影:

并且在最后发现了一些疑似 JS 代码的东西:

我们先来看这个代码,整理一下:

if (

JSON.parse.toString().replace(/\s|\n|\t/g, "") !==

"functionparse(){[nativecode]}"

) {

return;

}

if (

JSON.stringify.toString().replace(/\s|\n|\t/g, "") !==

"functionstringify(){[nativecode]}"

) {

return;

}

if (

window.eval.toString().replace(/\s|\n|\t/g, "") !==

"functioneval(){[nativecode]}"

) {

return;

}

if (

window.decodeURIComponent.toString().replace(/\s|\n|\t/g, "") !==

"functiondecodeURIComponent(){[nativecode]}"

) {

return;

}

if (

window.escape.toString().replace(/\s|\n|\t/g, "") !==

"functionescape(){[nativecode]}"

) {

return;

}

var userAgent = window.navigator.userAgent.toLowerCase();

var isAvailableSourceURLBrowser = false;

if (userAgent.indexOf("edge") !== -1) {

isAvailableSourceURLBrowser = true;

} else if (userAgent.indexOf("chrome") !== -1) {

isAvailableSourceURLBrowser = true;

} else if (userAgent.indexOf("safari") !== -1) {

isAvailableSourceURLBrowser = false;

} else if (userAgent.indexOf("firefox") !== -1) {

isAvailableSourceURLBrowser = true;

} else {

isAvailableSourceURLBrowser = false;

}

var json = {};

json.parse = JSON.parse;

json.stringify = JSON.stringify;

if (isAvailableSourceURLBrowser) {

eval("//# sourceURL=" + window.location.origin + "/%s");

eval(

decodeURIComponent(escape(unescape(encodeURIComponent(src)))) +

"\n" +

"//# sourceURL=" +

window.location.origin +

"/%s"

);

eval("//# sourceURL=" + window.location.origin + "/%s");

} else {

eval(decodeURIComponent(escape(unescape(encodeURIComponent(src)))));

}

window.JSON = json;

看到 48 行,我们不难发现:主程序代码执行是通过 eval 实现的。这也就是我针对这次 commons 失效的解决方案:劫持 eval 修改脚本内容实现内容输出。

接下来就是最重要的问题了:eval 的脚本哪里来?

入口(真)

事实上,在上面这串代码的正后方,就有一个有趣的东西:

事实也证明,这的确就是我们想要的文件。那这个文件是怎么加密的呢?还记得之前提到的完全一致的 key 吗?没错,这个文件就直接使用 decryptResponse 解密就行了。

事实上到了这里已经可以停下来了,但我们还有不明白的东西:这个字符串是怎么来的?

根据之前 encryptPath 的经验以及字符串的长度,我们判断出这是 sha256;那文件名是什么呢?要不要加 /assets 呢?这都是需要我们去尝试的。

经过试验,结果出炉了:

至此,我们拿到了 SC 的实际源文件。

Webpack

经过了这次更新,SC 的模块系统已经完全可以说是滴水不漏了。(改代码是犯规的,这一点必须指出(

和上一个版本相比,这个版本打包的特点就是可读性更强了。我们来看:

/******/ !(function (e) {

function t(n) {

if (r[n]) return r[n].exports;

var o = (r[n] = { i: n, l: !1, exports: {} });

return e[n].call(o.exports, o, o.exports, t), (o.l = !0), o.exports;

} // webpackBootstrap

/******/

var n = window.primJsp;

window.primJsp = function (t, r, i) {

for (var a, s, u = 0, l = []; u < t.length; u++)

(s = t[u]), o[s] && l.push(o[s][0]), (o[s] = 0);

for (a in r) Object.prototype.hasOwnProperty.call(r, a) && (e[a] = r[a]);

for (n && n(t, r, i); l.length; ) l.shift()();

};

这里有两个函数:t(n)primJsp(t, r, i)。前者相当于 require,或者准确地说,__webpack_require__;而后者,则是负责向整个 Webpack 中增加新模块的。

原本的代码中 primJsp 本身就有返回值,这次修改的结果是把返回值去掉了。安全性大大增加不说,还让这个函数的实现更漂亮了。(当然了,估计是自动生成的,enza 估计没想到这个((

接下来是一大堆 sha256 值,作用是定位脚本路径和确保脚本不被篡改,我们就跳过了。

然后是这个函数:t.e

(t.e = function (e) {

function n() {

(u.onerror = u.onload = null), clearTimeout(l);

var t = o[e];

0 !== t &&

(t && t[1](new Error("Loading chunk " + e + " failed.")),

(o[e] = void 0));

}

var r = o[e];

if (0 === r)

return new Promise(function (e) {

e();

});

if (r) return r[2];

var a = new Promise(function (t, n) {

r = o[e] = [t, n];

});

r[2] = a;

var s = document.getElementsByTagName("head")[0],

u = document.createElement("script");

(u.type = "text/javascript"),

(u.charset = "utf-8"),

(u.async = !0),

(u.timeout = 12e4),

(u.crossOrigin = "anonymous"),

t.nc && u.setAttribute("nonce", t.nc),

(u.src =

t.p +

"" +

{/* ... */}[e] +

".chunk.js");

var l = setTimeout(n, 12e4);

return (

(u.onerror = u.onload = n),

(u.integrity = i[e]),

(u.crossOrigin = "anonymous"),

s.appendChild(u),

a

);

}),

这里我们很明显能看出是创建 <script> 用的。这里指定了一系列属性以加载脚本,看看就好(

(t.m = e),

(t.c = r),

(t.d = function (e, n, r) {

t.o(e, n) ||

Object.defineProperty(e, n, {

configurable: !1,

enumerable: !0,

get: r,

});

}),

(t.n = function (e) {

var n =

e && e.__esModule

? function () {

return e.default;

}

: function () {

return e;

};

return t.d(n, "a", n), n;

}),

(t.o = function (e, t) {

return Object.prototype.hasOwnProperty.call(e, t);

}),

(t.p = "/"),

(t.oe = function (e) {

throw e;

}),

t((t.s = 535));

最后是一堆算是常规内容的东西,也没什么好说的。最后设置了起始模块为 535 号,整个 Webpack 的初始化就结束了。

结语

事出偶然,这篇原本是计划在资源获取之前写的,但机缘巧合之下留到了今天,留到了这个 commons 直接被合并的日子。

想办法注入使汉化脚本恢复的过程确实有点让人头秃,期间试了各种东西,加深了我对 JS 作用域链的理解(笑)。不过也正是这一个多小时的思维碰撞(指一个不行碰撞另一个不行),才有了劫持 eval 这个想法的出现。这个想法本身也很简陋,又是因为不知为什么想到的 Proxy,才使得现在的代码能够做到如此优雅。整个过程堪比一道 CTF Web 题:

嘛,就是这样(