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

2024-05-15 13:32

本文主要是介绍JS逆向时碰到了恶心的死代码怎么办?手把手教你解决!,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

阅读本文大概需要 3 分钟。

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

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

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

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

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

基础知识

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

死代码

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

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

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

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

花指令

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

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

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

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

a = 1
b = 2
# 花指令开始,对变量进行了一通操作
a += 1
a += b
# 花指令结束,又把变量的值给变回去了
a -= b
a -= 1c(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 操作还是很有必要的,建议学习一下。

推荐阅读

1

一些我日常使用的 Python 技巧分享

2

用它 5 分钟以后,我放弃用了四年的 Flask

3

精品连载丨安卓 App 逆向课程之二逆向神器 frida 的介绍

4‍‍

精品连载丨安卓 App 逆向课程一之环境配置

好文和朋友一起看~

这篇关于JS逆向时碰到了恶心的死代码怎么办?手把手教你解决!的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/991946

相关文章

一份LLM资源清单围观技术大佬的日常;手把手教你在美国搭建「百万卡」AI数据中心;为啥大模型做不好简单的数学计算? | ShowMeAI日报

👀日报&周刊合集 | 🎡ShowMeAI官网 | 🧡 点赞关注评论拜托啦! 1. 为啥大模型做不好简单的数学计算?从大模型高考数学成绩不及格说起 司南评测体系 OpenCompass 选取 7 个大模型 (6 个开源模型+ GPT-4o),组织参与了 2024 年高考「新课标I卷」的语文、数学、英语考试,然后由经验丰富的判卷老师评判得分。 结果如上图所

uniapp接入微信小程序原生代码配置方案(优化版)

uniapp项目需要把微信小程序原生语法的功能代码嵌套过来,无需把原生代码转换为uniapp,可以配置拷贝的方式集成过来 1、拷贝代码包到src目录 2、vue.config.js中配置原生代码包直接拷贝到编译目录中 3、pages.json中配置分包目录,原生入口组件的路径 4、manifest.json中配置分包,使用原生组件 5、需要把原生代码包里的页面修改成组件的方

2024.6.24 IDEA中文乱码问题(服务器 控制台 TOMcat)实测已解决

1.问题产生原因: 1.文件编码不一致:如果文件的编码方式与IDEA设置的编码方式不一致,就会产生乱码。确保文件和IDEA使用相同的编码,通常是UTF-8。2.IDEA设置问题:检查IDEA的全局编码设置和项目编码设置是否正确。3.终端或控制台编码问题:如果你在终端或控制台看到乱码,可能是终端的编码设置问题。确保终端使用的是支持你的文件的编码方式。 2.解决方案: 1.File -> S

公共筛选组件(二次封装antd)支持代码提示

如果项目是基于antd组件库为基础搭建,可使用此公共筛选组件 使用到的库 npm i antdnpm i lodash-esnpm i @types/lodash-es -D /components/CommonSearch index.tsx import React from 'react';import { Button, Card, Form } from 'antd'

17.用300行代码手写初体验Spring V1.0版本

1.1.课程目标 1、了解看源码最有效的方式,先猜测后验证,不要一开始就去调试代码。 2、浓缩就是精华,用 300行最简洁的代码 提炼Spring的基本设计思想。 3、掌握Spring框架的基本脉络。 1.2.内容定位 1、 具有1年以上的SpringMVC使用经验。 2、 希望深入了解Spring源码的人群,对 Spring有一个整体的宏观感受。 3、 全程手写实现SpringM

代码随想录算法训练营:12/60

非科班学习算法day12 | LeetCode150:逆波兰表达式 ,Leetcode239: 滑动窗口最大值  目录 介绍 一、基础概念补充: 1.c++字符串转为数字 1. std::stoi, std::stol, std::stoll, std::stoul, std::stoull(最常用) 2. std::stringstream 3. std::atoi, std

js+css二级导航

效果 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN""http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head><meta http-equiv="Con

记录AS混淆代码模板

开启混淆得先在build.gradle文件中把 minifyEnabled false改成true,以及shrinkResources true//去除无用的resource文件 这些是写在proguard-rules.pro文件内的 指定代码的压缩级别 -optimizationpasses 5 包明不混合大小写 -dontusemixedcaseclassnames 不去忽略非公共

vue同页面多路由懒加载-及可能存在问题的解决方式

先上图,再解释 图一是多路由页面,图二是路由文件。从图一可以看出每个router-view对应的name都不一样。从图二可以看出层路由对应的组件加载方式要跟图一中的name相对应,并且图二的路由层在跟图一对应的页面中要加上components层,多一个s结尾,里面的的方法名就是图一路由的name值,里面还可以照样用懒加载的方式。 页面上其他的路由在路由文件中也跟图二是一样的写法。 附送可能存在

vue+elementui分页输入框回车与页面中@keyup.enter事件冲突解决

解决这个问题的思路只要判断事件源是哪个就好。el分页的回车触发事件是在按下时,抬起并不会再触发。而keyup.enter事件是在抬起时触发。 so,找不到分页的回车事件那就拿keyup.enter事件搞事情。只要判断这个抬起事件的$event中的锚点样式判断不等于分页特有的样式就可以了 @keyup.enter="allKeyup($event)" //页面上的//js中allKeyup(e