各位观众老爷,晚上好!我是你们的老朋友,今晚咱们来聊聊一个略带神秘,却又无处不在的话题:动态代码执行在代码混淆中的作用,以及如何像福尔摩斯一样追踪到它们背后隐藏的真实代码。
开场白:动态代码,混淆的得力助手
话说,代码混淆这玩意儿,就像给代码穿上了一层又一层的迷彩服,让人难以一眼看穿它的真实意图。而动态代码执行,比如 eval
、new Function
和 <script>
标签注入,就像是混淆工具箱里的秘密武器,能让迷彩服更加复杂,更加难以破解。
为什么这么说呢?因为动态代码执行可以在运行时生成、修改甚至执行代码,这打破了静态分析的局限性。静态分析就像是拿着一张地图找宝藏,而动态代码执行就像是在寻宝过程中突然有人把地图给改了,甚至告诉你宝藏根本不在地图上!
第一幕:动态代码执行的三剑客
咱们先来认识一下动态代码执行的三位主角:
-
eval()
:老牌劲旅,简单粗暴eval()
函数可以将一个字符串作为 JavaScript 代码执行。let code = "console.log('Hello from eval!');"; eval(code); // 输出: Hello from eval!
优点: 简单易用,直接执行字符串。
缺点: 安全风险高,容易受到恶意代码注入攻击。性能也相对较差,因为它需要在运行时编译代码。
混淆方式:可以将复杂的代码逻辑拆分成字符串片段,然后通过eval()
拼接并执行。 -
new Function()
:构造函数,灵活多变new Function()
可以创建一个新的函数,它的参数是字符串形式的函数参数和函数体。let myFunction = new Function('a', 'b', 'return a + b;'); let result = myFunction(2, 3); console.log(result); // 输出: 5
优点: 可以动态创建函数,比
eval()
稍微安全一些,因为它可以控制函数的参数。缺点: 仍然存在安全风险,并且性能也不如直接定义的函数。
混淆方式:可以动态生成复杂的函数逻辑,增加代码的逆向难度。 -
<script>
标签注入:瞒天过海,暗度陈仓通过动态创建
<script>
标签,并将代码添加到标签的src
属性或textContent
属性中,可以加载和执行外部或内联的 JavaScript 代码。let script = document.createElement('script'); script.textContent = "console.log('Hello from script tag!');"; document.body.appendChild(script); // 输出: Hello from script tag!
或者加载外部脚本:
let script = document.createElement('script'); script.src = "https://example.com/evil.js"; //加载一个恶意脚本 document.body.appendChild(script);
优点: 可以加载外部资源,方便代码模块化。
缺点: 容易受到跨站脚本攻击 (XSS),安全性较低。
混淆方式:可以将关键代码放在外部文件中,然后通过动态<script>
标签加载,增加代码的隐藏性。
第二幕:混淆与动态代码的共舞
动态代码执行之所以能成为混淆的得力助手,是因为它可以实现以下几种混淆技巧:
-
字符串拼接与解密: 将代码分割成多个字符串片段,然后使用动态代码执行将它们拼接起来。或者将代码进行加密,然后在运行时解密并执行。
let part1 = "console."; let part2 = "log('"; let part3 = "Hello, "; let part4 = "world!"; let part5 = "');"; eval(part1 + part2 + part3 + part4 + part5); // 输出: Hello, world! // 加密的代码 let encryptedCode = "Uryyb, jbeyq!"; // ROT13 加密 let decryptionKey = 13; function decrypt(text, key) { let result = ""; for (let i = 0; i < text.length; i++) { let charCode = text.charCodeAt(i); if (charCode >= 65 && charCode <= 90) { // 大写字母 result += String.fromCharCode(((charCode - 65 - key + 26) % 26) + 65); } else if (charCode >= 97 && charCode <= 122) { // 小写字母 result += String.fromCharCode(((charCode - 97 - key + 26) % 26) + 97); } else { result += text[i]; } } return result; } let decryptedCode = decrypt(encryptedCode, decryptionKey); eval("console.log('" + decryptedCode + "')"); // 输出: Hello, world!
-
代码变形与重构: 将代码进行变形,比如改变变量名、函数名、代码结构等,然后使用动态代码执行将变形后的代码还原成可执行的代码。
let obfuscatedCode = "var _0xabc=['log','console'];(function(_0x234,_0x567){var _0xdef=function(_0x890){while(--_0x890){_0x234['push'](_0x234['shift']());}};_0xdef(++_0x567);}(_0xabc,0x123));var _0x456=function(_0x9ab,_0xdef){_0x9ab=_0x9ab-0x0;var _0x123=_0xabc[_0x9ab];return _0x123;};_0x567[_0x456('0x0')](_0x456('0x1'));"; eval(obfuscatedCode); // 输出: console
-
控制流平坦化: 将代码的控制流打乱,然后使用动态代码执行根据特定的条件来执行不同的代码块。
这种混淆方式会让代码的逻辑变得非常复杂,难以理解。通常会结合状态机和
switch
语句来实现。 -
动态加载模块: 将代码分割成多个模块,然后使用动态
<script>
标签或import()
函数动态加载这些模块。
第三幕:追踪动态代码的真相
面对这些狡猾的混淆技巧,我们该如何追踪到动态代码背后隐藏的真实代码呢?这里有一些常用的方法:
-
静态分析 + 猜测:
- 寻找关键字符串: 比如
eval
、new Function
、document.createElement('script')
等关键字。 - 分析代码结构: 尝试理解代码的整体逻辑,找出动态代码执行的位置。
- 猜测代码意图: 根据代码的上下文,猜测动态代码执行的目的是什么。
- 代码美化: 使用工具将混淆后的代码进行格式化,使其更易于阅读。
局限性: 当混淆程度较高时,静态分析可能会变得非常困难,甚至无法进行。
- 寻找关键字符串: 比如
-
动态调试:
- 使用浏览器开发者工具: 在浏览器开发者工具中设置断点,跟踪代码的执行过程,查看变量的值和函数调用堆栈。
- Hook 函数: 通过重写
eval
、new Function
等函数,记录它们的调用信息和参数,从而了解动态代码执行的内容。
// Hook eval 函数 let originalEval = eval; eval = function(code) { console.log("eval called with code:", code); debugger; // 暂停执行,方便调试 return originalEval(code); }; // Hook new Function 函数 let originalFunction = Function; Function = function() { let args = Array.from(arguments); console.log("new Function called with arguments:", args); debugger; // 暂停执行,方便调试 return new originalFunction(...args); }; // Hook document.createElement 函数 let originalCreateElement = document.createElement; document.createElement = function(tagName) { let element = originalCreateElement.apply(this, arguments); if (tagName === 'script') { // 监听 script 标签的属性设置 Object.defineProperty(element, 'src', { set: function(src) { console.log('Script tag src set to:', src); debugger; // 暂停执行,方便调试 this.setAttribute('src', src); }, get: function() { return this.getAttribute('src'); } }); Object.defineProperty(element, 'textContent', { set: function(textContent) { console.log('Script tag textContent set to:', textContent); debugger; // 暂停执行,方便调试 this.textContent = textContent; }, get: function() { return this.textContent; } }); } return element; };
优点: 可以实时观察代码的执行过程,获取动态生成的信息。
缺点: 需要一定的调试技巧,并且可能会受到反调试技术的干扰。
-
AST (Abstract Syntax Tree) 分析:
- 将代码解析成 AST: 使用工具将 JavaScript 代码解析成抽象语法树。
- 分析 AST 结构: 遍历 AST,查找
eval
、new Function
等节点,提取它们的参数和上下文信息。 - 重构 AST: 对 AST 进行修改,比如将动态生成的代码插入到 AST 中,然后将 AST 转换回 JavaScript 代码。
const esprima = require('esprima'); const estraverse = require('estraverse'); const escodegen = require('escodegen'); let code = "let x = 1; eval('console.log(x + 1)');"; // 解析代码成 AST let ast = esprima.parseScript(code); // 遍历 AST estraverse.traverse(ast, { enter: function(node) { if (node.type === 'CallExpression' && node.callee.name === 'eval') { console.log('Found eval call:', node.arguments[0].value); // 这里可以对 eval 的参数进行分析和处理 } } }); // 将 AST 转换回代码 let generatedCode = escodegen.generate(ast); console.log('Generated code:', generatedCode);
优点: 可以对代码进行深层次的分析和修改,不受动态执行的限制。
缺点: 需要一定的 AST 知识,并且实现起来比较复杂。
第四幕:实战演练
咱们来个实战演练,假设我们遇到一段混淆后的代码:
let encryptedCode = "ZnVuY3Rpb24gYWFhKGEpe3JldHVybiBhKzEwfQ==";
let decodedCode = atob(encryptedCode);
let func = new Function("return " + decodedCode)();
console.log(func(5));
-
静态分析: 我们发现代码中使用了
atob
函数进行解码,然后使用new Function
创建了一个函数。 -
动态调试: 我们可以使用浏览器开发者工具,在
new Function
的地方设置断点,查看decodedCode
的值。或者使用 Hook 函数的方式:
let originalFunction = Function; Function = function() { let args = Array.from(arguments); console.log("new Function called with arguments:", args); debugger; return new originalFunction(...args); }; let encryptedCode = "ZnVuY3Rpb24gYWFhKGEpe3JldHVybiBhKzEwfQ=="; let decodedCode = atob(encryptedCode); let func = new Function("return " + decodedCode)(); console.log(func(5));
通过调试,我们可以看到
decodedCode
的值是"function aaa(a){return a+10}"
。 -
理解代码: 我们可以知道这段代码定义了一个函数
aaa
,它的作用是将传入的参数加上 10。然后调用这个函数,传入参数 5,最终输出 15。
表格总结:追踪动态代码的利器
方法 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
静态分析 | 简单易用,不需要运行代码。 | 当混淆程度较高时,分析难度大。 | 适用于简单的混淆代码。 |
动态调试 | 可以实时观察代码的执行过程,获取动态生成的信息。 | 需要一定的调试技巧,可能会受到反调试技术的干扰。 | 适用于复杂的混淆代码,需要跟踪代码的执行过程。 |
AST 分析 | 可以对代码进行深层次的分析和修改,不受动态执行的限制。 | 需要一定的 AST 知识,实现起来比较复杂。 | 适用于需要对代码进行自动化分析和修改的场景,比如代码重构、漏洞扫描等。 |
Hook 函数 | 可以拦截关键函数的调用,获取函数的参数和返回值,方便调试。 | 可能会影响代码的正常执行,需要谨慎使用。 | 适用于需要监控特定函数的调用情况,比如检测恶意代码。 |
结语:道高一尺,魔高一丈
代码混淆和反混淆就像一场永无止境的猫鼠游戏。混淆技术不断发展,反混淆技术也在不断进步。掌握动态代码执行的原理和追踪方法,能够帮助我们更好地理解混淆后的代码,保护自己的代码安全,也能够更好地分析恶意代码,维护网络安全。
记住,没有绝对安全的代码,只有不断学习和进步的安全意识!
感谢各位的观看,咱们下期再见!