Inflated!!!
- C++异常化处理
- OLLVM-控制流平坦化
- Two Puzzles
Exception
一般碰到C++异常逆向,确定了异常分发、处理部分,直接把call throw改为jmp catch块,再F5即可
PS: 多个catch块根据rdx来当为异常处理数值决定哪个为对应的catch块
(关于以上,这篇讲的很详细)
然而,这题没这么简单,套了个ollvm!?基于异常处理的ollvm,无论从哪个角度都没法使用之前的老套路
耐心看完这两篇文章就会有所收获,对于此题的被异常处理搞乱掉的cfg就会有所理解
OLLVM
要是平常的ollvm都可以按照这篇来解决
其他的原理讲的非常好,问题是这题并不是那么简单,但为了去ollvm我们的思路也是一样的,所以要对ollvm的cfg熟悉,并懂得我们该如何恢复一个被ollvm混淆后的代码
现在就开始写我对这题的看法!
参考Write up:
https://github.com/Lnkvct/CTF-for-Fun/blob/main/Challenges/Inflated-ACTF2022/writeup.md
https://www.cnblogs.com/FW-ltlly/p/16472171.html
lchild师傅的Write up(pdf所以没法给链接)
0x00 日常查壳
(感觉好久没写wp了)
无壳64位
0x01 CFG
GETC
在讲这题ollvm与异常处理之前,有必要先搞懂我们到底是怎么输入的
一共有三处getc处理我们第一段输入的地方
1 | 407629 |
程序最先开始运行的是 407629,这里我们可以输入上下左右箭头与特定的数字
- 如果是数字,程序读取加密进行存放
- 如果是箭头,会继续进行处理
(同时我们的输入还会决定异常类型)
Official Write up: The value of the first field of the thrown
StdObfException
object comes from the second input passed to the construct ofStdObfException
.
(这图好大…有没有什么办法处理这些图片还有博客页面的方法)
那么异常处理先不深究,继续回来箭头如何处理这个问题
那么箭头其实为三字节码,上下左右箭头分别对应 ^[[A ^[[B ^[[C ^[[D
(此时开始动调,我第一次输入为上箭头,同时注意RAX)
那么在 407629 第一次处理箭头会读取为1B
随后到 40553A 读取为5B
最后到达 405676 可以发现我们的上箭头代码所对应的字符为A
以上就解释了第一段输入的处理,等到最后解密第一段输入就会用到此
OLLVM
引用这张图,想要去掉ollvm最基本的是要认识这几个块
先抛去原题,来认识一下这些名词
- 函数的开始地址为序言(Prologue)的地址
- 序言的后继为主分发器(Main dispatcher)
- 后继为主分发器的块为预处理器(Predispatcher)
- 后继为预处理器的块为真实块(Relevant blocks)
- 无后继的块为retn块
- 剩下的为无用块与子分发器(Sub dispatchers)
那参考文章,总结来说,利用angr符号执行去除控制流平坦化的步骤可以归结为三个步骤:
- 静态分析CFG得到序言/入口块(Prologue)、主分发器(Main dispatcher)、子分发器/无用块(Sub dispatchers)、真实块(Relevant blocks)、预分发器(Predispatcher)和返回块(Return)
- 利用符号执行恢复真实块的前后关系,重建控制流
- 根据第二步重建的控制流Patch程序,输出恢复后的可执行文件
简单来说就是获取所有的块,利用angr符号执行我们的真实块,查看真实块之间的流程,再抛去我们不要的块,patch程序,完成!
(那么具体的实现看文章)
然而这题…,根本不像啊!可以看出这题的CFG根本看不懂,不像单单ollvm混淆过的cfg那么漂亮
Exception
为了搞懂CFG为什么成这样了,得先了解下异常的原理,参考原文
对于最基本的thown catch不再赘述,这篇讲到很清楚
异常抛出后,发生了什么事情?
如果当前函数没有catch,就沿着函数的调用链继续往上抛,然后出现两种情况
在某个函数中找到相应的catch
没找到相应的catch,调用 std::terminate() (这个函数是把程序abort)
如果想找到了相应的catch,执行相应的操作
- 程序中catch的代码块有个专有名词:Landing pad
从抛异常到开始 -> 执行Landing pad代码 这整个过程叫作Stack unwind
Stack unwind
- 从抛异常函数开始,对调用链上的函数逐个往前查找Landing pad
- 如果没有找到Landing pad则把程序abort,如果找到则记下Landing pad的位置,再重新回到抛异常的函数那里开始,一帧一帧地清理调用链上各个函数内部的局部变量,直到 landing pad 所在的函数为止
1 | void func1() |
stack unwind的过程可以简单看成函数调用的逆过程,这个过程在实现上由一个专门的stack unwind库来实现
stack unwind库在intel平台上
属于Itanium ABI 接口中的一部分
与具体的语言无关,由系统实现
任何上层语言都可以通过这个接口的基础实现各自的异常处理
GCC就是通过这个接口实现C++的异常处理
Itanium C++ ABI
ltanium C++ ABI定义了一系列函数以及数据结构来建立整个异常处理的流程及框架,主要函数包括以下列:
1 | _Unwind_RaiseException, |
其中 _Unwind_RaiseException() 函数进行stack unwind,它在用户执行throw的时被调用
主要功能
从当前函数开始,对调用链上的每一个函数都调用一个叫做 personality routine 的函数(__gxx_personality_v0)
- personality routine 该函数由上层的语言定义及提供实现
_Unwind_RaiseException() 会在内部把函数栈调用现场重现,然后传给 personality routine,该函数主要做两件事情
- 检查当前函数是否有相对应的catch
- 清理调用栈上的局部变量
那么稍稍总结一下,就是当程序抛出异常就要进行 stack unwind 操作
而这个操作具体是 _Unwind_RaiseException() 中的 personality routine() 实现了检查catch和清理栈上的局部变量
C++ ABI
基于前面介绍的 ltanium ABI,编译器层面也定义了一系列 ABI 与之交互
当我们在代码中写下 throw xxx,编译器会分配一个数据结构 __cxa_exception 来表示该异常,该异常也有一个头部,定义如下
1 | struct __cxa_exception |
当用户 throw 一个异常时,编译器会帮我们调用相应的函数分配出如下的结构
其中 __cxa_exception 就是头部,exception_obj 则是 “throw xxx” 中的 xxx,这两部分在内存中是连续的。
异常对象由函数 __cxa_allocate_exception() 进行创建
最后由 __cxa_free_exception() 进行销毁
当我们在程序里执行了抛出异常的操作,编译器为我们做了如下的事情:
- 调用 __cxa_allocate_exception 函数,分配一个异常对象(__cxa_exception,数据结构如上)
- 调用 __cxa_throw 函数,这个函数会将异常对象做一些初始化
- __cxa_throw() 调用 Itanium ABI 里的 _Unwind_RaiseException() 从而开始 unwind
- _Unwind_RaiseException() 对调用链上的函数进行 unwind 时,调用 personality routine()
- 该异常如能被处理(有相应的 catch),则 personality routine 会依次对调用链上的函数进行清理
- _Unwind_RaiseException() 将控制权转到相应的catch代码
- unwind 完成,用户代码继续执行
总结太Bravo了!
再看异常处理
有了这些前置知识,再看题目中的异常,由前面描述可知实现 unwind stack 的具体过程是通过 __gxx_personality_v0(即personality routine)实现
这时候我们再去IDA里调整此函数
1 | _Unwind_Reason_Code __fastcall _gxx_personality_v0( |
光标在函数,按Y修改类型
scan_eh_tab
回忆__gxx_personality_v0函数功能
- 检查当前函数是否有相应的 catch 语句。
- 清理当前函数中的局部变量。
在personality routine()下的 scan_eh_tab() 该函数有我们最关心的两个值,同时也是魔改处
与源码对比:https://code.woboq.org/llvm/libcxxabi/src/cxa_personality.cpp.html#__cxxabiv1::scan_eh_tab
Shfit + F1 -> INS 导入结构体
1 | struct scan_results |
光标在scan_eh_tab函数上按Y修改
1 | void scan_eh_tab(scan_results *results, _Unwind_Action actions, bool native_exception, _Unwind_Exception *unwind_exception, _Unwind_Context *context) |
Landing pad
Landing pad(指向catch块的分发处,只单单拿到landing pad还不够,这时候还缺少一个对应异常类型ttypeIndex)
ttypeIndex
首先要求父类为StdObfException的异常
最后的ttypeIndex由 thrown_object_ptr(由我们的第一段输入所决定的thrown_object_ptr) 和 原始固定固定typeIndex 决定
Official Write up: And we have figured out that the
ttypeIndex
is determined by the first field of the thrownStdObfException
object and thelptinfo
passed to__cxa_throw
. The value of the first field of the thrownStdObfException
object comes from the second input passed to the construct ofStdObfException
.
那么这两个值到底具体指的是什么??
其实上面已经给出了答案,反复调试可知,可以发现我们的第一段输入设置了父类StdObfException
the first field of the thrown StdObfException
object 指的就是我们的输入
the lptinfo
passed to __cxa_throw
指的就是当 ___cxa_allocate_exception 创建的异常,也就是固定的
现在知道了魔改后的流程是从哪里来到哪里去,人工方式就是跳到landing pad再设置rdx为ttypeIndex就可以到达我们所对应的catch块
什么叫CFG!
那么现在知道了routine personality 中的 scan_eh_tab被修改了,而IDA平常能识别throw catch这些块的原因就是这些正常的源码
然而landingpad与ttypeIndex都被修改了,所以导致了IDA识别的CFG成了这个样子
我们根本没法用肉眼知道throw的块在哪,只有通过动调才能确定,然而这就导致了原先的deflat脚本都不不行了
原因主要为两点
- 无法确定throw后的块
- throw可能对着多个catch块,这时候就通过rdi(ttypeIndex)进行catch块分发(landingPad)
原因还有种种就不一一举例,就无法正常原先deflat所需要的CFG块
以下开始就是跟着官方脚本复现
我们再回忆一下正常的ollvm的执行流程
Prologue(入口块)-> Main dispatcher(主分发器)-> Sub dispathers(子分发器)-> Relevant blocks(真实块)-> Predispather(预分发器)-> Main dispatcher(主分发器)…
总结一下这道题的CFG
我们的下一个真实块取决于系统生产的lptinfo和我们的第一段输入所导致的StdObfException,在每个真实块的结束,我们不只是跳往与预分发器,而是调用 __cxa_throw 进行第二次调度,我们称二次调用为 second dispatch
所以我们的执行流就是
… -> main dispatcher -> sub dispatchers -> relevant block -> throw StdObfException exception -> Secondary dispatchers -> pre-dispatcher -> main dispatcher -> …
除此之外,程序还抛出了一些真正的异常,对于这些异常,第二次调用发生于Landing pad末尾
… -> main dispatcher -> sub dispatchers -> relevant block that throws real exceptions -> the according real LandingPad block -> throw StdObfException exception -> Secondary dispatchers -> pre-dispatcher -> main dispatcher -> …
0x02 Deflat Solution
去该平坦化控制流,有两个步骤:
- 找到所有的真实块
- 找到真实块之间的关系
Find all relevant blocks
我们可以从主分发器开始寻找,找到所有子分发器的后继者,这些后继者本身不是子分发器
官方WP中一眼丁真发现子分发器由该指令格式组成
1 | sub dispathers such as: |
于是由此区别出来
1 | isCmpRI = lambda instr: instr.mnemonic == "cmp" and\ |
首先判断是否为子分发器,然后排除法找到所有真实块
1 | class PatchHelper: |
Relevant blocks:
1 | *******************relevant blocks************************ |
Find the flow
官网WP指出抽象出来)留个坑,以后熟了试试
Official Write up: A good idea is to abstract the
throw StdObfException -> catch
process and do the one basic block symbolic execution (You can refer to Deobfuscation: recovering an OLLVM-protected program or 利用符号执行去除控制流平坦化 for more information).
于是官网WP又给了个更有趣的方法,GDB脚本!
为了找到真实块之间的流程,通过普通的执行然后打印真实块需要的信息!
但是我们不一样能得到所有的流程因为部分可能没执行到,但是我们依然可以利用提取出来的信息去恢复部分控制流,并弄清楚如何输入可以恢复更多流程。(怎么好像梦到过我在这写wp…)
生成GDB的脚本如下
- 40A3D4为我们catch块地址
- _ZN18StdSubObfExceptionC2Ec为了打印异常类型
1 | cmds = """\ |
1 | cat teatin |
于是可以获取真实块接下来的landing pad与异常类型
1 | Breakpoint 1 at 0x40a3d4 |
然后就写个PARSER分析
1 | def parse_logs(logfn, prologue, patch_helper): |
Flows:
1 | ************************flow****************************** |
Patch
修复程序环节!
当我们已经确定了执行流程,像抛异常 子分发器什么都是多余的了,统统patch掉
对于后继块只有一个的真实块,只需要jmp过去
对于有多个后继块的,需要通过esi(也就是异常类型)来改成cmp esi, … jz即可
1 | def patch_branches(self, bb, va_targets): |
官方完整脚本
1 | ## filename: deflat.py |
Work flow:
1 | $ python deflat.py inflated 0x404820 |
(按照以上流程,test.gdb可能会报个错,程序把本身有个\n是脚本中需要打印的,但直接转义成真换行了需要手动恢复)
观看修复后的流程
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
0x03 Solve the Puzzles
PART ONE
之前也提到过,由于我们的输入部分流可能执行不到,很明显我们刚刚根本没有输入上下左右箭头啥的
所以关于处理上下左右箭头的代码无了
1 | do |
这个时候就可以更改我们的输入(指的是输入箭头再输入字符)再来一遍
成功解析出我们的第一段输入
由于两个文件分析过程不贴了,可以直接看官方WP给出的源码
1 | int part1_size = 12; |
PART TWO
这个部分完全跟着lchild的分析来了
接着就是第二段输入
首先是经过一段Base64解码操作,再经过取模除十操作得到一个数组
1 | if ( (_DWORD)v50 == 4 ) |
之后计算了九个数值,和一堆pang臭的代码,不过干的事情不是很复杂
第一个循环是复制,后两个循环判断行列,不难发现这是个数独,拿网站一把梭了
具体参考lchild师傅的Write up
0x04 GetFlag!!
第一个解密就直接移回去即可
第二个解密出数独的值,列移动,取出值恢复原权位值,最后Base64即可!
1 | s = [] |
最后输入上上下下右左右左3417再二段
GetFlag!!
总结
对于我这种0 throw catch小白0 ollvm小白,过程中是补了不少文章…学识尚浅如果有错误的地方欢迎指出,一定会严加学习修改!
最后拿到flag,也是对这题的一个happy ending,真是REVERSE生涯中目前做的最难的一题了
回忆起最开始wjh师傅教我的unicorn再到Helen师傅出的login,这次又上了台阶!逆向真是越来越有趣了。–7.21 00:55
About this Post
This post is written by P.Z, licensed under CC BY-NC 4.0.