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

2023-10-26

文章作者:「夜幕团队 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();

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

image

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

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

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

image

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

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

image

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

image

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

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

先来看看第一段代码:

image

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

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

image

这是一个自执行的函数,没有返回值。但是注意,它的第一个实参是 _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));

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

image

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

image

这是一个函数,可以看到,引用全局变量 _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'));

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

image

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


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

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

image

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

总结

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

服务推荐

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

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

  • 从已排序的 ArrayList 中删除重复项,同时保留重复项中的某些元素

    好吧 一开始我以为这会很简单 但我想不出有效的方法来解决这个问题 我想出了一种蛮力的方法来解决这个问题 但这不是很优雅 我有一个数组列表 Contacts 是一个 VO 类 有多个成员 名称 区域 id ArrayList中存在重复项 因为
  • com.example.controller.UserController 中的字段 userRepository 需要类型为“com.example.repository.UserRepository”的 bean,但无法找到

    我正在学习 Spring Boot 运行应用程序时出现此错误 描述 com example controller UserController 中的字段 userRepository 需要类型为 com example repository
  • java.lang.NoSuchFieldError:APPLICATION_CONTEXT_ID_PREFIX

    我在运行项目时收到此错误 最终结果为 404 该项目是在Spring框架上进行的 我读了很多帖子 发现要么是混合了罐子 要么是多余的罐子 接下来我尝试整理我的罐子 以下列表是我的构建路径中的内容 antlr 2 7 6 jar asm ja
  • vue中有自动更新这段代码的东西吗?

    我在导航器中找到了这个按钮 当用户登录时会显示该按钮 而当用户注销时该按钮就会消失 但现在我需要在按钮删除 出现之前刷新页面 这是我的代码 Button div div class div div
  • Cordova/Phonegap 通过 JavaScript 在应用程序浏览器中打印

    我想从我正在开发的 iPad 应用程序打印一页 或某些页面 应用程序启动时所做的第一件事是通过以下代码加载外部网站 window location https 我现在想从这个外部网站打印一些东西 在 iPad 上的 Safari 中效果很好
  • Kivy:滚动缩放

    有没有办法在桌面 kivy 应用程序上放大图像 例如使用鼠标滚轮缩放 这里似乎讨论过 https github com kivy kivy issues 3563 https github com kivy kivy issues 3563
  • 如何使用Django模板作为组件?

    我有 5 个模板 index html detail html tag html login html register html and a 基本 html 所有 5 个模板都会扩展基本 html 索引 html 详细信息 html 标签
  • 如何动态更新属性文件?

    我的应用程序是一个批处理过程 它从 application properties 文件中提取环境变量 我使用的密码必须每隔几个月更新一次 我想自动化密码更新过程并将更新后的密码保存在属性文件中 以便在将来的运行中使用 但我尝试进行的任何更新
  • 有没有办法在不托管网站的情况下呈现网站并共享它?

    我正在为一个项目创建一个 repl it 网站 问题是我的老师要求不要发布该网站 这意味着我无法使用 repl it 来托管它 我想知道是否有任何方法可以制作可以通过 Google Chrome 查看的网站副本 而无需连接到主机 我有所有的
  • HTML Canvas:如何绘制翻转/镜像图像?

    当我在 HTML 画布上绘制图像时 我试图翻转 镜像图像 我发现一个游戏教程显示了角色必须面对的每个方向的精灵表 但这对我来说似乎不太正确 特别是因为每个框架都有不同的尺寸 实现这一目标的最佳技术是什么 我尝试致电setScale 1 1
  • 如何正确关闭资源

    当我清理一些代码时 FindBugs 向我指出了一些使用 Connection CallableStatement 和 ResultSet 对象的 JDBC 代码 这是该代码的一个片段 CallableStatement cStmt get
  • 如何通过pygit2获取当前签出的Git分支名称?

    这个问题应该与 如何获取Git中当前的分支名称 https stackoverflow com questions 6245570 how to get current branch name in git 获取 git 当前分支 标签名称
  • jtree 编程式多选

    是否能够以编程方式选择 JTree 中的多个树节点 我已经设置了多选模式tree getSelectionModel setSelectionMode TreeSelectionModel DISCONTIGUOUS TREE SELECT
  • Python 中的“lambda”是什么意思,最简单的使用方法是什么?

    您能否给出一个示例和其他示例来说明何时以及何时不使用 Lambda 我的书给了我一些例子 但它们很令人困惑 拉姆达 起源于拉姆达演算 http en wikipedia org wiki Lambda calculus和 AFAIK 首先实
  • 使用node和multer将图像上传到heroku不起作用

    我正在尝试使用 Node 后端将图像文件上传到 Heroku 我可以使其工作 同样的过程在本地主机测试中工作得很好 但是在将我的项目部署到 Heroku 并测试它之后 过程和文件中出现错误不会上传 后端 let storage multer
  • 关于 Executors.newSingleThreadExecutor() 的问题

    这是一个关于以下代码的程序流程的问题 import java util concurrent ExecutorService import java util concurrent Executors public class Test p
  • launchd执行python脚本,但导入失败

    我使用 appscript 编写了一个 python 脚本来跟踪我当前活动的窗口 我通过 launchd 运行它 但是当我这样做时 它无法导入 appscript 我已经在 launchd 的 plist 中设置了 PYTHONPATH 但
  • 使用 Android API 发布推文

    我一直在寻找一种使用 Android 应用程序发布推文的方法 但我发现的所有方法都不起作用 我不得不承认 Twitter 的 API 并不是那么容易理解 但是我的代码并不长 而且我看不出我的错误在哪里 这是我的代码 public class
  • 展平数组中的对象

    大家好 我从响应中获取了一系列对象 我需要将所有学生对象展平为简单的学生姓名 但不确定如何进行 任何帮助将不胜感激 数组示例 students id 123456 name Student Name active true students
  • 交响二阶颂歌

    我有一个简单的二阶 ODE 的齐次解 当我尝试使用 Sympy 求解初始值时 它返回相同的解 它应该替代 y 0 和 y 0 并产生一个没有常数的解 但事实并非如此 这是建立方程的代码 它是一个弹簧平衡方程 k 弹簧常数 m 质量 我在其他

随机推荐