JS `Reverse Engineering` 前端代码:从打包文件中还原源码与逻辑

各位靓仔靓女,晚上好!我是你们今晚的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逆向的流程大致如下:

  1. 获取目标JS代码: 从网页源代码中找到JS文件的URL,或者通过DevTools查看网络请求,找到对应的JS文件。

  2. 代码美化: 使用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!"
            }
        }])
    });
  3. 代码分析:

    • 入口函数: 找到代码的入口函数,通常是一个立即执行函数(IIFE)。
    • 变量和函数: 分析代码中的变量和函数,了解它们的作用和关系。
    • 关键算法: 找到代码中的关键算法,比如加密算法、数据处理算法等。
    • 事件监听: 查找事件监听器,比如addEventListener,了解代码是如何响应用户操作的。
  4. 代码调试: 使用Chrome DevTools的调试功能,可以单步执行代码,查看变量的值,帮助我们理解代码的执行流程。

  5. 代码修改和替换: 如果需要修改代码,可以在DevTools中直接修改,或者将代码复制到编辑器中修改,然后替换掉原始代码。

  6. 反混淆: 如果代码经过了混淆,可以使用一些反混淆工具,或者手动进行反混淆。

一些常用的逆向技巧:

  • 搜索字符串: 如果知道某个字符串在代码中出现过,可以使用搜索功能快速定位到相关的代码。

  • 断点调试: 在关键代码处设置断点,可以帮助我们理解代码的执行流程。

  • 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混淆方式:

  • 变量名混淆: 将变量名替换成无意义的字符串,比如abc
  • 字符串混淆: 将字符串进行编码或者加密,比如使用Base64编码或者自定义的加密算法。
  • 控制流混淆: 将代码的控制流打乱,比如使用if语句和goto语句。
  • 代码压缩: 去掉空格、注释,缩短变量名,让代码体积更小。
  • 死代码注入: 插入一些不会被执行的代码,干扰阅读。
  • 垃圾代码注入: 插入一些无意义的代码,增加代码体积。
  • 调试保护: 检测是否在调试模式下运行,如果是则阻止代码执行。

常见的反混淆技巧:

  • 代码美化: 使用js-beautify或者在线JS美化工具,将压缩后的JS代码格式化,提高可读性。
  • 变量名替换: 将混淆后的变量名替换成有意义的名称,比如usernamepassword
  • 字符串解密: 将编码或者加密后的字符串解密,还原成原始的字符串。
  • 控制流还原: 将打乱的控制流还原成正常的控制流。
  • 删除死代码和垃圾代码: 删除不会被执行的代码和无意义的代码。
  • 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'));

这个代码使用了字符串数组和位移运算进行混淆。 我们可以手动进行反混淆:

  1. 分析代码: 代码定义了一个字符串数组_0x1f4a,和一个函数_0x5b0d,这个函数用来从字符串数组中获取字符串。
  2. 替换变量名:_0x1f4a替换成stringArray,将_0x5b0d替换成getString
  3. 还原字符串:console[_0x5b0d('0x0')](_0x5b0d('0x1'))替换成console.log(getString('0x1'))
  4. 展开函数:getString('0x1')替换成stringArray[1]
  5. 替换字符串: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");
/******/
/******/ })()
;
  1. 代码分析: 代码是一个立即执行函数(IIFE),里面定义了一个__webpack_modules__对象,这个对象包含了所有的模块。

  2. 找到模块: __webpack_modules__对象包含了./src/index.js./src/hello.js两个模块。

  3. 提取源码:eval函数中提取源码。

    • ./src/index.js的源码是:

      const hello = __webpack_require__(/*! ./hello.js */ "./src/hello.js");
      
      console.log(hello);
    • ./src/hello.js的源码是:

      module.exports = 'Hello, World!';
  4. 还原源码:__webpack_require__替换成require,可以得到如下还原后的代码:

    • ./src/index.js的源码是:

      const hello = require("./hello.js");
      
      console.log(hello);
    • ./src/hello.js的源码是:

      module.exports = 'Hello, World!';

第六部分:安全提示与法律声明

最后,咱们来聊聊安全和法律问题。

  • 遵守法律法规: 逆向工程可能涉及到侵犯知识产权的问题,所以一定要遵守法律法规,不要搞非法的事情。
  • 保护个人信息: 在逆向分析过程中,可能会接触到一些敏感信息,比如用户名、密码等,一定要注意保护个人信息,不要泄露出去。
  • 尊重开发者: 即使我们成功地逆向了别人的代码,也要尊重开发者的劳动成果,不要恶意传播或者用于商业用途。
  • 仅供学习研究: 本文所介绍的逆向技巧,仅供学习研究使用,不要用于非法用途。

总结:

JS逆向工程是一个充满挑战和乐趣的过程,它可以帮助我们学习到很多有用的知识和技巧。 但是,在进行逆向工程时,一定要遵守法律法规,尊重开发者,保护个人信息。

希望今天的讲座对你有所帮助! 谢谢大家! 散会!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注