导语:夺旗(CTF)比赛的一个非常有趣的方面是经常需要拾取新知识,学习和应用各种逆向工程和二进制分析工具来解决比较难突破的挑战。最近我完成了FireEye FLARE-On 2017的挑战,我在我的二进制分析虚拟机中添加了一些工具。我想在这个博客文章
夺旗(CTF)比赛的一个非常有趣的方面是经常需要拾取新知识,学习和应用各种逆向工程和二进制分析工具来解决比较难突破的挑战。最近我完成了FireEye FLARE-On 2017的挑战,我在我的二进制分析虚拟机中添加了一些工具。我想在这个博客文章中分享这些工具,并展示他们是如何帮助我完成挑战的。
使用XDebug设置PHP动态分析环境
在Two Six Labs工作时,我主要从事二进制逆向工程和分析工作,所以CTF中的web挑战题目通常需要我的努力才能成功完成。对于那些不熟悉的人来说,PHP是一种用于服务器端应用程序的脚本语言,而JavaScript通常用于客户端应用程序。JavaScript的静态和动态分析相对比较简单 – 大多数浏览器都具有开发工具,可以让你交互式地查看和调试JavaScript。但是,设置PHP调试环境稍微复杂一些。
挑战题目
在FLARE-On 2017挑战赛的第十个挑战题目中,给我们提供了下面这个PHP脚本。
<?php $o__o_ = base64_decode('<Base64 block omitted>'); $o_o = isset($_POST['o_o']) ? $_POST['o_o'] : ""; $o_o = md5($o_o) . substr(MD5(strrev($o_o)), 0, strlen($o_o)); for ($o___o = 0; $o___o < 2268; $o___o++) { $o__o_[$o___o] = chr((ord($o__o_[$o___o]) ^ ord($o_o[$o___o])) % 256); $o_o .= $o__o_[$o___o]; } if (MD5($o__o_) == '43a141570e0c926e0e3673216a4dd73d') { if (isset($_POST['o_o'])) @setcookie('o_o', $_POST['o_o']); $o___o = create_function('', $o__o_); unset($o_o, $o__o_); $o___o(); } else { echo '<form method="post" action="shell.php"><input type="text" name="o_o" value=" "/><input type="submit" value=">"/></form>'; } ?>
上面的代码本质上是一个大的base64编码过的文本被解码,用一个密钥解密,并作为第二阶段的PHP函数执行。密钥以$o_o变量的MD5哈希开头(这是Flag的标志)。附加到MD5哈希的字符串的取值是反向的$o_o变量的MD5哈希字符串被截断到该标志的长度。假设该标志的长度大于0,我们可以得出结论:所需的密钥在32到64个字符之间(2个字符代表一个十六进制字符串中的一个字节)。
这将需要一些手动脚本,一个调试器和一点意念来破解出正确的密钥字符串。那么让我们来设置一个开发或调试环境吧?
环境设置
我在64位Ubuntu 16.04 LTS虚拟机中为这个挑战题目构建了一个开发调试环境。首先,我在 这个网站上找到了安装PHPStorm IDE的命令。接下来,创建一个项目,并复制粘贴shell.php代码然后保存。
然后,我使用包管理器在虚拟机上安装了XDebug:
sudo apt-get install php-xdebug
现在我们已经设置了一个开发环境和一个调试器,我们需要能够让他们彼此交互来调试我们的PHP脚本。首先,我们必须配置PHP来了解XDebug,端口和主机发送调试消息是必需的参数。导航到/etc/php/7.0/cli目录并编辑其中包含的php.ini文件并包含下面的内容。
zend_extension = /usr/lib/php/20151012/xdebug.so xdebug.remote_enable=1 xdebug.remote_port=9000 xdebug.remote_host=127.0.0.1 xdebug.idekey=PHPSTORM
这些环境变量告诉php进程XDebug库在哪里(你可能需要编辑相应的zend_extension变量),启用远程调试,建立调试主机和端口,并确定我们用来调试的IDE的密钥(在这里是PHPSTORM)。保存文件并返回到PHPStorm,以便我们可以对其进行配置。
在PHPStorm中,进入文件- >设置- >语言和框架- >调试,并确保“XDebug”的设置如下图所示:
接下来,安装浏览器的XDebug扩展,你将被导航到PHP脚本,该扩展将与PHPStorm进行通信来进行调试。我按照Google Chrome的说明操作,点击这里查看文档。
现在,我们准备启动我们的PHP服务器,导航到页面并调试我们的PHP脚本。打开PHPStorm,在PHP脚本的第一行设置一个断点,然后单击运行 – >“开始侦听PHP调试连接”。接下来,在终端中导航到PHPStorm项目目录并使用以下命令启动服务器:
php -S 127.0.0.1:8888
现在,你可以用浏览器打开http://127.0.0.1:8888/shell.php,如果全部配置正确,程序执行应该停在了你设置的断点。现在我们可以开始编写和调试更多的代码了!
挑战题目的解决方案
我们需要做的第一件事是编写一些粗糙的PHP代码来确定密钥的长度以及可能的字符。我们知道密钥字符串的长度在33到64之间,我们也知道密钥字符串只包含字符'a' – 'f'和'0' – '9',因为这些字符是唯一的可以出现在哈希摘要中的字符。我们也希望明文只能是可读的字符,因为它是PHP代码,所以它只包含字母数字字符,制表符,换行符和回车符。利用这些知识,我们可以编写一些PHP代码来打印每个位置的所有可能的长度和可能的字符。我的代码可以做到这一点,如下所示
<?php $possible_chars = array( '0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'); $massive_string = base64_decode('<Base64 Omitted'); for($k = 33; $k <= 64; $k++) { for($i = 0; $i < $k; $i++) { foreach ($possible_chars as $c) { $key = $c; $found = TRUE; for ($j = $i; $j < strlen($massive_string); $j += $k) { $s = chr((ord($massive_string[$j]) ^ ord($key)) % 256); // Tab, Newline or Carriage Return if (ord($s) == 9 || ord($s) == 13 || ord($s) == 10) { $key = $s; continue; } # Non-printable if (ord($s) < 0x20 || ord($s) >= 0x7f) { $found = FALSE; break; } $key = $s; } if ($found == TRUE) { printf("%d %d %s<br><br>", $k, $i, $c); } } } } ?>
如果代码找到了一个纯文本中只有可读的ASCII字符的位置,它将只显示一个长度。密钥长度是密钥内的每个位置提供一个可能的字符,其长度为64,因此密钥长度为64。下面是密钥中每个字符位置的所有可能字符(挑战题目作者给出了很好的列表https://www.fireeye.com/content/dam/fireeye-www/global/en/blog/threat-research/Flare-On%202017/Challenge10.pdf)。你可以通过简单地注释掉PHPStorm中的shell.php代码,粘贴代码并刷新页面来尝试执行此代码(这就是为什么开发环境很有用!)。
我们可以使用这些值来开始构建密钥,其中可能的字符是唯一的。然后我们可以使用我们的调试器来检查解密后的PHP代码。下面是一个例子:
然后,我们可以在解密后的输出中识别PHP关键字(例如索引716处的“b-e64”),并使用PHPStorm的控制台和调试功能通过反复试验和一点点直觉重建整个密钥 键字符串
“db6952b84a49b934acb436418ad9d93d237df05769afc796d067bccb379f2cac”。然后,我们可以将第2阶段的PHP代码从调试器中提取出来,并将其放到项目中的脚本中 – 我称之为“shell2.php”,如下所示。
shell2.php采用最初的“$ o_o”变量(挑战标志)并将其分成三个字符串。这三个字符串作为密钥来解密显示来自http://www.p01.org的JavaScript动画的三个HTML页面之一。知道这一点,我们可以采用类似的策略来应对挑战的第一部分 – 使用已知的明文来获得密钥。我们知道每个加密块的明文将以“<html>”开始,因此我们可以在解密阶段为PHB设置断点,以便使用调试控制台来导出密钥的前几个字节。
使用第一个解密的blob并在解密之前破解,我们可以使用调试器控制台来用“<html>”对密文blob进行异或运算,以获得密钥的前几个字节 – “t_rsaa”。剩下的唯一的问题是 – 我们如何弄清确切的密钥长度?那么答案就在于加密的blob本身 – 有一个13 0x0字节的字符串 – 表示在这些地方匹配的密钥和明文 – 因此我们可以尝试13的密钥长度,看看我们是否能得到正确的明文。为了获得明文,我们将“t_rsaa”填充到13个字节,使用调试控制台,并在结果明文字符串中查找关键字,以导出剩余的密钥字节。
正如我们在上面的调试会话中看到的那样,在将密钥填充到13个字节后,开始出现熟悉的字符串。“Ray”和“heckbo”很可能是字符串“Raytraced Checkboard”的一部分。这使我们能够找出第一个blob其余部分的关键字节。第一个blob的关键字节是“t_rsaat_4froc”。如果我们为其他密文块重复这个思路,我们最终得到“hx__ayowkleno”和“[email protected]”。使用这些字符串,我们可以通过从每个字符串中一次取一个字符来重建标志,产生“[email protected]”。
使用simavrAtmel进行AVR仿真
我总是对系统体系结构执行二进制的分析过程很感兴趣,这些体系结构与在大多数现代移动和台式机器上运行的标准x86 / x64 / ARM / AArch64二进制文件不同。在FLARE-On 2017的挑战中,我遇到了一个Atmel AVR二进制文件 – 更具体地说,是一个使用ATMega328p处理器在Arduino板上运行的.hex文件。不幸的是,当我处理这个挑战题目时,我没有一个Arduino的模拟器,所以我必须找到一个工具来让我可以模拟一下。
挑战题目
2017 FLARE-On挑战赛中的挑战题目9向二进制分析师展示了一个.hex文件以及一个暗示Arduino板的描述。题目描述没有提供其他背景信息。.hex文件是一系列十六进制字符串的ASCII表示,因此我将它们复制到文本编辑器中,并找到字符串“Flare-On 2017 Adruino UNO数字引脚状态:。”Arduino UNO使用8位ATMega328p处理器。了解到这一点,我们可以开始建立一个分析环境。
环境设置
对于静态分析。我发现使用的最简单的工具就是IDA Pro – 任何逆向工程师工具包的主要工具。如果在“加载新文件”对话框中将指令类型更改为“Atmel AVR”,则IDA Pro能够接收原始的.hex文件并对其进行反汇编,如下图所示。
之后,IDA将提示用户选择一个处理器。我的IDA Pro版本没有“ATMega328p”作为选项,所以我选择了“ATmega103_L” – 同一个处理器系列。之后,AVR操作码应该可以显示出来。
进行动态分析。我决定使用simavr。Simavr是Atmel AVR处理器的Python仿真器。特别吸引我的是,它完全支持GDB – 这意味着我们实际上可以在汇编级进行调试。simavr的GitHub页面包含了安装说明。
挑战题目解决方案
用IDA进行了一段时间的静态分析之后,我发现了一个可疑的解密循环函数。如下图所示。
一组长度为23的静态字节被插入到“Y”寄存器指向的数组中(因为ATMega328p是一个8位处理器,它使用三个特殊的存储器访问寄存器,称为“X”,“Y”和“ Z.“X寄存器基于r27:r26,Y寄存器基于r29:r28,Z寄存器基于r31:r30)。这些字节与存储在r24中的密钥字节进行XOR(Z和Y指向同一个数组),然后将数组的索引添加到字节值中。然后将其存储在由X寄存器指向的数组中。在解密之后,我们看到内存地址0x576处的值与“@”字符进行比较 – 指示正确的解密。我们知道FLARE-On挑战的Flag是电子邮件地址的形式,所以我们只需要找出'@'字符大小为23的数组中的索引。然后,我们可以编写一个程序,强制使用常量密钥字节,然后将解密的Flag从内存中取出。我们可以用gima使用simavr来做到这一点。首先,我们用下面的命令行启动simavr。
run_avr -m atmega328p -f 160000000 --gdb remorse.ino.hex
然后,在一个单独的终端中,我们可以使用以下命令通过gdb连接到模拟器:
avr-gdb -ex "target remote:1234"
然后我们可以在AVR代码中设置断点。首先,我们在IDA标识为“loc_576”的地方设置了一个断点,但实际上是在内存中的0xAEC处。要做到这一点,我们必须使用gdb命令 br * $ pc + 0xaec 。由于某些原因,只能从pc-relative偏移设置一个断点,与simavr一起工作(在执行开始时,$pc的值是0x0)。我们可以继续执行这个程序,当它中断的时候,我们可以运行一个 info reg 命令来确定X寄存器指向哪个地址。
(gdb) info reg r0 0x80 128 r1 0x0 0 r2 0x0 0 r3 0x0 0 r4 0x0 0 r5 0x0 0 r6 0x0 0 r7 0x0 0 r8 0x0 0 r9 0x0 0 r10 0x0 0 r11 0x0 0 r12 0x0 0 r13 0x0 0 r14 0x0 0 r15 0x0 0 r16 0x0 0 r17 0x0 0 r18 0x0 0 r19 0x0 0 r20 0x2 2 r21 0x0 0 r22 0xa 10 ---Type <return> to continue, or q <return> to quit--- r23 0x5 5 r24 0xff 255 r25 0x8c 140 r26 0x6c 108 r27 0x5 5 r28 0xf6 246 r29 0x7 7 r30 0xf7 247 r31 0x7 7 SREG 0xb5 181 SP 0x8007f6 0x8007f6 PC2 0xaec 2796 pc 0xaec 0xaec
info reg 命令的输出 如上所示。我们可以看到X寄存器在内存中指向0x56C(记住X寄存器包含r27和r26的值)。这意味着该标志的“@”字符位于索引0x576 – 0x56C = 0xA(十进制10)。我们现在可以通过抄录上面列出的解密算法来为Python中的密钥写一个暴力方法。
cipher_text_byte = 0xed test_index = 10 for key in range(256): if (((cipher_text_byte ^ key) + test_index) & 0xff) == ord('@'): print "KEY IS 0x%x" % (key) break
运行这个代码揭示了关键字节是0xDB。现在,我们可以在simavr中再次运行这个程序,在解密循环的开始处设置一个断点,将$ r24设置为0xDB,运行解密并在比较第十个字符到“@”时中断。在这里,我们应该能够在0x56C处打印标志字符串以在存储器中显示解密的标志。这个快速和肮脏的GDB命令应该做的伎俩 – 你可以将其保存到文本文件并运行 avr – gdb – x cmds 。txt 将其作为脚本运行。
# Connect to our debugging session target remote :1234 # Break at main decryption loop br *$pc + 0xaec # Continue c # Set r24 to be the key value we calculated set $r24=0xdb # Disable the breakpoint so we leave the decryption loop dis 1 # Break after the decryption loop br *$pc + 0x12 # Continue c # Print the flag x/s 0x56c
运行这个脚本产生以下内容:
执行脚本后打印出Flag“[email protected]”。或者,我们可以用Python将解密脚本推广到整个数组,但是这么做的乐趣在哪呢?
结论
在这个博客文章中,我分享了两个非常不同的工具和两个截然不同的应用。获取和学习新的工具和技能对于每一个逆向工程师来说都是非常重要的 – 从初出茅庐到退伍老兵。我希望能够传达出我使用这些工具的思路,动机和方法,并能够为读者提供灵感,让读者自己尝试一下。
相关链接
FLARE – 2017年挑战题目打包(解压密码: “flare”)——http://flare-on.com/files/Flare-On4_Challenges.zip
PHPStorm —— https://www.jetbrains.com/phpstorm/
XDebug —— https://www.xdebug.org/
simavr GitHub页面 —— https://www.github.com/buserror/simavr