各位靓仔靓女,晚上好!我是你们今晚的JS逆向工程讲师,咱们今天来聊聊一个有点意思的话题——JS逆向工程:如何从打包文件中还原源码和逻辑。
别害怕,虽然听起来高大上,但咱们尽量用大白话把它讲明白,保证你听完之后,至少能唬住隔壁的小白。
开场白:为什么要搞JS逆向?
首先,咱们得搞清楚,为啥要费劲巴拉地去还原别人的JS代码? 难道是闲的没事干吗? 当然不是!
- 学习和研究: 优秀的开源项目,或者一些很有意思的网站,它们的前端代码往往蕴含着很多巧妙的设计和精妙的算法。逆向它们的代码,可以帮助我们学习到很多实用的技巧和思路。
- 安全审计: 通过逆向分析,我们可以发现潜在的安全漏洞,比如代码混淆强度不够导致敏感信息泄露,或者存在逻辑缺陷导致被恶意利用。
- 破解和修改: 嗯… 这个我就不多说了,你懂的。但是记住,咱们要遵守法律法规,不要搞非法的事情。
- 找回丢失的源码: 有时候,我们会不小心把源码搞丢了,这时候如果只有打包后的代码,逆向工程就成了救命稻草。
第一部分:认识你的敌人——JS打包工具
在开始逆向之前,咱们得先了解一下,JS代码是怎么被打包成一坨坨的。 常见的JS打包工具有:
- Webpack: 功能强大,配置灵活,是目前最流行的打包工具之一。
- Rollup: 注重ES模块的打包,适合打包JS库。
- Parcel: 零配置,上手简单,适合小型项目。
- Browserify: 比较老牌的打包工具,现在用得少了。
这些打包工具的作用,简单来说就是把我们写的JS代码,以及各种依赖的模块,合并成一个或多个文件,方便浏览器加载和执行。
打包过程中,通常会进行一些优化操作,比如:
- 代码压缩(Minification): 去掉空格、注释,缩短变量名,让代码体积更小。
- 代码混淆(Obfuscation): 把代码变得难以阅读和理解,增加逆向的难度。
- 代码转换(Transpilation): 把高版本的JS代码转换成低版本的JS代码,兼容更多的浏览器。
所以,我们拿到的打包后的JS代码,通常是经过压缩、混淆和转换的,可读性很差。 这就给我们的逆向工作带来了很大的挑战。
第二部分:磨刀不误砍柴工——逆向工具箱
工欲善其事,必先利其器。 在开始逆向之前,我们需要准备一些趁手的工具。
工具名称 | 功能描述 | 备注 |
---|---|---|
Chrome DevTools | 浏览器自带的开发者工具,可以查看网络请求、调试JS代码、分析性能等。 | 这是必备工具,熟练使用DevTools是逆向的基础。 |
VS Code | 代码编辑器,可以安装各种插件,提高开发效率。 | 推荐使用,可以安装一些JS美化、反混淆的插件。 |
js-beautify | JS代码美化工具,可以将压缩后的JS代码格式化,提高可读性。 | 可以通过命令行或者在线工具使用。 |
UglifyJS | JS代码压缩和混淆工具,也可以用来解混淆一些简单的代码。 | 可以通过命令行或者在线工具使用。 |
AST Explorer | JavaScript抽象语法树(AST)可视化工具,可以帮助我们理解JS代码的结构。 | 对于理解复杂的JS代码,AST Explorer 非常有用。 |
de4js | 一个用于 JavaScript 反混淆的工具,支持多种混淆方式。 | 可以尝试用它来处理一些复杂的混淆代码。 |
cyberchef | 一个强大的数据分析和转换工具,可以进行各种编码、加密、解密操作。 | 在处理一些特殊的编码和加密时,cyberchef 很有用。 |
在线JS美化工具 | 网上有很多在线的JS美化工具,比如beautifier.io,可以方便地对JS代码进行格式化。 | 如果不想安装本地工具,可以使用在线工具。 |
第三部分:庖丁解牛——逆向流程与技巧
有了工具,接下来就是实践了。 JS逆向的流程大致如下:
-
获取目标JS代码: 从网页源代码中找到JS文件的URL,或者通过DevTools查看网络请求,找到对应的JS文件。
-
代码美化: 使用js-beautify或者在线JS美化工具,将压缩后的JS代码格式化,提高可读性。
// 压缩后的代码 !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.MyModule=t():e.MyModule=t()}(this,function(){return function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t){e.exports=function(){return"Hello, World!"}}])});
// 美化后的代码 (function(e, t) { "object" == typeof exports && "object" == typeof module ? module.exports = t() : "function" == typeof define && define.amd ? define([], t) : "object" == typeof exports ? exports.MyModule = t() : e.MyModule = t() })(this, function() { return function(e) { var t = {}; function n(r) { if (t[r]) return t[r].exports; var o = t[r] = { i: r, l: !1, exports: {} }; return e[r].call(o.exports, o, o.exports, n), o.l = !0, o.exports } return n.m = e, n.c = t, n.d = function(e, t, r) { n.o(e, t) || Object.defineProperty(e, t, { enumerable: !0, get: r }) }, n.r = function(e) { "undefined" != typeof Symbol && Symbol.toStringTag && Object.defineProperty(e, Symbol.toStringTag, { value: "Module" }), Object.defineProperty(e, "__esModule", { value: !0 }) }, n.t = function(e, t) { if (1 & t && (e = n(e)), 8 & t) return e; if (4 & t && "object" == typeof e && e && e.__esModule) return e; var r = Object.create(null); if (n.r(r), Object.defineProperty(r, "default", { enumerable: !0, value: e }), 2 & t && "string" != typeof e) for (var o in e) n.d(r, o, function(t) { return e[t] }.bind(null, o)); return r }, n.n = function(e) { var t = e && e.__esModule ? function() { return e.default } : function() { return e }; return n.d(t, "a", t), t }, n.o = function(e, t) { return Object.prototype.hasOwnProperty.call(e, t) }, n.p = "", n(n.s = 0) }([function(e, t) { e.exports = function() { return "Hello, World!" } }]) });
-
代码分析:
- 入口函数: 找到代码的入口函数,通常是一个立即执行函数(IIFE)。
- 变量和函数: 分析代码中的变量和函数,了解它们的作用和关系。
- 关键算法: 找到代码中的关键算法,比如加密算法、数据处理算法等。
- 事件监听: 查找事件监听器,比如
addEventListener
,了解代码是如何响应用户操作的。
-
代码调试: 使用Chrome DevTools的调试功能,可以单步执行代码,查看变量的值,帮助我们理解代码的执行流程。
-
代码修改和替换: 如果需要修改代码,可以在DevTools中直接修改,或者将代码复制到编辑器中修改,然后替换掉原始代码。
-
反混淆: 如果代码经过了混淆,可以使用一些反混淆工具,或者手动进行反混淆。
一些常用的逆向技巧:
-
搜索字符串: 如果知道某个字符串在代码中出现过,可以使用搜索功能快速定位到相关的代码。
-
断点调试: 在关键代码处设置断点,可以帮助我们理解代码的执行流程。
-
Hook函数: Hook函数可以拦截函数的调用,并修改函数的参数和返回值,可以用来分析函数的行为。
// Hook函数示例 (function() { var originalAlert = window.alert; window.alert = function(message) { console.log("Alert 被 Hook 了,消息是:" + message); // 可以选择是否调用原始的alert函数 originalAlert(message); }; })();
-
AST分析: 对于复杂的代码,可以使用AST Explorer来分析代码的结构,帮助我们理解代码的逻辑。
-
耐心和毅力: 逆向工程是一个需要耐心和毅力的过程,不要轻易放弃。
第四部分:JS代码混淆与反混淆
JS代码混淆是一种常见的保护代码的技术,它可以让代码变得难以阅读和理解,增加逆向的难度。
常见的JS混淆方式:
- 变量名混淆: 将变量名替换成无意义的字符串,比如
a
、b
、c
。 - 字符串混淆: 将字符串进行编码或者加密,比如使用Base64编码或者自定义的加密算法。
- 控制流混淆: 将代码的控制流打乱,比如使用
if
语句和goto
语句。 - 代码压缩: 去掉空格、注释,缩短变量名,让代码体积更小。
- 死代码注入: 插入一些不会被执行的代码,干扰阅读。
- 垃圾代码注入: 插入一些无意义的代码,增加代码体积。
- 调试保护: 检测是否在调试模式下运行,如果是则阻止代码执行。
常见的反混淆技巧:
- 代码美化: 使用js-beautify或者在线JS美化工具,将压缩后的JS代码格式化,提高可读性。
- 变量名替换: 将混淆后的变量名替换成有意义的名称,比如
username
、password
。 - 字符串解密: 将编码或者加密后的字符串解密,还原成原始的字符串。
- 控制流还原: 将打乱的控制流还原成正常的控制流。
- 删除死代码和垃圾代码: 删除不会被执行的代码和无意义的代码。
- Hook调试保护: Hook调试保护函数,阻止其阻止代码执行。
- 使用反混淆工具: 使用de4js等反混淆工具,可以自动反混淆一些简单的代码。
示例:
假设我们有如下混淆后的代码:
var _0x1f4a=['log','Hello, World!'];(function(_0x5b0d8a,_0x55284f){var _0x1e195d=function(_0x1aa69f){while(--_0x1aa69f){_0x5b0d8a['push'](_0x5b0d8a['shift']());}};_0x1e195d(++_0x55284f);}(_0x1f4a,0x10f));var _0x5b0d=function(_0x55284f,_0x1aa69f){_0x55284f=_0x55284f-0x0;var _0x1e195d=_0x1f4a[_0x55284f];return _0x1e195d;};console[_0x5b0d('0x0')](_0x5b0d('0x1'));
这个代码使用了字符串数组和位移运算进行混淆。 我们可以手动进行反混淆:
- 分析代码: 代码定义了一个字符串数组
_0x1f4a
,和一个函数_0x5b0d
,这个函数用来从字符串数组中获取字符串。 - 替换变量名: 将
_0x1f4a
替换成stringArray
,将_0x5b0d
替换成getString
。 - 还原字符串: 将
console[_0x5b0d('0x0')](_0x5b0d('0x1'))
替换成console.log(getString('0x1'))
。 - 展开函数: 将
getString('0x1')
替换成stringArray[1]
。 - 替换字符串: 将
stringArray[1]
替换成"Hello, World!"
。
最终,我们可以得到如下反混淆后的代码:
var stringArray = ['log', 'Hello, World!'];
console.log(stringArray[1]);
第五部分:实战演练——分析一个简单的Webpack打包文件
咱们来分析一个简单的Webpack打包文件,看看如何还原源码。
假设我们有如下Webpack打包后的代码:
/******/ (() => { // webpackBootstrap
/******/ var __webpack_modules__ = ({
/***/ "./src/index.js":
/*!**********************!*
!*** ./src/index.js ***!
**********************/
/***/ ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
eval("const hello = __webpack_require__(/*! ./hello.js */ "./src/hello.js");nnconsole.log(hello);nnn//# sourceURL=webpack://test/./src/index.js?");
/***/ }),
/***/ "./src/hello.js":
/*!**********************!*
!*** ./src/hello.js ***!
**********************/
/***/ ((module) => {
eval("module.exports = 'Hello, World!';nn//# sourceURL=webpack://test/./src/hello.js?");
/***/ })
/******/ });
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ id: moduleId,
/******/ loaded: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.loaded = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/************************************************************************/
/******/ /* webpack/runtime/hasOwnProperty shorthand */
/******/ (() => {
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
/******/ })();
/******/
/************************************************************************/
/******/ // startup
/******/ // Load entry module and return exports
/******/ // This entry module can't be inlined because the eval devtool is used.
/******/ var __webpack_exports__ = __webpack_require__("./src/index.js");
/******/
/******/ })()
;
-
代码分析: 代码是一个立即执行函数(IIFE),里面定义了一个
__webpack_modules__
对象,这个对象包含了所有的模块。 -
找到模块:
__webpack_modules__
对象包含了./src/index.js
和./src/hello.js
两个模块。 -
提取源码: 从
eval
函数中提取源码。-
./src/index.js
的源码是:const hello = __webpack_require__(/*! ./hello.js */ "./src/hello.js"); console.log(hello);
-
./src/hello.js
的源码是:module.exports = 'Hello, World!';
-
-
还原源码: 将
__webpack_require__
替换成require
,可以得到如下还原后的代码:-
./src/index.js
的源码是:const hello = require("./hello.js"); console.log(hello);
-
./src/hello.js
的源码是:module.exports = 'Hello, World!';
-
第六部分:安全提示与法律声明
最后,咱们来聊聊安全和法律问题。
- 遵守法律法规: 逆向工程可能涉及到侵犯知识产权的问题,所以一定要遵守法律法规,不要搞非法的事情。
- 保护个人信息: 在逆向分析过程中,可能会接触到一些敏感信息,比如用户名、密码等,一定要注意保护个人信息,不要泄露出去。
- 尊重开发者: 即使我们成功地逆向了别人的代码,也要尊重开发者的劳动成果,不要恶意传播或者用于商业用途。
- 仅供学习研究: 本文所介绍的逆向技巧,仅供学习研究使用,不要用于非法用途。
总结:
JS逆向工程是一个充满挑战和乐趣的过程,它可以帮助我们学习到很多有用的知识和技巧。 但是,在进行逆向工程时,一定要遵守法律法规,尊重开发者,保护个人信息。
希望今天的讲座对你有所帮助! 谢谢大家! 散会!