使用 JavaScript 进行恶意代码分析






4.43/5 (6投票s)
2005年9月21日
11分钟阅读

62860

436
一篇关于如何揭示 JS 脚本真实意图的文章。
引言
恶意代码编写者有许多攻击向量。在此,我将介绍一个用于解析编码 JavaScript 的 JS 类。我将通过一个真实世界的例子,展示一个试图通过使用一些常见技术来隐藏其行为的脚本,以及如何绕过这些技术来揭示代码的真实意图。
背景
JavaScript 是一种非常灵活的面向对象的脚本语言,它最广为人知的能力是能在浏览器中运行并操纵客户端的网页。更多信息请参阅维基百科上的 JavaScript [1] 和原型式面向对象编程语言 [2]。一些作者甚至认为 JavaScript 将是未来的脚本语言 [3]。本文假定读者了解 JavaScript 的基本语法。
大多数代码隐藏技术由两部分组成:一个加密字符串和一个解密器,解密器会解开纠缠并最终执行生成的代码片段。JavaScript(以及大多数脚本语言)提供了接受字符串并将其作为代码执行的函数。这个过程会重复多次(因此“解密”的字符串可能实际上包含另一个需要解密的字符串)。本文的主要目标是向您展示如何挂钩这些常用的函数,并将它们重定向到日志窗口而不是执行,以便您可以方便地解释数据。
被挂钩的函数和基本思想
在这些例程中常用的函数是:document.write
、document.writeln
和 eval
(或其旧的已弃用的对应函数 – Object.prototype.eval
)。下面您可以看到一段这样的代码片段
<script language=javascript>
document.write(unescape('%3C%73%63%72%69%70%74%2...
dF('%286FVFULSW%2853odqjxdjh%28...
</script>
很明显,第一行必须以某种方式定义函数 dF
,它很可能就是解密器。我们的目标是挂钩 document.write
,而不是执行,输出应该被重定向到某个日志窗口,以便我们可以分析结果。(一个快速的替代方法是将 document.write
替换为 alert
并观察输出。然而,这有两个缺点:如果一个人想重现代码,她/他必须重新输入它 – 因为您无法从 alert 框复制粘贴 – 并且 alert 框限制了可以显示的字符数,在这个案例中这被证明是不够的)。幸运的是,挂钩非常容易做到。您可以简单地写
function someFunction() {
//...
}
document.write = someFunction;
并且所有对 document.write
的调用现在都重定向到 someFunction
。接下来我们需要一个单独的窗口来倾倒输出。这可以通过 window.open
打开,但很可能会被弹出窗口拦截器阻止(因为窗口必须在启动时出现,无需用户干预即可记录每次调用 - 即使是在页面加载阶段进行的调用,比如大多数解密调用,因为它们的目的是向浏览器/用户呈现解密后的版本)。所以我们应该提供一种替代方法来打开窗口,并记住我们想显示的内容,直到窗口打开并且我们可以将文本倾倒在那里。此外,我们希望尽可能少地造成命名空间污染(命名空间污染是指我们定义的全局函数/变量可能会与现有变量冲突)。为了避免这种情况,我们始终在函数中声明局部变量,并将整个代码包装在一个类中,该类的名称可以通过提供搜索和替换功能的任何编辑器轻松更改。
注意:可以使用 Venkman [5],这是 Mozilla / Firefox 非常强大的 JavaScript 调试器。然而,这个出色的系统在处理自修改代码时表现不佳(毕竟,哪个正常程序员会编写这样的代码?!)
实现细节
代码完全包含在 "jsdebug.js" 文件中。它包含三个主要部分:JsInterceptor
类的声明(如果需要,可以重命名),初始化系统的初始调用,以及替换默认 eval
函数的函数(这是必要的,因为 eval
表现得像一个独立的函数)。
一般说明:实现是在 Firefox 1.0.6(撰写本文档时最新的稳定版本)上进行的,虽然我努力做到跨浏览器兼容,但我从未在其他浏览器上测试过。此外,如果您要分析恶意代码,我推荐 Firefox,因为它是一个非常安全的浏览器,并且漏洞会得到非常迅速的修补(并且通过自动化通知系统,您可以很快了解到它)。
在系统初始化期间,完成了以下事情:
- 备份原始函数(因为我们需要在我们的代码中调用它们!),
- 尝试打开调试窗口,
- 如果失败,我们注册一个事件处理器(
JsInterceptor.SetupWindowOpener
),该处理器在文档加载终止时被调用,并创建一个元素,点击该元素将打开调试窗口(这样我们可以避免弹出窗口拦截器,因为它们会检测到窗口是点击的结果并允许其创建), - 我们覆盖旧函数,还伪造它们的
toString()
函数(稍后在“检测我们的实现”部分可以阅读原因,但基本上是为了阻止简单的检测方法。
现在,我将简要描述每个方法以及它可能包含的任何重要的实现细节
CopyToClipboard
- 将给定的文本复制到剪贴板。该脚本来自 experts-exchange.com [13]。InterceptorWriteLog
- 这是记录给定事件的函数。有两种可能:如果日志窗口已打开,则立即将其倾倒在那里,否则将其存储在临时数组中以供以后显示。它接受三个参数:事件描述(字符串)、事件参数(数组)和模拟事件执行的函数。如果窗口已打开,它会构建 HTML 代码来显示日志并将其放入日志窗口。条目由所有收到的数据加上一个模拟函数执行的链接和一个复制参数到剪贴板的链接组成。- 代替内置函数调用的自定义函数是:
NewDocumentWrite
、NewDocumentWriteLn
和NewEval
。一个值得注意的地方是,前两个函数并不 100% 模拟其原生对应物的行为。这是因为完全准确的模拟可能会覆盖文档,而我们想避免这种情况。相反,它们只是在文档末尾添加一个 div 元素,其中包含它应该包含的文本。 AddEvent
- 一个来自 onlinetools.org [14] 的函数,用于将函数与元素上的事件关联起来。SetupDebugWindow
- 在创建调试窗口后调用,用于初始化其内容。WindowOpener
- 当用户点击链接打开日志窗口时调用(如果日志窗口在页面加载期间被阻止打开)。SetupWindowOpener
- 创建托管上述函数链接的元素。
代码中的一个有用技巧是在使用字符串构造函数时使用 escape / unescape。由于这些函数本身需要包含字符串(由单引号或普通引号分隔),因此必须消除这些符号。此外,还需要消除换行符。我发现,与其写一个包含多个连续 replace 方法的函数,不如使用上述函数来包装/解包字符串更容易。
与代码的示例工作会话
出于显而易见的原因,我不会重新分发恶意代码,但我将描述分析它的会话,以举例说明提供的类的用法。
第一步是仔细检查代码。推荐工具:一个提供语法高亮的编辑器(我个人使用 jEdit [6])和 Tidy [7]。首先,我将 HTML 代码通过 Tidy 处理使其更易读,然后用 jEdit 打开并仔细查看(最好多次)。这是一个非常重要的一步,因为之后您将在实时浏览器中打开脚本,并且您永远无法确定您的浏览器包含哪些可利用的部分(特别是 IE 在 Secunia 数据库中目前被评为“高度关键” [8],而 Firefox 被评为“较低关键” [9],但也有说法称它也存在一些未披露的漏洞 [10])。
在这个特定案例中,我们看到以下代码
<script language=javascript>
document.write(unescape('%3C%73%63%72%69%70%74%2...
仔细查看后,我们发现实际上有两行被写成一行,很可能是为了进一步混淆代码。在正确缩进后,我们得到
<script language=javascript>
document.write(unescape('%3C%73%63%72%69%70%74%2...
dF('%286FVFULSW%2853odqjxdjh%28...
</script>
仔细查看后,我们认为第一行可能定义了“dF
”函数,第二行调用它,很可能是用一个函数的加密体。我们认为现在可以安全地将其放入浏览器中,所以我们编辑 HTML 页面以在 head 中包含以下行
<script type="text/javascript" language="javascript" src="jsdebug.js"></script>
然后在浏览器中启动它。我们收到第一个(也是唯一一个)document.write
调用的结果,以及关于 dF
函数的错误(因为我们重定向了前面提到的函数)
function dF(s) {
var s1=unescape(s.substr(0,s.length-1));
var t='';
for(i=0;i<s1.length;i++)
t+=String.fromCharCode(s1.charCodeAt(i)-s.substr(s.length-1,1));
document.write(unescape(t));
}
(原始代码再次是单行,缩进是我添加的。)这是一个简单的例程,它与“环境”的唯一交互是通过我们已挂钩的 document.write
代码。我们将它复制回原始源代码(在调用 dF
之前),然后刷新页面。现在我们得到了调用 dF
的结果(因为它使用 document.write
来显示它)
<SCRIPT language="JScript.Encode">#@~^fAAAAA==@#@&NG1Es+xDRS...
哇,这看起来很奇怪。一些背景信息:在 2003 年,微软创建了一个小工具叫做 Script encoder [11]。它提供了一种非常弱的加密,可以很快被破解,并且只能防止随意查看(它也不兼容任何标准或其他浏览器!)。我使用 Google 搜索“JScript.Encode decode”,然后找到以下网站“解码包含“jscript.encode”部分的网页”(我与该网站无关)。通过它,最终结果显示为
document.write(
'<OBJECT classid=XXXX-XXXX-XXX codebase=XXXXXX.cab></OBJECT>');
在 Google 上搜索 XXXXXX.cab 得到了文件的 URL。下载并解压后得到了一个安装脚本(.inf)和一个 DLL。在将 DLL 提交给 VirusTotal [12] 后,我们得出结论,该威胁已被许多杀毒引擎检测到,现在(至少在这种情况下)我们可以放心了。
检测我们的实现
如果这种技术变得广泛使用,未来的恶意软件作者将尝试检测它的存在,就像(一些)当前的编译型恶意软件试图检测调试器的存在一样。对此的最佳防御当然是在运行代码之前仔细检查代码并对其进行修改,以便跳过检测代码。我将介绍三种用于检测此系统存在的方法以及如何击败它们。我欢迎任何关于如何检测此系统以及如何击败特定检测方法的建议,但我也想再次强调,最好的防御是在运行代码之前对其进行深入检查。
- 使用类似“if
(JsInterceptor != null)
”的构造来检测JsInterceptor
变量的存在。非常基础,非常容易发现。可能的对策:使用带有搜索和替换功能的编辑器,并为变量赋予新名称。 - 调用目标方法的
toString
(例如:document.write.toString()
)。在正常情况下,这将返回类似“function write() { [native code] }
”的内容,但在我们的情况下,它将返回实现源代码。我们可以通过交换write
对象(是的,它是Function
类型的一个对象!)的toString
函数来欺骗这一点,但是然后攻击者可以再次调用toString
函数的toString
(例如:document.write.toString.toString()
)依此类推。通过事先检查可以发现并删除这一点。 - 取决于系统的一些特定实现细节,如求值顺序等。我不知道有什么,但它们可能存在。建议仔细检查代码。
- 重写整个 document。可以使用以下序列
document.open()
、document.write(...)
、document.close()
来有效地消除调试器 JavaScript 代码。同样,深入检查可以揭示这一点。
结论
今天的脚本恶意软件正在经历与二进制病毒相同的进化历程:从概念验证到多态,再到变形(尚无实例,但会出现)。为了应对这一点,有必要研究能提供最快、最方便的代码分析方法的技术,以便能够快速应对新的威胁。
参考文献
- JavaScript – 维基百科
- 原型式面向对象语言
- 脚本语言:走向未来
- David Flanagan:JavaScript Pocket Reference,第 2 版 - O'Reilly 2002。
- Venkman 主页
- jEdit – 程序员文本编辑器
- HTML Tidy
- Secunia(Microsoft Internet Explorer 6.x)
- Secunia(Mozilla Firefox 1.x)
- Firefox 远程漏洞利用技术细节
- Script Encoder
- VIRUSTOTAL
- Experts-Exchange
- 非侵入性 JavaScript
全披露:我是 (SOFTWIN) 的初级病毒分析师,该公司是 BitDefender 防病毒产品的制造商。然而,本文中的任何内容都不得被解释为公司的官方声明,仅代表个人观点。