简介
为了庆祝网络安全月,恶意软件分析和逆向工程团队 (MARE) 很高兴参与了 Mandiant 的 FLARE-ON 挑战赛。FLARE-ON 对于想要了解有关恶意软件分析的更多信息的各种背景和经验水平的参与者来说,都是一项出色的活动。今年由 11 个不同的逆向工程挑战组成,其中包含一系列有趣的二进制文件。我们非常享受这些挑战,并在此处发布了我们 Elastic Security Labs 的解决方案。
挑战 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 - “像素扑克”
我说过你赢不了最后一个。我说谎了。上一个挑战基本上是一个验证码。现在真正的工作开始了。我们再玩一个游戏?
解决方案
这个挑战包含一个 32 位 Windows 应用程序,它风靡全国,名为 Pixel Poker!用户有 10 次机会在程序终止之前单击窗口中的正确像素。
10 次尝试失败后的错误消息提供了可靠的线索,我们专注于实施该点击限制的位置。我们将十进制值 10 转换为十六进制 (0xA) 并启动了立即值搜索。
我们搜索的第一个结果列出了指令:cmp eax, 10。你可能不精通汇编,但“cmp”是一个数学指令,用于将“eax”的内容与数字 10 进行比较。乍一看,这看起来像是该点击限制背后的逻辑。
通过查看反编译的代码,我们可以确认这是我们的目标指令,以及我们在 10 次尝试后在之前的屏幕截图中看到的错误消息。我们离知道在窗口中单击的位置又近了一步。
为了找到验证逻辑和这些坐标,我们查看了靠近先前错误消息的代码。我们观察到两个实例,其中 EAX 寄存器使用字符串 (“FLAR”) 和 (“E-On”) 填充,然后用硬编码值进行除法,并与我们单击的像素值进行比较。
经过这些简单的操作,我们得出了两个坐标 (95, 313)。如果你准备迎接挑战并且没有喝太多咖啡,请继续单击该像素。
还可以通过利用调试器并在两个 JNZ(如果非零则跳转)指令上启用零标志 (ZF) 来获得标志,这两个指令直接出现在前面提到的比较检查之后。此方法允许我们绕过手动单击正确像素位置。
为了好玩,我们编写了一个小程序,用于修补点击限制并使用 SendMessage API 暴力点击所有可用的像素。
两分钟后,大约点击了 100,000 次,标志发布给了我们。
标志:_w1nN3r_W!NneR[email protected]
挑战 3 - “魔法 8 球”
你有什么问题?问 8 球!
解决方案
这个挑战似乎是一个使用开源 SDL 库开发的交互式 8 球游戏。根据快速观察,有两个明显的输入以方向方式移动 8 球(左、上、下、右),以及一个最多 75 个字符的输入框。
第一个起点是追踪应用程序中显示的字符串“按箭头键摇动球”。包含此字符串的函数的反编译视图显示,其正上方正在复制另一个字符串(“gimme flag pls?”)。
我们的下一个支点是查看调用此函数的代码以获取更多上下文。在软件执行并显示游戏后,“do while”循环会轮询输入。
我们审查的一个函数很突出,它包含多个基于单个字符值的“if then”条件语句。
我们的恶意软件分析师从童年就开始他们的职业生涯,他们勤奋地玩电子游戏,每次都玩好几个小时,对他们来说,这种模式类似于 Konami 代码,玩家通过输入一系列输入(左、左、上、右、上、左、下、上、左)来启用未记录的功能。
通过先按此操作顺序移动 8 球,然后输入先前恢复的字符串(“gimme flag pls?”),我们解锁了标志。
标志:UcRackeD_th1$_maG1cBaLL!![email protected]
挑战 4 - “该死的鼠标”
“如果它崩溃了,那就是用户错误。” -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<5 位数字>”。我们还知道字符串“ahoy”通过此过程进行加密和编码,这意味着我们可以通过搜索“ydN8BXq16RE=”在 PCAP 中查找该字符串。
我们的脚本告诉我们随机字符串 (F0911950) 和哈希 (a5c6993299429aa7b900211d4a279848),因此我们可以模拟 C2 服务器并重放 PCAP 以解密数据。但是,如下面的屏幕截图所示,只需在 decrypt_server_data 函数之后设置一个断点,我们就可以找到 flag。
Flag: is33[email protected]
挑战 6 - “à la mode”
FLARE FACT #824:如果您也是 .NET 逆向工程师,请忽略 flare fact #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 并返回“Try Again”错误消息。
![](/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”
我真是一个后门,为什么不反编译我呢……
解决方案
这个挑战由一个 11 MB 的 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 之前转储它,使用正确的 metadatatoken 修补它,然后修补二进制文件以修复损坏的函数。
我们可以创建一个 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。这可能不是巧合,所以让我们检查一下使用该标志的任何函数。
每次从值数组中删除元素时,都会触发此函数。
我们可以预期在满足正确条件后会发生一些事情,并努力创造这些条件。
首先,我们生成了所有可能的 IP 地址值的列表。然后,我们配置 FakeDns 将 *.flare-on.com 解析为该值列表。
接下来,我们使用 FakeDns 使用轮询方法来响应请求,该方法按顺序解析为每个 IP 地址,直到最终我们得到我们等待的响应为止。
*Flag: *_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)。
我们推断出前面的 2 个函数是产生 p-1 和 q-1 的递减函数。
最后,我们有一个使用 phi 和指数 e 生成密钥 d 的操作。
但是请注意,已经发生了一些可疑的事情,因为包含指数 e 的全局变量被重用,并将包含私钥 d。现在我们至少可以验证密钥是用私钥 (d, n) 而不是公钥 (e, n) 加密的。
我们可以使用公钥解密 ChaCha20 密钥,但是我们不知道模数值或加密密钥。幸运的是,它们都被十六进制化并附加到加密的输出文件中。
加密的 ChaCha20 密钥实际上包含在 init 结构的最后三行中,以及随机数。
可以使用一些 Python 代码解密密钥。
我们离目标更近了一步。
通过使用 x64dbg 跟踪执行,我们可以通过将 ChaCha20 参数替换为我们刚刚获得的密钥和随机数来强制解密加密文件。又一个 Flag 被拿下,还剩最后一个!
*Flag: *_R$A$16n1n615_0pp0$17e[email protected]
挑战 10 - 卡拉 OK 迷宫
不知何故,团队的每个成员都对歌词有着几乎百科全书般的知识,并通过直觉找到了解决办法。出乎意料地异想天开,不需要逆向工程。
挑战 11 - “不可言说的挑战”
保护、混淆、限制... 我的天哪!这个挑战的好处是,如果你未能解决它,我不需要给你寄奖品。
解决方案
这是 FLARE-ON 9 的第 11 个也是最后一个挑战,在之前的一些挑战之后,它出乎意料地直截了当。这个挑战包含一个二进制文件,在其上运行字符串给出了一些提示。
“PyInstaller 将 Python 应用程序及其所有依赖项捆绑到一个包中”,这是对 PyInstaller 用途的很好总结。此二进制文件是从 Python 脚本编译的,并打包为一个可执行文件,这不像看起来那么麻烦。我们经常遇到这些,以至于我们找到了 工具 来提取以这种方式编译的 python 代码,并且我们提取了一些 python 文件。
当我们尝试单步执行其中一个文件 11.py 时,它抛出了错误,并抱怨库 “‘crypt’ 没有属性 ‘ARC4’”。
这有点意思。值得注意的是,我们可以修改位于“PYTHON_FOLDER_PATH\lib\crypt.py”中的 crypt.py 脚本,添加 ARC4 函数和它使用我们的自定义加密函数返回的类。
当我们再次运行 11.py 时,这次它打印给我们一个美丽的 flag,它将我们从 FLARE-ON 挑战的梦(或噩梦)中唤醒。
*Flag: *_Pyth0n_Prot3ction_tuRn3d_Up[email protected]
结论
2022 年的 FLARE-ON 挑战就此结束!今年我们学到了很多新东西,我们希望你喜欢阅读我们的解决方案。我们期待阅读你们的解决方案,并学习我们没有尝试过的事情。
对于那些耐心等待脚本链接的人,这是链接。
致谢
我们要感谢 Elastic 和 Devon Kerr,他们给了我们机会花一周时间专注于这个活动。还要感谢 Mandiant 团队提供的有趣且周到的挑战:做得好。感谢参与的研究人员,感谢你们使这一周成为学习的非凡一周。