https://www.blackglory.me/javascript-deobfuscate-general-routines-and-tips-chapter-1/
最近发现网上没有什么专门深入去讲解JavaScript反混淆的文章, 能找到的, 基本都是针对于某一种加密方式的简单解密方法, 虽然能够解决一时的问题, 但从学习和研究的角度去看, 并没有太多价值. 加之近日从他人手中接手了一个算是有些棘手的反混淆单子, 感觉有所收获, 遂将自己在反混淆方面的一些理解和方案, 做一下记录, 于是便有了此文.
这篇文章假设你是一位使用JavaScript的中级以上水平的前端工程师, 对于一些相对基础的内容, 将不做讨论和额外讲解. 原先打算只写一篇文章, 但考虑到内容较多, 没有一次性写完的精力, 请原谅我分成多篇文章来写.
作为一位熟练使用JavaScript的编程人员, 你应该已经非常明白, JavaScript作为一个以函数式为核心的多范式动态弱类型脚本语言, 它的灵活性太强大了, 这直接导致了源代码在经过一些压缩工具的蹂躏之后, 变得极难还原. 所以, 你几乎不可能把代码还原到跟原始代码一致, 这是事实, 你能还原出来的, 通常只是和原本的代码运行流程一致的另一份代码.
不过, 也确实存在可以直接还原的代码, 比如这个:
eval(function(p,a,c,k,e,r){e=String;if(!''.replace(/^/,String)){while(c--)r1=k1||c;k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k1)p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k1);return p}('0.1("2 3!")',4,4,'console|log|Hello|World'.split('|'),0,{}))
我想告诉你的是, 这段代码的混淆太弱, 只做了单纯的加密, 所以并不能归类到需要认真去反混淆的代码范畴之中. 对于类似Jspacker这样的加密手段, 解密方法在网上一搜有一大把, 由于它过于简单和流行, 以至于不少代码格式化工具都自带了Jspacker的解密功能. 这种程度的加密, 基本只在表面上做了功夫, 防君子不防小人. 当然, 你也可以把加密后的代码再加密一次, JavaScript的强大灵活性允许你这么做, 但和加密一次相比, 效果也差不多, 因为加密太简单, 而原始代码的可读性又太强.
还有一类混淆器, 在降低代码可读性上确实下了不少功夫, 比如把变量名设置成i, 1, l, I,|或者O,o,0这样的相似字符, 把加密后的代码拆分后再次加密等等, 以前写过的《l1l=document.all 特征 JS 混淆器反混淆流程详解》就是这种混淆, 比较可惜的是, 那篇文章里的例子在执行后会将原始代码直接作为script挂在document上, 去进行反混淆的唯一作用就是看看解密过程中除了把script挂到document上之外, 还有没有做其他的事, 付出与回报的价值有很大可能性不符, 性价比太低了.
读到这里你大概也猜到了, 在进行反混淆时, 你需要了解和明确的是: 什么是有价值去反混淆的代码? 怎样最小化你因为反混淆在时间上的损失? 而这些, 就是我接下来要告诉你的.
为什么要反混淆这段代码? 在面对一大堆看都不想看一眼的数字字母符号组合出来的文本之前, 你必须问自己这个问题. 一般来说, 只有利益驱使这么一种原因, 这里的利益并不一定是金钱, 如果你在看到”利益”二字时只能想到金钱, 或许你该慎重考虑一下是否应该继续在这行干下去, 这里的利益可以是为了满足自己的好奇心一窥原理, 为了使用其中的某一段代码, 或是为了取证, 当然, 纯粹的金钱交易也是一种情况. 如果是最后一种情况, 那么你必须权衡好付出和回报, 也就是反混淆的性价比, 因为其他非金钱驱动的利益作为动机时, 多半情况下你是非干不可的, 而涉及到金钱时, 你是有权利不去干这种脏活的, 何况, 你也知道, 任何涉及到代码的事情, 预估需要耗费的时间, 是比较困难的. 作为程序员, 请尽量偷懒和拒绝去做你不感兴趣的事.
答案其实很简单, 只要充分的利用工具就行了. 工具永远是你最好的帮手, 如果有现成的工具, 请拿来用. 在存在现成的反混淆方案/工具的时候, 不拿来用而是尝试自己去人工进行反混淆, 在学习以外的目的上这么做绝对是自杀行为.
而在人工进行反混淆时, 你必然会用到的工具有2个: 一个符合你习惯的代码格式化工具和一个称手的代码编辑器.
在一般的反混淆流程中, 格式化代码总是最先要做的事, 代码格式化工具可以很快的帮你还原代码的格式, 这样你就可以比较轻松的看出代码的结构, 不过一般用完之后就没它什么事了, 剩下的就看你的技术和编辑器是否强力了.
代码编辑器则至少应该具有代码高亮和替换查找功能, 代码高亮应能正确的区分关键字和符号, 替换查找功能则必须能区分单词和大小写, 或是正则表达式.
除了格式化工具和编辑器, 你还可能用到类似js2coffee的代码转化工具以及类似CoffeeScript的JavaScript预编译器, 是否会用到这些工具, 取决于你的知识储备量和需要反混淆的代码的复杂程度.
至此, 你在反混淆上的准备应该完全了, 在下一篇文章《JavaScript 反混淆的一般套路和技巧: 承》中, 将讨论一些常见的混淆方式和反混淆的方法, 以及怎样组织各个流程的顺序.
https://www.blackglory.me/javascript-deobfuscate-general-routines-and-tips-chapter-2/
反混淆的本质, 是提升代码的可读性, 反混淆的过程中, 你所做的大多数事情, 都是在保证代码运行结果不变的情况下, 提升代码的可读性, 人工进行反混淆这一行为本身, 就是对自己阅读代码能力的一种锻炼.
本篇文章将讨论一些常见的混淆方式和反混淆的方法, 以及怎样组织各个流程的顺序, 为了方便叙述, 我们以反混淆的几个主要步骤来展开讲解:
对于一些加密过多次的混淆代码, 必须先将代码解密, 由于JavaScript是脚本语言, 脚本解释器最终执行代码的效果必然和原始代码的运行效果一致, 所以理论上不存在无法解密的代码.
大部分加密工具最后都会通过eval函数执行被加密的代码, 很多时候我们只要把eval换成输出用的函数就可以得到加密前的代码.
少部分加密工具会将原始代码拆分后分别加密, 这时可能需要先对加密后的代码进行反混淆, 找出解密函数所在的位置, 逐一得到加密前的代码, 人工进行拼接.
IIFE的传参位置在整段代码的尾部, 人类的正常阅读习惯是从上至下的阅读, 如果保留IIFE的参数, 会影响我们对代码的理解, 绝大多数情况下, 我们都应该去除IIFE的参数, 将参数转变为变量定义.
反混淆前:
(function(a){
...
})('Hello World!')
反混淆后:
(function(){
var a = 'Hello World!'
...
})()
有一些变量在代码中只被使用到了一次, 把这些变量直接用变量的值或表达式替代, 会有助于提升代码的可读性.
反混淆前:
var a = 'Hello World',
b = a.split(' ')
反混淆后:
var b = 'Hello World'.split(' ')
需要注意的是, 一些变量定义被写在代码中的位置可能与代码在运行时的时间有关, 这类变量不应该简单的被去除, 在还未知晓具体用处的时候, 应当保留.
例如:
var a = new Date
对于可直接计算出结果的表达式, 建议只保留代码运行的产生的结果.
反混淆前:
'Hello World!'.split(' ')
反混淆后:
['Hello', 'World']
这个例子的转换是可能存在风险的, 只有在测试了split函数没有被修改的情况下, 才能进行化简, 一定要做足测试.
另外, 使用new RegExp(pattern, attributes)创建的正则表达式, 也应该被简写成直接量/pattern/attributes, new Array和new Object也需要视具体情况进行简写.
JavaScript的判断语句可以用短路逻辑简写, 简写后的代码在一定程度上降低了代码的可读性, 通常我们会将其还原.
反混淆前:
a === 'Hello World!' && b()
反混淆后:
if(a === 'Hello World!'){
d()
}
JavaScript的变量类型转换也可以被简写, 部分简写在JavaScript中被视为陷阱, 所以尽量将简写还原成等价代码或不影响运行效果的近似代码.
反混淆前:
function a(){
return '0x5f3759df'
}
var b = +a()
反混淆后:
function a(){
return '0x5f3759df'
}
var b = Number(a())
一些混淆器会往表达式中添加一些无关紧要的表达式或变量赋值, 例如:
var a = Math.random(),
b = Math.round(a * 9),
c = ''.split()[0],
d = [1, 1, 1, 1, 1, 1, 1, 1, 1 ,1][b > 9 ? c : b]
当你发现一些数量或调用方式明显异常的代码, 就要好好检查一下是否是例子中的这种情况了, 类似的情况在反混淆中可能会遇到很多, 有时你甚至得重写函数.
除”解密被加密的代码, 将代码结构尽可能还原至最接近原始代码的状态”、”猜解变量名和函数名”, 其他流程是可以交叉进行的, 但个人建议每次只处理1~2种情况, 以免发生混乱. 优先级方面, 先解决数量最多的混淆, 再解决最影响可读性的混淆, 其他的依次处理即可.
在反混淆过程中, 务必频繁进行测试, 以保证代码执行效果与混淆代码一致, 越早发现问题, 也越容易解决问题.
由于篇幅所限, “基于主函数的运行顺序整理”和”猜解变量名和函数名”将放在下一章《JavaScript 反混淆的一般套路和技巧: 转》中讲解.
https://www.blackglory.me/javascript-deobfuscate-general-routines-and-tips-chapter-3/
把没用的多余的简写的代码处理完后, 代码就差不多有个基本的样子了, 不过现在还没到休息的时候. 由于函数错综复杂、变量名表达不出语义等原因, 我们的代码虽然可读性较之前已经得到了提高, 但想要完整理解代码的意图, 在当前的情况下仍然是一件麻烦的事.
为了理解代码, 通常我们要做两件事, 一件是“基于主函数的运行顺序整理”, 另一件是” 猜解变量名和函数名 ” , 之后, 代码在可读性上就能更加接近原始代码的级别了.
除非你在反混淆的代码是一个库或者模块, 否则每一段独立的程序, 都会有一个主体的部分, 反混淆到达这一步, 你需要找出整个代码的主函数.
主函数可能是整段代码中最长的那段代码, 也可能是整段代码最尾部的那一段代码, 或者, 整段代码就是被安排好了顺序执行的, 那么整段代码就是主函数.
找到主函数之后, 再继续寻找那些代码中只被调用过一次的函数, 如果存在这些函数, 直接把这些函数的代码抽出来, 合并进主函数里, 当然, 你还需要确定和管理好变量各自的作用域, 这并不是件易事, 但如果你这么做了, 代码可能会产生像从异步到同步那样在可读性方面的神奇转变.
再接下来, 你需要调整函数和变量声明的位置, 将它们分类, 比如把变量声明全部放在函数头部之类的, 而函数都应该被放在它所处层的最顶部, 如果能把代码像ES5定义的严格模式那样格式化好, 可读性也会得到上升, 起码这个时候, 你不需要借助IDE之类的工具把各种语法上的定义通过列表之类的控件特别抽离出来就能找到它们所应处的位置, 不过这也是非常难做的一件事.
事实上, 在ES6和ES7里, JavaScript被赋予了很多新的特性, 让这门本来就显得有些离经叛道的语言, 变得更加酷炫也更加难以琢磨, 不过我没反混淆过ES5以上的JavaScript代码, 所以关于ES6和之后的标准在反混淆时应当如何处理运行顺序, 我是不知道的, 如果有这个机会, 我会写一篇续篇补上关于ES6和ES7的部分.
这可能是整个反混淆中最压抑的部分, 每一个程序员都怕用a, b, c…或者apple, banana, pear…作为变量名和函数名的程序员, 而大多数压缩工具, 都是这样的程序员.
在这里, 我们先从规模最小的函数开始入手, 像那些只有几行的函数, 是最容易搞明白具体意思的, 当你看懂它的意图之后, 请根据你所熟悉的命名规范来为其命名, 不过无论是哪种变量命名方式, 我都建议在变量名上标上它指向的数据的类型(如果一个变量指向过多种类型, 建议创建一个新的变量来表示这种新的类型). 如果变量名起了冲突或实在无法决定该如何命名时, 可以考虑在变量名或类型名后加上数字序号的命名方法, 因为即便这样命名, 也比用a, b, c来得好.
规模最小的函数搞定之后, 继续猜解调用这些函数的其他函数, 逐层向上, 最终就能猜解完大多数的代码.
另外, 如果函数内部出现了有关浏览器环境的内容, 比如XHR和Cookie之类的内容, 你完全有理由相信并大胆猜测这个函数与这些内容有关, 以此作为假设继续猜解其他内容, 将可能得到效率上的提升. 多利用编辑器的查找和替换功能, 也可以帮助减少疏漏.
至此, 代码的反混淆已经大致完成, 在下一章, 也是最后一章《JavaScript 反混淆的一般套路和技巧: 结》中, 将说明一些反混淆过程中的其他注意事项.
https://www.blackglory.me/javascript-deobfuscate-general-routines-and-tips-chapter-4/
本来《JavaScript 反混淆的一般套路和技巧》是打算作为一篇单独的长文来写的, 后来硬生生被我拆成了《起》、《承》、《转》、《结》四章, 在《转》里面整个反混淆的事情其实已经被我们解决了, 结果就是这作为最后一篇的《结》, 变成了多余的一章. 这章该写些什么让我想了很久, 最后决定还是想到什么写什么.
下面是我在写这篇文章时暂时能够想到的一些反混淆中的注意事项.
由于JavaScript和ES在历史上遗留的问题很多, 运行环境复杂, 所以存在不少的陷阱. 一旦遇到可能是陷阱的代码, 请立即运行查看结果, 否则到最后发现出现了什么问题, 由于调试困难, 半天都找不到问题在哪, 会白白浪费很多时间.
比如==在JavaScript中很不可靠, 在确定修改为===后执行效果不变的情况下, 建议将==全部修改为===. 发现和解决陷阱的另一个好处是, 能帮助自己养成良好的JavaScript编码习惯.
事实上我印象中有一个绝大多数JavaScript程序员都会犯错的陷阱, 可惜一时半会想不起来了(也忘了记录), 不然拿出来当例子真是极好的.
这是反混淆中很容易遇到的情况, 个人的建议是直接删掉与你测试用的环境无关的兼容代码, 如有需要, 在反混淆的最后将兼容代码自己加回来. 不过, 在反混淆结束后把兼容代码加回来并非易事, 而且必定会造成可读性的下降.
建议直接删除的理由很简单, 因为有不少古老的浏览器兼容代码采用的是旁敲侧击式的判断方法, 在没有注释的情况下很难看出它究竟兼容的是哪种浏览器的哪个版本, 还有一些兼容代码写得就像巫师的巫术一样, 你很难分清这究竟是混淆代码还是兼容用的代码, 所以只保留你测试环境需要的代码即可(建议的测试环境是V8引擎和Webkit引擎, 也就是Chrome). 反混淆的过程中你应当只关注测试环境与代码的执行效果本身, 而不应该被兼容代码所拖累.
在这种情况下你可以考虑用js2cofee这样的工具, 将原JavaScript代码转换为你所熟悉的其他语法干净的语言, 比如CoffeeScript(前提是你对这门语言足够熟悉, 知道它存在的语法陷阱, 而且转换后不会比原来可读性更差, 并且这门语言可以将代码转换为普通的JavaScript). 通常在去除了大量堪称糟粕的C类语言关键字和符号之后, 代码的可读性会大幅上升.
如果你把代码转换成了CoffeeScript, 那么在猜解变量名时会特别难受, 因为CoffeeScript的变量作用域和JavaScript使用的都是词法作用域, 而CoffeeScript里是无法像JavaScript一样用var关键字来声明一个变量的, 倘若代码里存在重复的变量名, 你需要小心翼翼的把不同作用域的变量名区别命名, 以免在编译后混用同一个作用域的变量.
你可以使用版本控制系统来保存代码, 也可以手工复制代码文件来保存各个版本, 不过无论你用的是哪种方法, 我都建议你备一个文本比较工具或者带有比较功能的编辑器.