专栏首页未闻CodeJS逆向时碰到了恶心的死代码怎么办?手把手教你解决!

JS逆向时碰到了恶心的死代码怎么办?手把手教你解决!

文章作者:「夜幕团队 NightTeam」 - 蔡老板、Loco

润色、校对:「夜幕团队 NightTeam」 - Loco

你是否也曾有过「在逆向时看到一大坨代码,但自己却无从下手」的遭遇?

你是否也曾有过「跟着代码跳了很久之后,才发现那一大坨代码其实没有任何作用」的惨痛经历?

你是否也曾碰到过「代码量特别大、一格式化就卡死,但后来发现有很大一坨代码都没有任何用处」的狗血场景?

别担心,其实这些情况我们只需要静下心来好好分析一下代码,并将无用代码统统剔除,就能轻松解决掉。

本文将带你实际地分析一段被知名 Javascript 代码混淆工具 Obfuscator 混淆过的代码,并将混淆后的代码中的无用代码全部剔除,尽可能地将这段代码打回原形。

基础知识

在开始之前,我们先了解一下这种「在代码中插入大量无用代码以混淆视听」的混淆方式吧。这种混淆方式有两种叫法,或者说是两种做法,它们分别是「死代码」和「花指令」。

死代码

死代码一开始是被用来描述一些人写代码时写出的没有用到的代码的,为了编译后的文件尽可能地小,编译器通常会对死代码进行移除处理。

而在不知道什么时候开始,死代码被安全工作者们用来作为一种混淆机制,以将代码量变得极为庞大,使进行逆向工程的人难以找到主要逻辑

但死代码有个很明显的特征:它虽然看着代码量很大,但实际却完全不会在程序的正常代码中被调用

如果你有兴趣的话,可以对一些包含了死代码的代码进行聚类分析,你会发现死代码和正常代码之间泾渭分明,正常代码都是互相关联着的,而死代码却是孤零零的一块或者多块,并且正常代码还完全不会与死代码产生关联。

花指令

花指令是以前被大量运用在木马、病毒的免杀上的一种反反汇编手段,花指令中的“指令”通常指的是汇编中的 jmpcall 之类的调用、跳转指令,而攻击者们会将这些指令巧妙地插入到恶意代码的执行逻辑中,使得静态分析工具在分析到这个位置时无法正常反汇编。

花指令曾经的目的主要有两个,一个是使杀毒软件无法自动分析出恶意代码,达到瞒天过海的效果;一个是给安全工作者在分析恶意软件时设下层层阻拦,使安全工作者需要花费更多的时间才能理清代码逻辑,达到拖延时间的效果。

同样是不知道什么时候开始,花指令也被安全工作者们用来作为一种混淆机制。在这种应用场景下,花指令和死代码其实很类似,它们都是用了大量无用代码来混淆视听,但花指令和死代码最大的区别就是,花指令的无用代码是会被混在正常代码中进行执行的。相比于死代码而言,花指令会造成一些性能损失,但同时也会让进行逆向工程的人更加难以分析。

但花指令也不是无懈可击的,为了不影响程序正常的执行,花指令不能干扰到程序的原有逻辑,举个例子:

a = 1
b = 2
# 花指令开始,对变量进行了一通操作
a += 1
a += b
# 花指令结束,又把变量的值给变回去了
a -= b
a -= 1

c(a, b)

所以其实只要你能看出这一通操作没有任何意义,花指令也自然就没法影响到你了。

小结

不管是死代码还是花指令,其实都只需要我们仔细观察就能将其剔除,它们并不是什么很难搞的东西,见得多了之后你甚至都不需要细看就能快速排除掉一些明显不是正常代码的部分,毕竟常见的混淆器中用到的代码其实重合度是很高的,同样的套路见多了之后自然很容易分辨。

更何况,代码混淆是需要考虑性能损耗的,对方不可能为了防你逆向工程而无止尽地对代码进行混淆,要不然人家正常业务也没办法进行了。

实战

基础知识了解完了,我们来进入实战环节。

首先,我们打开 https://obfuscator.io/,这是 Obfuscator 的网页版本,可以快速在网页上进行混淆参数的配置,并且一键生成并导出混淆后的代码。

顺带一提,Obfuscator 是一款非常优秀的 JavaScript 代码混淆工具,但代码结构都是固定的,如果想要更好的混淆效果,可将混淆后的代码进行修改,从而让别人更难分析和调试

现在,我们用它给出的样例代码来进行混淆。样例代码如下:

// Paste your JavaScript code here
function hi() {
 console.log("Hello World!");
}
hi();

注意,我们需要勾选以下选项:

这三个选项的效果分别是:

•Compact code将代码中的换行符全部去掉,使得代码看起来毫无结构性。也就是所谓的代码压缩。•Self Defending在代码中插入自检代码,用来干扰逆向工程的人对代码进行格式化、变量重命名操作,如果代码被格式化了就会无法正常运行。•Dead Code Injection在代码中插入死代码,也就是本文的重点。

配置好参数后点击 Obfuscate 按钮,即可生成按配置混淆后的代码,我生成的代码是这样的(长图警告⚠️):

可以看到,原本短短的几行代码,在经过混淆后变成了这么多。而且这个代码还是经过压缩的,完全看不出层级。

当然,这个代码是可以正常运行的,我们用NodeJS跑一遍看看:

看起来混淆并没有影响到正常的代码逻辑,我们再把这一坨代码给格式化一下看看:

果不其然,格式化后的代码直接就没法运行了。在平时我们遇到这种情况时要记住,原代码可以正常运行但格式化之后不行,那么这个报错肯定是跟格式化代码有关系的,至于它报错的内容具体是啥意思其实并不重要。

那么怎么办呢?我们来静态分析一下它的代码就知道了。

先来看看第一段代码:

定义了一个数组并初始化,显然不可能造成什么问题。

接着看看第二段代码(长图警告⚠️):

这是一个自执行的函数,没有返回值。但是注意,它的第一个实参是 _0x2831,也就是之前定义的那个数组,对应的形参是 _0x528cba。我们可以根据这个来判断它对 _0x2831 做了些什么。

现在我们来一段一段地分析这第二大段代码中的每一段代码,首先是第一段代码:

var _0x1b0e99 = function(_0x5beb46) {
       while (--_0x5beb46) {
           _0x528cba['push'](_0x528cba['shift']());
       }
   };

这么短的代码相信大家都应该能看懂,是对 _0x528cba 进行 shift 操作,而 _0x528cba 是自执行函数的形参,实参是 _0x2831。换句话说,它就是对实参进行 shift 操作。不过这里它只是声明,并没有调用,所以还不会去改变实参。

然后是第二段代码和第三段代码,这里因为代码量太大就不整个贴出来了,之前已经贴过完整代码了。

第二段代码是定义了一个函数,而第三段代码则是调用这个函数,因此我们主要分析这第二段代码即可。

如果你不会分析,可以跳过它声明的语句,它真正开始执行的是这行代码:

var _0x53c9b6 = _0x1d1bc5['updateCookie']();

不要看它这段代码里面既有 setCookie,又有 getCookie,其实它跟 cookie 没有半毛钱关系,它只是一个 object 的 key 值,仅此而已。

因此,我们只需要关注它有没有改变实参,有没有改变全局变量。整个代码全局变量只有一个 _0x2831,它也是实参,也就是说只需要关心这个 _0x2831 即可。

通过上面的分析我们可以知道,它的第一段代码定义了一个函数,确实改变了实参,但是没有调用。因此,我们得找找看它在哪里被调用的,直接搜函数名 _0x1b0e99,定位到这里:

_0x4c51d1(_0x1b0e99, _0x283138);

这时,_0x1b0e99是第一个实参,第二个实参 _0x283138 则是自执行函数的形参,它对应的实参是 0x1bf,是一个整形的数值。这下,我们只需要看看 _0x4c51d1 的函数声明即可:

var _0x4c51d1 = function(_0x3d5743, _0x3c21e0) {
    _0x3d5743(++_0x3c21e0);
};

这么大一段代码,其实真正改变实参的只有这里。我们将这两行代码结合一下,就会变成这样:

_0x1b0e99(++_0x283138);

所以,第二大段代码这个自执行函数中的第二段代码只有这一句是真正改变实参的地方,其他的全部是垃圾代码,直接删除即可。删除后,这个自执行函数就变成了这样:

(function(_0x528cba, _0x283138) {
   var _0x1b0e99 = function(_0x5beb46) {
       while (--_0x5beb46) {
           _0x528cba['push'](_0x528cba['shift']());
       }
   };
   _0x1b0e99(++_0x283138);
}(_0x2831, 0x1bf));

这样看起来就清爽多了,运行试试看:

还是报同样的错误,接着往下分析第三段代码(长图警告⚠️):

这是一个函数,可以看到,引用全局变量 _0x2831 的只有这一行:

var _0x1b0e99 = _0x2831[_0x528cba];

这是一个赋值语句,但是不会改变 _0x2831 这个变量,因此我们只需要重点关注它的返回值 _0x1b0e99 就好。

再来看看它最后赋值的地方,有两处,一处在 if 语句里面:

_0x1b0e99 = _0x1b0e['SmClCt'](_0x1b0e99, _0x283138);

另外一处在 else 语句里面:

_0x1b0e99 = _0x309846;

那它到底是执行的那行代码呢,来看看 if 语句的条件:

_0x309846 === undefined

继续分析上面的代码:

var _0x309846 = _0x1b0e['jZzRvK'][_0x528cba];

以及 _0x1b0e['jZzRvK'] 最近的定义的地方:

_0x1b0e['jZzRvK'] = {};

这样就清楚了,_0x309846 === undefined 这个条件是成立的,所以 _0x1b0e99 最后赋值的地方是这里:

_0x1b0e99 = _0x1b0e['SmClCt'](_0x1b0e99, _0x283138);

函数 _0x1b0e['SmClCt'] 赋值在这里:

_0x1b0e['SmClCt'] = _0x5beb46;

而实参,则是在 _0x5beb46 函数之前定义过,因此只需要这两行代码提到 _0x5beb46 函数之后即可。注意,这里为了清楚一点,可以这样操作:

if (_0x1b0e['DVdkAf'] === undefined) {
............................
       _0x1b0e['SmClCt'] = _0x5beb46;
       _0x1b0e['jZzRvK'] = {};
       _0x1b0e['DVdkAf'] = !![];
}
_0x1b0e99 = _0x1b0e['SmClCt'](_0x1b0e99, _0x283138);
return _0x1b0e99;

根据上面的思路继续分析 hi 函数,同样也注入了一些不会改变全局变量 _0x2831 的垃圾代码,真正有效的代码只有这一行:

console[_0x1b0e('0xc', '^G6o')](_0x1b0e('0xd', 'Bi36'));

删除掉这些垃圾代码后,我们就可以知道这份代码原来长什么样了。

BOOM!结果就是这么三行代码:

function hi() {
    console[_0x1b0e('0xc', '^G6o')](_0x1b0e('0xd', 'Bi36'));
}

最后我们再运行一下试试看吧:

没有报错,代码成功运行了~

总结

碰到大段代码时不要慌,先试着分析一下,把无用代码剔除掉之后其实最后剩下的可能就只有几行而已。当然实际情况中你往往会碰到混淆参数更复杂的代码,你需要让程序来帮助你进行分析,而想要写出这种程序又会使用到 AST 操作,所以说掌握 AST 操作还是很有必要的,建议学习一下。

本文分享自微信公众号 - 未闻Code(itskingname)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-06-16

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 先找到爱的人,再写出色的代码

    我曾经为某个著名的开源项目贡献过代码。这个项目的编码之规范、测试之完整、文档之清晰在我见过的开源项目中名列前茅。

    青南
  • 一日一技:如何正确移除Selenium中window.navigator.webdriver的值

    有不少朋友在开发爬虫的过程中喜欢使用Selenium + Chromedriver,以为这样就能做到不被网站的反爬虫机制发现。

    青南
  • 你花了两天时间,就给老子写了两行代码?!

    原文地址:You’ve only added two lines - why did that take two days! ~ Matt Lacey: Cr...

    青南
  • iOS程序员请改掉影响你升职加薪的36个坏习惯!

    IT行业的科技公司们一直苦苦追寻传说中以一当十的超级程序员,最新的研究表明确实存在这样一小撮效率奇高的“程序金刚”,但是一位普通程序猿如何能够蜕变成代码金刚呢?

    原来是泽镜啊
  • 有了这个神器,贴代码请大佬调试的时候再也不怕被骂这是什么鬼玩意儿啦!

    作为一个不知名的号主,承蒙大家不嫌弃,经常性的会有人非常客气的把一堆代码扔到我的脸上,这些代码千奇百怪,姿态各异,让我喜笑颜开...

    Rocky0429
  • 高级Python工程师教你如何正确写代码

    我接手的第一样东西就是React UI。我们有一个主要组件,它容纳了其他所有组件。我喜欢在代码中加入一点幽默感,我想把它命名为GodComponent。在cod...

    小小科
  • Dead Code为什么能在代码库中永生?

    在一些遗留系统中,经常会看到大片大片灰掉的代码(被注释掉了),这种代码是死代码吗?如果要我下定义,我认为这些不是死代码,因为它们连代码都称不上,如何又能叫死代码...

    袁慎建@ThoughtWorks
  • JS逆向时碰到了恶心的死代码怎么办?手把手教你解决!

    你是否也曾有过「跟着代码跳了很久之后,才发现那一大坨代码其实没有任何作用」的惨痛经历?

    崔庆才
  • 万物代码化:从低代码、云开发到云研发

    我也是从我的所做、所见、所听中,构建了整个的模型,并非从未来穿越到现在,所以其中的一些设想,可能并非如此准确。

    Phodal
  • 代码能写多少写多少 No.187

    有个朋友说,他十天写了 20000 行代码,当时我的膝盖就直接给它了,怎么会有这么强的选手??!!

    大蕉

扫码关注云+社区

领取腾讯云代金券