flatmap-stream 有效载荷 A
11月,npm软件包event-stream因恶意依赖项flatmap-stream而遭到利用。整个事件的详细记录见此处。本文将以此为案例,探讨如何对JavaScript进行逆向工程。与flatmap-stream相关的3个有效载荷既足够简单,便于介绍,又足够复杂,值得深入研究。虽然无需了解该事件的背景也能读懂本文,但文中会基于一些前提展开;如果不太熟悉事件细节,这些前提可能并不明显。
大多数JavaScript的逆向工程都比桌面操作系统上运行的二进制可执行文件更直接,毕竟源代码就摆在眼前。但为了让人难以理解而编写的JavaScript代码通常会经过多轮混淆,以掩盖其意图。其中一些混淆来自所谓的“压缩”,即尽可能减少源代码的总字节数以节省空间。具体做法包括将变量缩短为单字符标识符,以及将true等表达式转换为更短但等效的形式,如!0。由于JavaScript源自Web浏览器生态,压缩主要是其生态系统特有的做法;由于工具复用,node软件包中偶尔也会采用这种做法,但其目的并非作为安全措施。如需对常见的压缩和混淆技术进行基本还原,请查看Shape的unminify工具。专门的混淆处理可能由混淆工具完成,也可能由开发者手动完成。
第一步是获取独立的源代码进行分析。flatmap-stream包经过专门设计,表面上看似无害,但其中只有一个版本(0.1.1版)包含恶意载荷。通过比较0.1.2版和0.1.1版的差异,可以快速查看源代码的变化;甚至只需在两个标签页中交替查看这两个URL即可。在本文后续内容中,我们将附加的源代码称为“载荷A”。以下是载荷A经过格式化的源代码。

首先要强调的是:切勿运行恶意代码(隔离环境除外)。我编写了自己的工具,并使用Shift解析器和JavaScript转换器套件来帮助我动态重构代码。不过,若要跟随本文操作,也可以使用Visual Studio Code等IDE。
对JavaScript进行逆向工程时,应尽量减少需要同时处理的信息。这意味着移除所有无法立即提供价值的表达式或语句,同时将经过自动或手动优化的代码从DRY形式还原。由于我们是在对JavaScript进行静态分析,并在脑中跟踪其执行过程,因此思维栈越深,就越容易迷失方向。
最简单的做法之一,是将第3行和第4行中被赋予 require 和 process 等全局属性的变量还原为未压缩形式。
可以使用任何支持重构功能的IDE来完成此操作(通常将光标置于要重命名的标识符上,然后按“F2”)。之后会看到一个函数定义e,它似乎只是用于解码十六进制字符串。

第一行值得关注的代码似乎导入了一个文件,该文件路径来自函数e对字符串“2e2f746573742f64617461”的解码结果。
经过刻意混淆的JavaScript通常会隐藏所有字符串字面量,避免有人匆匆浏览时因看到明显可疑的字符串或属性而警觉。大多数开发者都知道,这种手段很容易破解,因此经常会采用可轻松还原的编码方式,此处也不例外。e函数只是将十六进制字符串反转,可以通过在线工具手动完成,也可以自行编写一个便捷函数。即使确信自己了解e函数的作用,也建议不要使用恶意文件中的输入来运行该函数(即使已将其提取出来),因为无法保证攻击者没有发现可由这些数据触发的安全漏洞。
将该字符串反转后,可以看到脚本包含了一个数据文件“'./test/data'”,该文件位于分发的npm包中。

将 n 重命名为 data,并对 e(n[2]) 至 e(n[9]) 的调用进行反混淆处理后,我们开始更清楚地了解这里所处理的内容。

这些字符串为何被隐藏也不难理解:如果在一个简单的flatmap库中发现任何与解密相关的内容,就会明显暴露出其中存在严重问题。
从这里可以看出,该脚本正在导入node.js的“crypto”库;查阅API后,我们发现createDecipher的第二个参数(此处为o)是用于解密的密码。现在,可以根据API为该参数及后续返回值重新命名,使其含义更清晰。每当找到一块新的拼图时,都应通过重构或注释将其记录下来,即使只是重命名一个看似无关紧要的变量。深入研究陌生代码数小时后,很容易忘记进度、分心,或因某次错误重构而需要回溯。重构期间使用git保存检查点也很有价值,但是否这样做由您决定。代码现在如下所示:e函数因不再使用而被删除,if (!o) {...语句也被删除,因为它对分析没有帮助。

您还会注意到,我已将 f 重命名为 newModuleInstance。对于这么短的代码,这一点并不重要;但如果代码可能长达数百行,就必须尽可能确保所有内容清晰易懂。
现在,payload A的大部分内容已经完成反混淆,我们可以逐步分析它,了解其作用。
第3行导入外部数据。
第4行从环境中获取密码。process.env允许从Node脚本中访问变量,而npm_package_description是Node包管理器npm在运行package.json文件中定义的脚本时设置的变量。
第5行创建了一个解密器实例,并将npm_package_description的值用作密码。这意味着,只有通过npm执行此脚本并且针对的特定项目在package.json中包含指定的description字段时,才能解密加密的有效负载。这可不容易。

第6行和第7行对外部文件中的第一个元素进行解密,并将其存储在变量“decrypted”中
第8至11行创建一个新模块,然后将解密后的数据传入未公开的方法_compile。该模块随后导出外部数据文件中的第二个元素。module.exports是node用于在模块之间公开数据的机制,因此newModuleInstance.exports(data[1])会公开外部数据文件中的第二个加密有效负载。

此时,我们已经获得了加密数据。只有使用某个package.json中找到的密码才能解密这些数据,解密后的数据会传入_compile方法。现在还剩下一个问题:密码未知时,如何解密数据?这并非易事。如果aes256加密很容易被暴力破解,我们面临的问题就不只是npm软件包被接管了。幸运的是,可能的密码范围并非完全未知,而是曾被输入某个位置的package.json中的任意字符串。package.json最初是npm软件包元数据的文件格式,因此不妨从npm官方注册表入手。幸运的是,有一个npm软件包可以为我们提供所有软件包元数据的数据流。

无法保证目标文件位于npm包中。许多非npm项目也使用package.json存储基于Node.js的工具配置,而且package.json中的描述可能因版本而异,但从这里入手是个不错的选择。使用多个密钥都可能解密此有效负载,但结果可能是乱码,因此在暴力破解过程中,我们需要通过某种方式验证解密后的有效负载。由于我们处理的内容会传入Module.prototype._compile,后者会将其传入 vm.runInThisContext,因此可以合理推断输出为JavaScript,并可使用任意JavaScript解析器验证数据。如果密码不正确,或者密码正确但解析器抛出错误,就需要继续尝试下一个package.json。Shape Security恰好构建了自己的一套JavaScript解析器,可用于JavaScript和Java环境。所用的暴力破解脚本如下:

运行92.1秒并处理740543个软件包后,我们得到了密码“A Secure Bitcoin Wallet”,该密码成功解码了下方的有效负载:

这次很幸运。原本可能需要进行海量暴力破解的问题,最终不到100万次迭代就解决了。受影响的软件包正是比特币钱包Copay的客户端应用,其中包含上述密钥。接下来的两个有效载荷将进一步深入该应用本身。鉴于目标应用的核心功能是存储比特币,您大概已经猜到接下来会发生什么。
如果您对这类话题感兴趣,并希望阅读另外两个有效载荷或未来攻击的分析,请务必为本文“点赞”,或在Twitter上联系我:@jsoverson。
关于作者
相关博客文章
Thingbot正转变为攻击平台
Lori MacVittie重点介绍了F5 Labs对2018年1月至6月全球物联网设备攻击的研究,并指出了其中令人警醒的影响。物联网设备不仅持续遭到利用,还被转变为攻击平台,这意味着攻击者可以更充分地利用这个必然增长的市场。
借助F5全新Quick Start,在AWS上快速开展业务
在AWS re:Invent开幕前,我们很高兴与大家分享F5与AWS之间全新的Quick Start集成。F5的Tom Atkins将介绍该服务如何启动、配置和运行在AWS上部署工作负载所需的计算、网络、存储及其他AWS服务。