引言
为了庆祝网络安全月,恶意软件分析与逆向工程团队 (MARE) 很高兴能够参与Mandiant 的 FLARE-ON 挑战赛。FLARE-ON 对于所有背景和经验水平的参与者来说都是一个极好的学习恶意软件分析的活动。今年的挑战赛包含11个不同的逆向工程挑战,涵盖一系列有趣的二进制文件。我们非常享受解决这些挑战,并将我们的解决方案发布到Elastic 安全实验室。
挑战 1 - “Flaredle”
欢迎来到 FLARE-ON 9!你可能赢不了。也许你像我们一样,一年都在玩 Wordle。我们制作了自己的版本,如果不作弊很难打败它。在以下网址在线玩:http://flare-on.com/flaredle/
解法
下载并解压文件后,我们看到4个文件对象。
index.html 文件和相应的 js 文件表明这是一个 HTML/JavaScript 挑战。打开 script.js 文件证实了我们的猜测。对于训练有素的人来说,代码的前几行就能清楚地看出答案。让我们解释一下。
在第9行,**rightGuessString** 的值转换为 WORDS[57]。即使你不知道 JavaScript,变量和迭代循环也表明对用户提供的猜测 (rightGuessString) 和硬编码值进行了评估。如果我们查看 words.js 的内容,我们会在第58行(JavaScript 数组从0开始,但文件从第1行开始)看到正确的值:“flareonisallaboutcats”。
通过访问在线游戏并提交此字符串,我们可以验证第一个挑战的正确标识符!
挑战 2 - “Pixel Poker”
我说你赢不了上一个,我撒谎了。上一个挑战基本上是一个验证码。现在真正的工作开始了。我们玩另一个游戏吧?
解法
这个挑战包含一个席卷全国的32位Windows应用程序,名为 Pixel Poker!用户只有10次机会点击窗口中的正确像素,然后程序就会终止。
10次失败尝试后的错误消息提供了一个可靠的线索,我们专注于点击限制的实现位置。我们将10的十进制值转换为十六进制 (0xA) 并启动了一个立即值搜索。
我们搜索的第一个结果列出了说明:**cmp eax, 10**。“cmp” 是一个比较“eax”的内容与数字十的数学指令。乍一看,这似乎是该点击限制背后的逻辑。
通过查看反编译的代码,我们可以确认这是我们预期的目标指令,以及我们在之前的屏幕截图中看到的10次尝试后的错误消息。我们离知道在窗口中点击的位置更近一步了。
为了找到验证逻辑和那些坐标,我们查看与之前的错误消息非常接近的代码。我们观察到两个实例,其中 EAX 寄存器使用字符串 (“FLAR”) 和 (“E-On”) 进行填充,然后除以硬编码值并与我们点击的像素值进行比较。
经过这些简单的操作后,我们得到了两个坐标 (95, 313)。如果你想挑战一下,而且咖啡喝得不太多,那就点击那个像素吧。
也可以利用调试器并在两个 JNZ(非零跳转)指令(出现在前面提到的比较检查之后)上启用零标志 (ZF) 来获得标识符。此方法允许我们绕过手动点击正确的像素位置。
为了好玩,我们编写了一个小程序来修补点击限制,并使用 SendMessage API 强行点击所有可用的像素。
两分钟后,大约100,000次点击后,标识符就显示出来了。
标识符:_w1nN3r_W!NneR[email protected]
挑战 3 - “Magic 8 Ball”
你有问题吗?问问8号球!
解法
这个挑战似乎是一个使用开源 SDL 库 开发的交互式 8 号球游戏。根据快速的观察,有两个明显的输入可以移动 8 号球的方向(左、上、下、右),还有一个输入框,最多可输入 75 个字符。
第一个起点是追踪应用程序中显示的字符串“按箭头键摇晃球”。包含此字符串的函数的反编译视图显示,在其正上方的另一个字符串正在被复制 (“gimme flag pls?”)。
我们的下一个重点是查看调用此函数的代码以获取更多上下文。软件执行完毕并显示游戏后,“do while”循环会轮询输入。
我们审查的一个函数很突出,它包含多个基于单个字符值的“if then”条件语句。
我们的恶意软件分析师从孩童时代就开始他们的职业生涯,他们每天玩电子游戏的时间长达数小时——对他们来说,这种模式类似于Konami 代码,玩家通过输入一系列输入(左、左、上、右、上、左、下、上、左)来启用未公开的功能。
通过首先按照此操作顺序移动 8 号球,然后输入先前恢复的字符串 (“gimme flag pls?”),我们解锁了标识符。
标识符:UcRackeD_th1$_maG1cBaLL!! [email protected]
挑战 4 - “darn_mice”
“如果它崩溃了,那就是用户错误。”——Flare 团队
解法
第四个挑战是一个 32 位 PE 二进制文件。在不带任何参数的情况下执行时,二进制文件最初似乎短暂运行后就终止了。但是,当使用参数运行时,我们会看到一条奇怪的错误消息。
在 IDA 中打开二进制文件并追踪该错误后,我们确定第一个参数正在传递给函数 sub_401000。
在这个函数中,我们看到我们的输入被添加到一个常量数组的值中,并且在第 51 行,我们看到结果被执行为代码。这意味着我们的输入和数组中的值被解析为一个操作码,然后返回。如果你在跟着操作,这意味着 NOP 操作码 (0x90) 不是一个选项。我们正在寻找的操作码是 RET (0xC3):我们从 IDA 中复制了字节序列,并在 Python 中拼凑了一个计算。
arr = [0x50,0x5E,0x5E,0xA3,0x4F,0x5B,0x51,0x5E,0x5E,0x97,0xA3,0x80,0x90,0xA3,0x80,0x90,0xA3,0x80,0x90,0xA3,0x80,0x90,0xA3,0x80,0x90,0xA3,0x80,0x90,0xA3,0x80,0x90,0xA2,0xA3,0x6B,0x7F]"".join([chr(0xC3 - c) for c in arr])
使用当前输入,我们可以检索标识符。
标识符:iw0uld_l1k3_to_RETurn_this[email protected]
挑战 5 - “T8”
FLARE 事实 #823:研究表明,C++ 逆向工程师的平均朋友数量少于普通人。这就是你在这里逆向分析代码而不是和他们在一起的原因,因为他们根本不存在。我们在我们的一个主机上发现了一个未知的可执行文件。该文件已经存在一段时间了,但是我们的网络日志只显示了一天可疑的流量。你能告诉我们发生了什么吗?
解决方案
在这个挑战中,除了二进制文件外,我们还得到了一个 PCAP 文件。
PCAP 文件概述
PCAP 包含二进制文件和 C2 服务器(未提供)之间的通信。在研究了数千个 PCAP 文件后,我们注意到二进制文件和 C2 服务器之间存在类似于 Base64 的交换。
二进制文件概述
这个二进制文件似乎是用 C++ 编写的,或者以类似的方式实现了类。
如果这个二进制文件是用 C++ 编写的,我们的目标是找到 VTABLE 并重建它。目标 VTABLE 位于 .rdata 段的地址 0x0100B918,这意味着我们可以停止猜测它是否是 C++ 了。
重命名 VTABLE 函数可以使分析更容易更高效。我们逐步执行代码,一些操作脱颖而出。跟踪执行流程,位于 0x0FC1020 的函数使用 srand 和 rand API 随机生成 5 位数字,生成了一个伪随机字符串。将其附加到子字符串 FO9 后,整个字符串将进行 MD5 哈希。
字符串“ahoy”使用 MD5 哈希作为密钥进行 RC4 加密,然后结果被 Base64 编码并使用 HTTP POST 请求发送到服务器。从 C2 发送回来的数据将被 Base64 解码,然后使用相同的 MD5 哈希解密。要继续进行挑战,我们需要运用对这种配置的理解。
我们的下一个目标是暴力破解随机字符串以导出 RC4 密钥。为此,我们编写了一个脚本来生成该八字符字符串所有可能值的词表,该字符串类似于“FO9<5DIGITS>”。我们也知道字符串“ahoy”被此过程加密和编码,这意味着我们可以通过搜索“ydN8BXq16RE=”在 PCAP 中查找该字符串。
我们的脚本告诉我们随机字符串 (F0911950) 和哈希值 (a5c6993299429aa7b900211d4a279848),因此我们可以模拟 C2 服务器并重放 PCAP 来解密数据。但是,如下面的屏幕截图所示,只需在 decrypt_server_data 函数之后设置一个断点,我们就可以找到 flag。
Flag:is33[email protected]
挑战 6 - “à la mode”
FLARE 事实 #824:如果你也是 .NET 逆向工程师,请忽略 FLARE 事实 #823。现在我们将用一个小型二进制挑战来奖励你出色的努力。你已经做得很棒了,孩子!
解决方案
这个挑战以一种令人毛骨悚然熟悉的方式开始:一个事件响应聊天日志和一个 .NET DLL。
聊天日志提供了另一个(缺失的)组件可能与 DLL 交互的线索。
经常使用 .NET 样本,你就会熟悉 dnSpy。我们立即发现 DLL 的一个名为 GetFlag 的函数,其中包含连接到名为 FlareOn 的命名管道的客户端代码。
根据之前的线索,我们知道这个 DLL 还有更多内容。我们在 IDA 中打开它,并注意到一些有趣的字符串,这些字符串看起来表面上很相似。
交叉引用这些字符串,我们找到程序中使用的简单加密函数,使用单字节 XOR (0x17)。在这个函数中,库导入与命名管道功能一致。
注释库并回顾此功能后,它建立了一个命名管道并执行验证。
当连接发生时,此验证函数使用新的字符串加密函数和字符串比较 (lstrcmpA)。
有了这些信息,我们使用 x64dbg 将此验证函数设置为起始函数并检索解密后的 flag。
Flag:M1x3dM0dE[email protected]
挑战 7 - “anode”
你已经走到了这一步!我简直不敢相信!而且还有很多人领先于你!
解决方案
这个挑战是一个 55 MB 的 Windows PE 文件,它似乎是一个打包的 Node.js 二进制文件。当执行二进制文件时,它会要求输入 flag 并返回“请重试”错误消息。
![](/assets/images/flare-on-9-solutions-burning-down-the-house/image40.jpg
方便的是(但没有帮助),我们在搜索字符串时可以看到它。
我们可以使用 HxD 十六进制编辑器更好地定位它,它在一个更大的清晰文本代码块中显示出来。
这段代码还告诉我们,flag 的长度应为 44 个字符。不过,有时错误的答案也能告诉你足够的信息来得到正确的答案。不过,这次尝试产生了一个新的错误。读者应该注意,这次尝试巧合地使用了解包版本。
该错误消息出现在我们发现的清晰文本代码块中,这有助于我们找到负责的逻辑并更接近正确的 flag。
奇怪的是,使用打包的二进制文件提交相同的错误 flag 时,错误是不同的。
![](/assets/images/flare-on-9-solutions-burning-down-the-house/image40.jpg
如果我们注释掉该条件以绕过该验证,我们将得到另一个新的错误。
肯定发生了一些事情,虽然实验揭示了一些事情,但我们应该完成对这段清晰文本代码块的审查,以了解挑战的工作原理。它似乎 flag 会在一个我们需要弄清楚的状态机中提交和转换。
并且该状态机操作的结果将针对正确的 flag 进行评估。
但是现在我们有一个不同(更大)的问题,因为它看起来每个值都与 math.random 函数提供的随机生成的值进行了 XOR 加密。我们也不知道产生预期操作序列的值序列。但这在挑战二进制文件中是可行的,这意味着存在一个固定的随机数序列。
我们需要转储这些值,我们可以通过修补挑战二进制文件使用的脚本并将该值序列写入文件来做到这一点。
我们还使用相同的方法转储状态序列。
现在我们可以修补二进制文件以输出值和状态的两个序列,这使得调试变得容易得多。
我们拥有了反转操作及其顺序所需的元素,但我们感觉很懒,所以让我们为自己构建一个 Javascript 反混淆器!这将有助于摆脱该状态机并反转加密以揭示 flag,我们使用的是 pyesprima Javascript 前端。首先,我们将创建一个继承 esprima.NodeVisitor 类的类,并且能够访问 JavaScript 抽象语法树 (AST)。
接下来,我们访问 AST 并收集与 switch case 节点关联的每个子树。
对于先前提取的每个状态,我们测试 if/else 节点的条件并选择正确的分支的内部子树。谓词要么是文字,我们直接测试其值,要么谓词是 Math.random 调用,因此我们测试下一个值。
最后,对于每个表达式,我们确定它是否包含 Math.floor(Math.random) 调用,然后将其替换为正确的随机值,然后对于当前状态,将原始子树替换为我们的表达式。
Pyesprima 不会从其 AST 返回 JavaScript 代码。因此,我们实现了一个非常小的 JavaScript 代码发射器,它递归地将每个节点替换为正确的 JavaScript 代码。
但是,在比较反混淆脚本和打包的二进制文件后,我们仍然没有得到相同的结果!
除了 math.random 之外,还必须有一些恶作剧。通过测试我们很快发现,if(x) 和 if(xn)(x 为数字)具有两种奇怪的不同行为。如果数字 > 0,则 if(x) 总是返回 false;如果数字包含零,则 if(xn) 总是返回 false!
因此,考虑到这一点,我们在再次运行反混淆器之前修复了脚本中的谓词。
这看起来像是我们的混淆脚本。
让我们反转这种混淆。
最终反转的脚本,其中“target”作为初始 flag,如下所示
有兴趣了解为 FLARE-ON 挑战创建的脚本的读者可以在本文末尾找到链接。
运行脚本最终会生成一个数组。
Flag:n0tju5t_A_j4vaSCriP7[email protected]
挑战 8 - “Backdoor”
我就是一个后门,来反编译我吧……
解决方案
这个挑战包含一个 11MB 的 Windows PE 二进制文件,它在启动时执行,但不会向控制台返回任何内容。我们经常使用数据包捕获来增强分析,并在观察到 DNS 解析事件时使用 Wireshark 监听。我们已经取得了良好的开端。
我们注意到一个可能很重要的约定:我们有 flare_xx 函数及其对应的 flared_yy 函数。如果我们检查 flare_xx 函数,它们每个都包含一个“try/catch”结构。
![](/assets/images/flare-on-9-solutions-burning-down-the-house/image31.jpg
但是当我们转向查看它们的 flared_yy 对应函数时,有些事情不太对劲。
在 dnSpy 中,我们跟踪执行到 InvalidProgramException 并没有到达 flared_yy 代码。但是尽管如此,挑战似乎在某种程度上成功地执行了。
从 main 开始并分析第一个函数,我们大致了解了正在发生的事情:有两层“try/catch”逻辑以不同的方式做类似的事情,并以某种方式通过参数创建动态方法中间语言 (IL)。
第一层 flare_71 使用直接作为参数传递的 IL 构造一个动态方法
![](/assets/images/flare-on-9-solutions-burning-down-the-house/image31.jpg
在调用 SetCode 之前,会发生一些幕后工作来使用具有动态方法上下文的元数据标记来修补 IL 代码。位置和元数据标记的字典也通过在相同上下文中调用 GetTokenFor 来解析。
修补后,IL 仅在动态方法的上下文中有效。为了正确重建二进制文件,现在我们需要在修改 IL 之前转储它,使用正确的元数据标记对其进行修补,然后修补二进制文件以修复损坏的函数。
我们可以用 Python 创建一个脚本来做到这一点。
修补二进制文件的第一层后,它可以正确反编译。但是,负责运行第二层混淆的 flared_70 函数则比较复杂。
该函数将按名称读取其 PE 段之一,使用元数据标记哈希的前 8 个字符,并对应于引发 InvalidProgramException 错误的函数。这是使用硬编码密钥解密的。解密后的部分包含要调用的函数的 IL。
这次 IL 修补有点复杂,涉及一些混淆。
下一个问题是我们事先没有所有哈希值,只有在调用函数时才有。如果我们在解析函数上设置断点,我们可以转储每个哈希值。
我们编写了一个脚本来自动执行修补并每次添加新哈希值时运行它。
此时大多数函数都已反混淆,我们可以继续进行挑战的核心部分。
起初我们观察到大量的DNS解析事件,但没有看到恶意软件尝试连接到我们的Flask服务器。然而,在调试样本时,我们发现了一些看起来像是尝试处理命令的行为。
问题是,我们仍然不知道如何与后门交互。通过回溯到每个命令的来源,我们可以看到这个样本正在使用从这些DNS解析接收到的IP地址进行通信。现在我们至少知道了为什么没有看到这个样本尝试连接到我们的Flask服务器。
这究竟是如何运作的,我们即将了解,这有点复杂。
第一个IP地址用于创建文件,之后所有命令都以“255.x.y.z”网络地址的形式到达。样本会解析返回的每个IP地址的每个八位字节,但用一个具体的例子可能更容易理解。
当DNS解析返回255.0.0.2时,后门程序期望两个特定的数据字节(43d和50d),这两个字节用于计算一个表面上类似于网络地址的值,即43.50.0.0。然后,命令处理函数执行比较并将一个0到22之间的值附加到后面。
flared_56函数将数组中的一个值与248进行异或运算,以确定结果是否等于参数中传递的值。如果是,它将一小段文本附加到对象的属性之一,然后从数组中删除该值。
这告诉我们应该发送哪个命令以及以什么顺序附加所有文本块。我们还注意到,当数组值为空时,_bool标志被设置为false。这可能并非偶然,所以让我们检查一下使用该标志的所有函数。
每次从value数组中删除元素时都会触发此函数。
我们可以预期一旦满足正确的条件,某些事情就会发生,并努力创造这些条件。
首先,我们生成了所有可能的IP地址值列表。然后,我们配置了FakeDns将*.flare-on.com解析到该值列表。
接下来,我们使用FakeDns通过轮询的方式响应请求,依次解析每个IP地址,直到最终得到我们等待的响应。
*标志:*_W3_4re_Known_f0r*[email protected]
挑战9 - “加密器”
你已经做得非常出色,才走到这一步。这可能是你的终点了。祝你明年好运!
解决方案
在这个挑战中,我们得到了两个文件:一个Windows PE可执行文件和一个加密文件。
加密方式很有趣,当我们在HxD中打开它时,我们立即看到一堆垃圾数据,后面跟着十六进制数据。
执行二进制文件时,它会提示需要一个路径作为参数。
但是选择随机文件时没有任何反应,所以我们可能需要一个不太随机的文件。
我们首先在IDA中跟踪该函数,并注意到它正在查找特定的扩展名“.EncryptMe”。
让我们再尝试一个使用该特定文件扩展名的随机文件。
我们会看到一个使用不同扩展名(“.Encrypted”)并具有更大文件大小的新文件。
更仔细地查看IDA中的可执行文件,我们确定该二进制文件使用ChaCha20,并使用RSA-2048加密随机密钥。
我们需要这个密钥。
从最基本的层面来说,加密只是由加法和乘法等基本运算组成的数学系统。RSA被认为是一种强大的实现,因为它使用大数,并且大多数RSA库都实现某种大数库。但是我们并不想为了密钥而反向工程所有这些内容,尤其是在我们可以在样本中找到所有相关函数并应用我们的RSA知识的情况下。
我们需要为两个变量p和q生成素数。
我们需要生成模值n,它等于p*q。使用p和q作为输入,返回n。到目前为止,一切顺利。
我们还需要一个phi值,它等于(p-1)*(q-1)。
我们推断前两个函数是生成p-1和q-1的减法函数。
最后,我们有一个操作,它使用phi和指数e生成密钥d。
但是请注意,一些可疑的事情已经发生了,因为包含指数e的全局变量被重用,并将包含私钥d。现在我们至少可以验证密钥是用私钥(d, n)而不是公钥(e, n)加密的。
我们可以使用公钥解密ChaCha20密钥,但是我们不知道模值或加密密钥。幸运的是,它们都被转换成十六进制并附加到加密输出文件中。
加密的ChaCha20密钥实际上包含在init结构的最后三行中,以及nonce。
可以使用一些Python代码解密密钥。
我们又近了一步。
通过使用x64dbg跟踪执行,我们可以通过将ChaCha20参数替换为我们刚刚获得的密钥和nonce来强制解密加密文件。又找到一个标志,还有一个要找!
*标志:*_R$A$16n1n6_15_0pp0$17e*[email protected]
挑战10 - 卡拉OK迷宫
不知何故,团队的每个成员都拥有近乎百科全书式的歌词知识,并凭直觉解决了这个问题。令人惊讶的是,不需要逆向工程。
挑战11 - “不可言说的挑战”
保护、混淆、限制……哦,我的天哪!这方面的好处是,如果你未能解决它,我不需要给你寄奖品。
解决方案
这是FLARE-ON 9的第十一也是最后一个挑战,在经历了之前的几个挑战之后,出乎意料地简单直接。这个挑战包含一个二进制文件,运行strings命令可以得到一些提示。
“PyInstaller将Python应用程序及其所有依赖项捆绑到单个包中”很好地总结了PyInstaller的用途。这个二进制文件是从Python脚本编译并打包成单个可执行文件的,这不像看起来那么棘手。我们经常遇到这种情况,因此我们找到了工具来提取以这种方式编译的python代码,我们提取了一些python文件。
其中一个文件11.py在我们尝试单步执行时抛出错误,并抱怨库“‘crypt’ has no attribute ‘ARC4’”。
这很有趣。值得注意的是,我们可以修改位于“PYTHON_FOLDER_PATH\lib\crypt.py”中的crypt.py脚本,添加ARC4函数及其返回的类以及我们的自定义加密函数。
当我们再次运行11.py时,这次它打印出一个漂亮的标志,让我们从FLARE-ON挑战的梦境(或噩梦)中醒来。
*标志:*_Pyth0n_Prot3ction_tuRn3d_Up*[email protected]
结论
对于2022年的FLARE-ON挑战,就到这里了!今年我们学习了很多新东西,希望您喜欢阅读我们的解决方案。我们期待阅读你们的解决方案,并学习我们没有尝试过的事情。
对于那些耐心等待脚本链接的人,这里就是链接。
鸣谢
我们要感谢Elastic和Devon Kerr,他们给了我们机会专注于这个活动一周。还要感谢Mandiant团队提供的有趣且有深度的挑战:干得漂亮。感谢所有参与研究的各位,感谢你们让这成为一个非凡的学习周。