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

推荐订阅源

SecWiki News
SecWiki News
I
InfoQ
The Cloudflare Blog
人人都是产品经理
人人都是产品经理
博客园 - Franky
T
Tailwind CSS Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
量子位
博客园_首页
罗磊的独立博客
V
V2EX
李成银的技术随笔
大猫的无限游戏
大猫的无限游戏
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
True Tiger Recordings
Vercel News
Vercel News
Cyberwarzone
Cyberwarzone
Cisco Talos Blog
Cisco Talos Blog
F
Fox-IT International blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
M
Microsoft Research Blog - Microsoft Research
Know Your Adversary
Know Your Adversary
爱范儿
爱范儿
The Register - Security
The Register - Security
G
Google Developers Blog
The Hacker News
The Hacker News
Malwarebytes
Malwarebytes
S
Securelist
博客园 - 三生石上(FineUI控件)
Jina AI
Jina AI
T
Threat Research - Cisco Blogs
T
The Exploit Database - CXSecurity.com
S
SegmentFault 最新的问题
博客园 - 叶小钗
F
Fortinet All Blogs
Apple Machine Learning Research
Apple Machine Learning Research
宝玉的分享
宝玉的分享
博客园 - 聂微东
T
Threatpost
博客园 - 【当耐特】
D
Docker
P
Privacy & Cybersecurity Law Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
G
GRAHAM CLULEY
V
Visual Studio Blog
C
Cisco Blogs
IT之家
IT之家
S
Security Archives - TechRepublic
Latest news
Latest news
阮一峰的网络日志
阮一峰的网络日志

Yesterday17's Blog

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

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


ToC

前言

对于 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 题:

嘛,就是这样(