文章作者: hasherezade(@hasherezade)
原文链接: Import all the things! Solving FlareOn4 Challenge 3 with libPeConv
翻译前言: 虽然依旧是Flareon4第3题的分析,但是一道题的解决方法多种多样,这次给大家分享如何使用libPeConv来解决问题,又可以get到新姿势啦
libPeConv: 是作者hasherezade开发的用于加载和转换PE文件的库,github仓库地址是:libpeconv
ps: 程序可以从附件下载, 程序可能会报毒但是安全的, 建议在虚拟机下操作, 解压密码: www.pediy.com
题目greek_to_me.exe是一个32位PE文件, 程序已经剔除了重定位信息. 我们以下就简称该程序为crackme
我们运行crackme, 只有一个空白的控制台程序, 并且没有从标准输入中读取任何数据, 所以我们可以推断程序是使用了一些其他方式来读取用户的password
我们使用IDA静态分析, crackme结构非常简洁并没有混淆过. 我们可以在代码开头看见程序创建了一个socket并等待着输入
socket监听着本地2222端口
在建立连接后, crackme从用户输入中取前4字节读入到缓冲区中:
读入4字节后, crackme开始处理输入并用来解码已加密的缓冲区数据
如果校验值是合法的, 也就是说加密数据被正确解密了, 那么crackme就会进一步执行下去.
我们可以看到, 输入中的数据只有1字节用于解码缓冲区数据, 所以我们可以轻易地穷举获得结果. 解码部分的代码也相当简单:
程序唯一的难点在于校验值 - 这个函数并没有那么好复现. 然而如果我们想要暴力穷举, 我们却又需要在穷举后计算校验值.
在我之前的解答中, 我复现了校验函数并表现良好, 但这并没有那么好玩. 我看过了一些其他的解决方式如使用Unicorn引擎模拟执行校验函数
, 或使用angr框架, 或通过socket使用暴力穷举程序来获得原始程序等等. 但是我们可以解决得更快速吗?我们来接着看...
使用PeConv我们可以将原始格式的任何PE文件转换成虚拟内存格式并返回. 它也提供有一个可定制的PE加载器 - 用于加载任意PE文件到当前进程(就算它不是dll文件也没有重定位表, 这我会在之后的部分进行解释). 载入的PE文件随后可以在当前进程内运行. 我们也可以选择文件中的任意函数来使用 - 而我们只需要知道函数的RVA和API.
在这次, 我将会使用libpeconv来加载crackme并导入校验值的计算函数. 不用复制加密缓冲区数据到我们的代码中, 我们可以直接从载入的PE文件中读取它.
让我们再一次在IDA中查看crackme. 我们需要找到恰当的偏移量并明白我们需要导入的API函数.
首先我们计算校验值的函数起始于RVA 0x11E6处:
函数读取2个参数: 指向缓冲区的指针和缓冲区大小
函数返回一个WORD类型数据.
总结一下, 我们可以定义一个如下的函数原型:
还有一点需要注意, 就是这个函数是可独用的并且没有调用任何的导入库函数 - 这让我们导入这个校验值函数更加轻松(我们不必加载任何导入库模块或进行重定位).
另一个我们需要的信息就是加密的缓冲区. 缓冲区起始于RVA 0x107C并且长度为0x79(121)字节
信息搜集完毕!我们开始写代码.
当前版本的libpeconv
允许两种方式来载入PE文件. 使用到的函数有load_pe_module
和load_pe_executable
. 第2个函数load_pe_executable
是一个完整的加载器, 它加载指定PE文件到当前进程的可读可写可执行(RWX)内存中, 并自动应用重定位信息和载入其他依赖. 第1个函数load_pe_module
则不能载入依赖并且我们需要提供更多的控制: 我们可能会加载PE文件到一个不可执行的内存中而是否进行重定位也是可选的. 更多详细信息(或者该API的重要更新)请看: https://github.com/hasherezade/libpeconv/blob/master/libpeconv/include/peconv/pe_loader.h
正如我们所见, 我们想要导入的函数是独用的, 因此如果我们载入crackme的PE文件时没有加载导入表和重定位信息也不会造成什么危害(我们将在文章的下一部分看如何载入一个完整的PE文件). 我将使用到load_pe_module
函数
现在, 我们来导入函数, 首先我们来定义一个指针
计算在载入模块中该函数的绝对偏移
然后填充指针
现在我们就可以在我们的应用程序里该函数
但在那之前, 我们可以开始暴力穷举, 我们也同样也需要填充缓冲区指针.
以下链接是我准备的完整穷举程序: https://gist.github.com/hasherezade/44b440675ccc065f111dd6a90ed34399#file-brutforcer_1-cpp
并且结果表现良好. 我们得到的结果跟crackme需要的一样.
但目前为止, 我们找到的值也只是解答过程的一部分, 并不是我们需要找到的flag. 我们从先前静态分析时可以知道, 如果给出正确值, 那么代码块就能解密并执行. 如果我们能看到解密后代码块到底是怎样的, 那岂不是很酷?
而且这也非常容易实现. 我们的PE文件载入进了当前进程可读可写可执行内存中 - 因此我们可以轻易地将解密后的数据替换回加密块代码, 我们只需要一个简单的memcpy
就能完成这个工作
随后, libPeConv
可以帮助我们将PE文件转换回原始格式以便用IDA打开. 我们可以用libPeConv
的pe_virtual_to_raw
来完成.
并且以下是完整的解答: brutforcer_2.cpp
与初始的文件相比, 我们可以看到dump出来的可执行文件的缓冲区已经覆写过了.
所以我们在IDA里看下修改的可执行文件
搞定!在0x000F107C
处显示出我们的flag: et_tu_brute_force@flare-on.com
OK, 你可能会说, 这很简单呀, 导入的函数是独立的, 所以我们可以从原来文件中抽出来, 并不需要使用任何加载器. 但是如果函数调用了一些其他的模块内的其他函数或是导入函数呢? 我们之前的方法还能生效吗? 不止如此, 剔除掉重定位信息的PE文件又能行吗?
为了回答这些问题, 我准备了其他的测试用例. 与之前载一个函数相反, 我将会在穷举程序中载入并执行完整的crackme文件.
首先我们将会修改一些东西. 这次不使用load_pe_module
, 我使用load_pe_executable
来加载完整的可执行文件和依赖.
这个函数将自动地识别出这个PE文件没有重定位信息, 并且载入到初始模块基址. 注意, 分配的指定基址处的内存可能不总会生效, 因此有时需要运行多次使得程序正确地执行. 你也必须确定加载器的模块基址跟payload需要的模块基址不相冲突(如果加载器的基址是随机的话就很好).
一旦PE文件加载完毕, 我们就需要获取它的入口地址, 并且随后我们就可以像其他函数一样调用它:
但还要注意这与payload的具体实现细节有关, 一旦你转向执行入口点代码, 它可能在完成工作后直接退出而不会返回到你的代码中.
我打算修改穷举程序的代码, 使得在找到正确值之后crackme会继续运行. 以下是代码的完整版本: brutforcer_3.cpp
为了确保一切运行正常(尽管运行payload确实建立了socket并给出跟之前载入独立函数时相同的回应), 我写了一个简短的python脚本来交流和显示回应结果: test.py
现在, 你可以在YouTube观看整个过程的操作: https://www.youtube.com/watch?v=x3T3qFEDkF0
以上就是我今天所准备的内容, 我希望大家都能有所收获! 该库现在正处于快速开发阶段, 所以许多东西会进行重构并优化, 敬请期待.
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!