首页
社区
课程
招聘
【ACTF2022】Inflated(C++异常处理控制流下的OLLVM混淆)
发表于: 2022-7-21 18:03 17950

【ACTF2022】Inflated(C++异常处理控制流下的OLLVM混淆)

P.Z 活跃值
2
2022-7-21 18:03
17950

一般碰到C++异常逆向,确定了异常分发、处理部分,直接把call throw改为jmp catch块,再F5即可

PS: 多个catch块根据rdx来当为异常处理数值决定哪个为对应的catch块

(关于以上,这篇讲的很详细)

https://4nsw3r.top/2022/02/03/SCTF-REVERSE-CplusExceptionEncrypt-%E8%B5%9B%E5%90%8E%E5%A4%8D%E7%8E%B0/#Clang-x64

然而,这题没这么简单,套了个ollvm!?基于异常处理的ollvm,无论从哪个角度都没法使用之前的老套路

耐心看完这两篇文章就会有所收获,对于此题的被异常处理搞乱掉的cfg就会有所理解

https://www.cnblogs.com/catch/p/3604516.html

https://www.cnblogs.com/catch/p/3619379.html

要是平常的ollvm都可以按照这篇来解决

https://bluesadi.github.io/0x401RevTrain-Tools/angr/10_%E5%88%A9%E7%94%A8angr%E7%AC%A6%E5%8F%B7%E6%89%A7%E8%A1%8C%E5%8E%BB%E9%99%A4%E6%8E%A7%E5%88%B6%E6%B5%81%E5%B9%B3%E5%9D%A6%E5%8C%96/

其他的原理讲的非常好,问题是这题并不是那么简单,但为了去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所以没法给链接)

(感觉好久没写wp了)

无壳64位

图片描述

在讲这题ollvm与异常处理之前,有必要先搞懂我们到底是怎么输入的

一共有三处getc处理我们第一段输入的地方

程序最先开始运行的是 407629,这里我们可以输入上下左右箭头与特定的数字

(同时我们的输入还会决定异常类型)

Official Write up: The value of the first field of the thrown StdObfException object comes from the second input passed to the construct of StdObfException.

图片描述

(这图好大...有没有什么办法处理这些图片还有博客页面的方法)

那么异常处理先不深究,继续回来箭头如何处理这个问题

那么箭头其实为三字节码,上下左右箭头分别对应 ^[[A ^[[B ^[[C ^[[D

(此时开始动调,我第一次输入为上箭头,同时注意RAX)

那么在 407629 第一次处理箭头会读取为1B

图片描述

随后到 40553A 读取为5B

图片描述

最后到达 405676 可以发现我们的上箭头代码所对应的字符为A

图片描述

以上就解释了第一段输入的处理,等到最后解密第一段输入就会用到此

引用这张图,想要去掉ollvm最基本的是要认识这几个块

https://security.tencent.com/index.php/blog/msg/112

图片描述

先抛去原题,来认识一下这些名词

那参考文章,总结来说,利用angr符号执行去除控制流平坦化的步骤可以归结为三个步骤:

简单来说就是获取所有的块,利用angr符号执行我们的真实块,查看真实块之间的流程,再抛去我们不要的块,patch程序,完成!

(那么具体的实现看文章)

https://bluesadi.github.io/0x401RevTrain-Tools/angr/10_%E5%88%A9%E7%94%A8angr%E7%AC%A6%E5%8F%B7%E6%89%A7%E8%A1%8C%E5%8E%BB%E9%99%A4%E6%8E%A7%E5%88%B6%E6%B5%81%E5%B9%B3%E5%9D%A6%E5%8C%96/

然而这题...,根本不像啊!可以看出这题的CFG根本看不懂,不像单单ollvm混淆过的cfg那么漂亮

图片描述

为了搞懂CFG为什么成这样了,得先了解下异常的原理,参考原文

https://www.cnblogs.com/catch/p/3604516.html

对于最基本的thown catch不再赘述,这篇讲到很清楚

https://4nsw3r.top/2022/02/03/SCTF-REVERSE-CplusExceptionEncrypt-%E8%B5%9B%E5%90%8E%E5%A4%8D%E7%8E%B0/#Clang-x64

如果当前函数没有catch,就沿着函数的调用链继续往上抛,然后出现两种情况

在某个函数中找到相应的catch

没找到相应的catch,调用 std::terminate() (这个函数是把程序abort)

如果想找到了相应的catch,执行相应的操作

从抛异常到开始 -> 执行Landing pad代码 这整个过程叫作Stack unwind

Stack unwind

stack unwind的过程可以简单看成函数调用的逆过程,这个过程在实现上由一个专门的stack unwind库来实现

stack unwind库在intel平台上

属于Itanium ABI 接口中的一部分

ltanium C++ ABI定义了一系列函数以及数据结构来建立整个异常处理的流程及框架,主要函数包括以下列:

其中 _Unwind_RaiseException() 函数进行stack unwind,它在用户执行throw的时被调用

主要功能

从当前函数开始,对调用链上的每一个函数都调用一个叫做 personality routine 的函数(__gxx_personality_v0)

_Unwind_RaiseException() 会在内部把函数栈调用现场重现,然后传给 personality routine,该函数主要做两件事情

那么稍稍总结一下,就是当程序抛出异常就要进行 stack unwind 操作

而这个操作具体是 _Unwind_RaiseException() 中的 personality routine() 实现了检查catch和清理栈上的局部变量

基于前面介绍的 ltanium ABI,编译器层面也定义了一系列 ABI 与之交互

当我们在代码中写下 throw xxx,编译器会分配一个数据结构 __cxa_exception 来表示该异常,该异常也有一个头部,定义如下

当用户 throw 一个异常时,编译器会帮我们调用相应的函数分配出如下的结构

图片描述

其中 __cxa_exception 就是头部,exception_obj 则是 "throw xxx" 中的 xxx,这两部分在内存中是连续的。

异常对象由函数 __cxa_allocate_exception() 进行创建

最后由 __cxa_free_exception() 进行销毁

当我们在程序里执行了抛出异常的操作,编译器为我们做了如下的事情:

总结太Bravo了!

有了这些前置知识,再看题目中的异常,由前面描述可知实现 unwind stack 的具体过程是通过 __gxx_personality_v0(即personality routine)实现

这时候我们再去IDA里调整此函数

光标在函数,按Y修改类型

图片描述

scan_eh_tab

回忆__gxx_personality_v0函数功能

在personality routine()下的 scan_eh_tab() 该函数有我们最关心的两个值,同时也是魔改处

与源码对比:https://code.woboq.org/llvm/libcxxabi/src/cxa_personality.cpp.html#__cxxabiv1::scan_eh_tab

Shfit + F1 -> INS 导入结构体

光标在scan_eh_tab函数上按Y修改

Landing pad(指向catch块的分发处,只单单拿到landing pad还不够,这时候还缺少一个对应异常类型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 thrown StdObfException object and the lptinfo passed to __cxa_throw. The value of the first field of the thrown StdObfException object comes from the second input passed to the construct of StdObfException.

那么这两个值到底具体指的是什么??

其实上面已经给出了答案,反复调试可知,可以发现我们的第一段输入设置了父类StdObfException

the first field of the thrown StdObfException object 指的就是我们的输入

the lptinfo passed to __cxa_throw 指的就是当 ___cxa_allocate_exception 创建的异常,也就是固定的

图片描述

现在知道了魔改后的流程是从哪里来到哪里去,人工方式就是跳到landing pad再设置rdx为ttypeIndex就可以到达我们所对应的catch块

那么现在知道了routine personality 中的 scan_eh_tab被修改了,而IDA平常能识别throw catch这些块的原因就是这些正常的源码

然而landingpad与ttypeIndex都被修改了,所以导致了IDA识别的CFG成了这个样子

我们根本没法用肉眼知道throw的块在哪,只有通过动调才能确定,然而这就导致了原先的deflat脚本都不不行了

原因主要为两点

原因还有种种就不一一举例,就无法正常原先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 -> ...

去该平坦化控制流,有两个步骤:

我们可以从主分发器开始寻找,找到所有子分发器的后继者,这些后继者本身不是子分发器

官方WP中一眼丁真发现子分发器由该指令格式组成

于是由此区别出来

首先判断是否为子分发器,然后排除法找到所有真实块

Relevant blocks:

官网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的脚本如下

于是可以获取真实块接下来的landing pad与异常类型

然后就写个PARSER分析

Flows:

修复程序环节!

当我们已经确定了执行流程,像抛异常 子分发器什么都是多余的了,统统patch掉

对于后继块只有一个的真实块,只需要jmp过去

官方完整脚本

Work flow:

(按照以上流程,test.gdb可能会报个错,程序把本身有个\n是脚本中需要打印的,但直接转义成真换行了需要手动恢复)

观看修复后的流程

之前也提到过,由于我们的输入部分流可能执行不到,很明显我们刚刚根本没有输入上下左右箭头啥的

所以关于处理上下左右箭头的代码无了

这个时候就可以更改我们的输入(指的是输入箭头再输入字符)再来一遍

成功解析出我们的第一段输入

图片描述

由于两个文件分析过程不贴了,可以直接看官方WP给出的源码

这个部分完全跟着lchild的分析来了

接着就是第二段输入

首先是经过一段Base64解码操作,再经过取模除十操作得到一个数组

之后计算了九个数值,和一堆pang臭的代码,不过干的事情不是很复杂

图片描述

第一个循环是复制,后两个循环判断行列,不难发现这是个数独,拿网站一把梭了

具体参考lchild师傅的Write up

# https://sudoku.vip/sudoku-x-solver/

第一个解密就直接移回去即可

第二个解密出数独的值,列移动,取出值恢复原权位值,最后Base64即可!

最后输入上上下下右左右左3417再二段

GetFlag!!

图片描述

对于我这种0 throw catch小白0 ollvm小白,过程中是补了不少文章...学识尚浅如果有错误的地方欢迎指出,一定会严加学习修改!

最后拿到flag,也是对这题的一个happy ending,真是REVERSE生涯中目前做的最难的一题了

回忆起最开始wjh师傅教我的unicorn再到Helen师傅出的login,这次又上了台阶!逆向真是越来越有趣了。--7.21 00:55

 
 
 
 
 
 
 
 
 
 
407629
40553A(专门用来处理箭头)
405676(专门用来处理箭头)
407629
40553A(专门用来处理箭头)
405676(专门用来处理箭头)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
void func1()
{
  cs a; // stack unwind时被析构。
  throw 3;
}
 
void func2()
{
  cs b;
  func1();
}
 
void func3()
{
  cs c;
  try
  {
    func2();
  }
  catch (int)
  {
    //进入这里之前, func1, func2已经被unwind.
  }
}
void func1()
{
  cs a; // stack unwind时被析构。
  throw 3;
}
 
void func2()
{
  cs b;
  func1();
}
 
void func3()
{
  cs c;
  try
  {
    func2();
  }
  catch (int)
  {
    //进入这里之前, func1, func2已经被unwind.
  }
}
_Unwind_RaiseException,
_Unwind_Resume,
_Unwind_DeleteException,
_Unwind_GetGR,
_Unwind_SetGR,
_Unwind_GetIP,
_Unwind_SetIP,
_Unwind_GetRegionStart,
_Unwind_GetLanguageSpecificData,
_Unwind_ForcedUnwind
_Unwind_RaiseException,
_Unwind_Resume,
_Unwind_DeleteException,
_Unwind_GetGR,
_Unwind_SetGR,
_Unwind_GetIP,
_Unwind_SetIP,
_Unwind_GetRegionStart,
_Unwind_GetLanguageSpecificData,
_Unwind_ForcedUnwind
 
 
 
struct __cxa_exception
{
  std::type_info *    exceptionType;
  void (*exceptionDestructor) (void *);
  unexpected_handler    unexpectedHandler;
  terminate_handler    terminateHandler;
  __cxa_exception *    nextException;
 
  int     handlerCount;
  int     handlerSwitchValue;
  const char *     actionRecord;
  const char *     languageSpecificData;
  void *     catchTemp;
  void *     adjustedPtr;
 
  _Unwind_Exception    unwindHeader;
};
struct __cxa_exception
{
  std::type_info *    exceptionType;
  void (*exceptionDestructor) (void *);
  unexpected_handler    unexpectedHandler;
  terminate_handler    terminateHandler;
  __cxa_exception *    nextException;
 
  int     handlerCount;
  int     handlerSwitchValue;
  const char *     actionRecord;
  const char *     languageSpecificData;
  void *     catchTemp;
  void *     adjustedPtr;
 
  _Unwind_Exception    unwindHeader;
};
 
 
 
_Unwind_Reason_Code __fastcall _gxx_personality_v0(
        int Version,
        _Unwind_Action actions,
        __int64 exceptionClass,
        _Unwind_Exception *exceptionObject,
        _Unwind_Context *context)
_Unwind_Reason_Code __fastcall _gxx_personality_v0(
        int Version,
        _Unwind_Action actions,
        __int64 exceptionClass,
        _Unwind_Exception *exceptionObject,
        _Unwind_Context *context)
 
 
 
 
struct scan_results
{
int64_t ttypeIndex;
const uint8_t* actionRecord;
const uint8_t* languageSpecificData;
uintptr_t landingPad;
void* adjustedPtr;
_Unwind_Reason_Code reason;
};
struct scan_results
{
int64_t ttypeIndex;
const uint8_t* actionRecord;
const uint8_t* languageSpecificData;
uintptr_t landingPad;
void* adjustedPtr;
_Unwind_Reason_Code reason;
};
void scan_eh_tab(scan_results *results, _Unwind_Action actions, bool native_exception, _Unwind_Exception *unwind_exception, _Unwind_Context *context)
void scan_eh_tab(scan_results *results, _Unwind_Action actions, bool native_exception, _Unwind_Exception *unwind_exception, _Unwind_Context *context)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
sub dispathers such as:
cmp
jx
sub dispathers such as:
cmp
jx
isCmpRI = lambda instr: instr.mnemonic == "cmp" and\
  hasattr(instr.operands[0], "_X86RegisterOperand__key") and\
  hasattr(instr.operands[1], "_X86ImmediateOperand__key")
isCJmp = lambda instr: instr.mnemonic.startswith("j") and \
  instr.mnemonic != "jmp"
isSubDispatcher = lambda bb: (len(bb.instrs) == 2) and\
   isCmpRI(bb.instrs[0]) and isCJmp(bb.instrs[1])
isCmpRI = lambda instr: instr.mnemonic == "cmp" and\
  hasattr(instr.operands[0], "_X86RegisterOperand__key") and\
  hasattr(instr.operands[1], "_X86ImmediateOperand__key")
isCJmp = lambda instr: instr.mnemonic.startswith("j") and \
  instr.mnemonic != "jmp"
isSubDispatcher = lambda bb: (len(bb.instrs) == 2) and\
   isCmpRI(bb.instrs[0]) and isCJmp(bb.instrs[1])
class PatchHelper:
  ## ......
  # To get all cfgs
  def block(self, addr):
    bb = self.cfg.find_basic_block(addr)
    if bb is None:
      bb = barf.bb_builder.strategy._disassemble_bb(addr, barf.binary.ea_end, {})
    return bb
 
def get_relevant_blocks(cfg, patch_helper, main_dispatcher):
  isCmpRI = lambda instr: instr.mnemonic == "cmp" and\
    hasattr(instr.operands[0], "_X86RegisterOperand__key") and\
    hasattr(instr.operands[1], "_X86ImmediateOperand__key")
  isCJmp = lambda instr: instr.mnemonic.startswith("j") and \
    instr.mnemonic != "jmp"
  isSubDispatcher = lambda bb: (len(bb.instrs) == 2) and\
     isCmpRI(bb.instrs[0]) and isCJmp(bb.instrs[1])
  relevant_blocks = []
  visited = set()
  q = SimpleQueue()
  q.put(patch_helper.block(main_dispatcher))
  while not q.empty():
    bb = q.get()
    # Either Sub Patchers or Relevant blocks?
    if isSubDispatcher(bb):
      for succ, cond in bb.branches:
        if succ in visited:
          continue
        q.put(patch_helper.block(succ))
        visited.add(succ)
    else:
      relevant_blocks.append(bb)
  return relevant_blocks
class PatchHelper:
  ## ......
  # To get all cfgs
  def block(self, addr):
    bb = self.cfg.find_basic_block(addr)
    if bb is None:
      bb = barf.bb_builder.strategy._disassemble_bb(addr, barf.binary.ea_end, {})
    return bb
 
def get_relevant_blocks(cfg, patch_helper, main_dispatcher):
  isCmpRI = lambda instr: instr.mnemonic == "cmp" and\
    hasattr(instr.operands[0], "_X86RegisterOperand__key") and\
    hasattr(instr.operands[1], "_X86ImmediateOperand__key")
  isCJmp = lambda instr: instr.mnemonic.startswith("j") and \
    instr.mnemonic != "jmp"
  isSubDispatcher = lambda bb: (len(bb.instrs) == 2) and\
     isCmpRI(bb.instrs[0]) and isCJmp(bb.instrs[1])
  relevant_blocks = []
  visited = set()
  q = SimpleQueue()
  q.put(patch_helper.block(main_dispatcher))
  while not q.empty():
    bb = q.get()
    # Either Sub Patchers or Relevant blocks?
    if isSubDispatcher(bb):
      for succ, cond in bb.branches:
        if succ in visited:
          continue
        q.put(patch_helper.block(succ))
        visited.add(succ)
    else:
      relevant_blocks.append(bb)
  return relevant_blocks
*******************relevant blocks************************
main_dispatcher:0x404a80
relevant_blocks: ['0x409437', '0x406443', '0x404ab8', '0x408031', '0x407842', '0x407d31', '0x407437', '0x407f4f', '0x4076bd', '0x407a6b', '0x40723e', '0x407fc4', '0x409458', '0x407bc7', '0x40732f', '0x407ebc', '0x407566', '0x407960', '0x4070fa', '0x405e7a', '0x4078e3', '0x407e5a', '0x4074ca', '0x405c87', '0x407741', '0x407af5', '0x4072b4', '0x405ded', '0x4077b6', '0x407c6b', '0x4073a4', '0x405b29', '0x4075f9', '0x407a06', '0x4071aa', '0x406cfe', '0x406c94', '0x406ef0', '0x406859', '0x40707d', '0x406b62', '0x406f5f', '0x4065c9', '0x406e5d', '0x406a72', '0x406d7b', '0x406704', '0x406def', '0x406964', '0x40944b', '0x4064a5', '0x405469', '0x405a5f', '0x404fae', '0x40532c', '0x40589c', '0x404d58', '0x4053d3', '0x405923', '0x404ec5', '0x40529a', '0x4057b8', '0x404bc4', '0x405f2a', '0x4056f0', '0x406299', '0x4068f0', '0x4063b0', '0x406bf9', '0x406323', '0x406646', '0x40620f', '0x406b00', '0x4060e7', '0x4067bb', '0x40617c', '0x4069e3', '0x40606d', '0x406521', '0x4051fe', '0x405647', '0x404e14', '0x4055b5', '0x4050cc', '0x40550b', '0x404ca4']
*******************relevant blocks************************
main_dispatcher:0x404a80
relevant_blocks: ['0x409437', '0x406443', '0x404ab8', '0x408031', '0x407842', '0x407d31', '0x407437', '0x407f4f', '0x4076bd', '0x407a6b', '0x40723e', '0x407fc4', '0x409458', '0x407bc7', '0x40732f', '0x407ebc', '0x407566', '0x407960', '0x4070fa', '0x405e7a', '0x4078e3', '0x407e5a', '0x4074ca', '0x405c87', '0x407741', '0x407af5', '0x4072b4', '0x405ded', '0x4077b6', '0x407c6b', '0x4073a4', '0x405b29', '0x4075f9', '0x407a06', '0x4071aa', '0x406cfe', '0x406c94', '0x406ef0', '0x406859', '0x40707d', '0x406b62', '0x406f5f', '0x4065c9', '0x406e5d', '0x406a72', '0x406d7b', '0x406704', '0x406def', '0x406964', '0x40944b', '0x4064a5', '0x405469', '0x405a5f', '0x404fae', '0x40532c', '0x40589c', '0x404d58', '0x4053d3', '0x405923', '0x404ec5', '0x40529a', '0x4057b8', '0x404bc4', '0x405f2a', '0x4056f0', '0x406299', '0x4068f0', '0x4063b0', '0x406bf9', '0x406323', '0x406646', '0x40620f', '0x406b00', '0x4060e7', '0x4067bb', '0x40617c', '0x4069e3', '0x40606d', '0x406521', '0x4051fe', '0x405647', '0x404e14', '0x4055b5', '0x4050cc', '0x40550b', '0x404ca4']
 
 
 
 
cmds = """\
set pagination off
 
b *0x40A3D4
commands
  silent
  printf "landingPad: %x\\n", $rdx
  continue
end
 
b _ZN18StdSubObfExceptionC2Ec
commands
  silent
  printf "selector: %x\\n", $rsi
  continue
end
 
define mytrace
  break $arg0
  commands
    silent
    printf "%x\\n", $pc
    python gdb.execute('continue')
  end
end
"""
for bb in relevant_blocks:
    cmds += (f"mytrace *{hex(bb.address)} \n")
cmds += "run\n"
with open("test.gdb", "w") as f:
    f.write(cmds)
cmds = """\
set pagination off
 
b *0x40A3D4
commands
  silent
  printf "landingPad: %x\\n", $rdx
  continue
end
 
b _ZN18StdSubObfExceptionC2Ec
commands
  silent
  printf "selector: %x\\n", $rsi
  continue
end
 
define mytrace
  break $arg0
  commands
    silent
    printf "%x\\n", $pc
    python gdb.execute('continue')
  end
end
"""
for bb in relevant_blocks:
    cmds += (f"mytrace *{hex(bb.address)} \n")
cmds += "run\n"
with open("test.gdb", "w") as f:
    f.write(cmds)
cat teatin
0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
 
gdb inflated -x test.gdb --batch < testin > testout
cat teatin
0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
 
gdb inflated -x test.gdb --batch < testin > testout
Breakpoint 1 at 0x40a3d4
......
Breakpoint 88 at 0x404ca4
4075f9
selector: 0
landingPad: 4089bf
4072b4
selector: 0
landingPad: 408503
4075f9
selector: 2
landingPad: 4089bf
4060e7
selector: 0
......
40617c
selector: 0
landingPad: 409100
409437
[Inferior 1 (process 13732) exited normally]
Breakpoint 1 at 0x40a3d4
......
Breakpoint 88 at 0x404ca4
4075f9
selector: 0
landingPad: 4089bf
4072b4
selector: 0
landingPad: 408503
4075f9
selector: 2
landingPad: 4089bf
4060e7
selector: 0
......
40617c
selector: 0
landingPad: 409100
409437
[Inferior 1 (process 13732) exited normally]
def parse_logs(logfn, prologue, patch_helper):
  with open(logfn, "r") as f:
    t = f.readlines()
  i = 0
  selector_s = "selector: "
  landingpad_s = "landingPad: "
  relations = set()
  laddr = prologue
  lselector = 0
  landingpad = 0
  while i < len(t):
    try:
      addr = int(t[i], 16)
    except:
      i += 1
      continue
    if not laddr is None:
      relations.add((laddr, lselector, addr))
    if t[i+1].startswith(selector_s):
      selector = int(t[i+1][len(selector_s):], 16)
      i += 2
    elif t[i+1].startswith(landingpad_s):
      landingpad = int(t[i+1][len(landingpad_s):], 16)
      relations.add((addr, -1, landingpad))
      addr = landingpad
      while not patch_helper.is_unreachable(patch_helper.block(addr).direct_branch):
        addr = patch_helper.block(addr).direct_branch
      if t[i+2].startswith(selector_s):
        selector = int(t[i+2][len(selector_s):], 16)
      i += 3
    elif t[i+1].startswith("[Inferior "):
      i += 1
    else:
      print("Warning: %x doesn't have selector. "%addr)
      exit(0)
    laddr = addr
    lselector = selector
  return list(relations)
 
print('************************flow******************************')
relations = parse_logs(sys.argv[3], prologue, patch_helper)
relations.sort(key = lambda x:x)
flow = {}
for bb, selector, child in relations:
  if bb in flow:
    while len(flow[bb]) < selector:
      flow[bb].append(-1)
    flow[bb].append(child)
    assert(len(flow[bb]) == selector+1)
  else:
    flow[bb] = [child]
for (k, v) in list(flow.items()):
    print('%#x:' % k, [hex(child) for child in v])
def parse_logs(logfn, prologue, patch_helper):
  with open(logfn, "r") as f:
    t = f.readlines()
  i = 0
  selector_s = "selector: "
  landingpad_s = "landingPad: "
  relations = set()
  laddr = prologue
  lselector = 0
  landingpad = 0
  while i < len(t):
    try:
      addr = int(t[i], 16)
    except:
      i += 1
      continue
    if not laddr is None:
      relations.add((laddr, lselector, addr))
    if t[i+1].startswith(selector_s):
      selector = int(t[i+1][len(selector_s):], 16)
      i += 2
    elif t[i+1].startswith(landingpad_s):
      landingpad = int(t[i+1][len(landingpad_s):], 16)
      relations.add((addr, -1, landingpad))
      addr = landingpad
      while not patch_helper.is_unreachable(patch_helper.block(addr).direct_branch):
        addr = patch_helper.block(addr).direct_branch
      if t[i+2].startswith(selector_s):
        selector = int(t[i+2][len(selector_s):], 16)
      i += 3
    elif t[i+1].startswith("[Inferior "):
      i += 1
    else:
      print("Warning: %x doesn't have selector. "%addr)
      exit(0)
    laddr = addr
    lselector = selector
  return list(relations)
 
print('************************flow******************************')
relations = parse_logs(sys.argv[3], prologue, patch_helper)
relations.sort(key = lambda x:x)
flow = {}
for bb, selector, child in relations:
  if bb in flow:
    while len(flow[bb]) < selector:
      flow[bb].append(-1)
    flow[bb].append(child)
    assert(len(flow[bb]) == selector+1)
  else:
    flow[bb] = [child]
for (k, v) in list(flow.items()):
    print('%#x:' % k, [hex(child) for child in v])
************************flow******************************
0x404820: ['0x4075f9']
0x404ab8: ['0x404ab8', '0x406c94']
0x404bc4: ['0x407bc7']
0x404ca4: ['0x406bf9']
0x404ec5: ['0x4053d3']
0x404fae: ['0x406b00']
0x4051fe: ['0x40707d']
0x4053d3: ['0x406521']
0x405469: ['0x407d31']
0x4056f0: ['0x405a5f', '0x4056f0']
0x4057b8: ['0x404ab8']
0x405923: ['0x405923', '0x406e5d']
0x405a5f: ['0x4067bb']
0x405b29: ['0x406964', '0x406646']
0x405c87: ['0x405c87', '0x407437']
0x405f2a: ['0x405f2a', '0x4063b0']
0x4060e7: ['0x40723e']
0x40617c: ['0x409437']
0x40620f: ['0x405f2a']
0x406299: ['0x404bc4', '0x4057b8']
0x4063b0: ['0x4063b0', '0x405469']
0x4064a5: ['0x406704', '0x40620f']
0x406521: ['0x4074ca', '0x404bc4']
0x4065c9: ['0x40723e']
0x406646: ['0x406964']
0x406704: ['0x405c87']
0x4067bb: ['0x4082b6']
0x406964: ['0x405b29', '0x404ca4']
0x4069e3: ['0x408281']
0x406a72: ['0x404fae']
0x406b00: ['0x406299']
0x406bf9: ['0x405923']
0x406c94: ['0x4074ca']
0x406cfe: ['0x40723e']
0x406e5d: ['0x406e5d', '0x4077b6']
0x406f5f: ['0x406f5f', '0x407566']
0x40707d: ['0x40707d', '0x407960']
0x4070fa: ['0x406f5f']
0x4071aa: ['0x4056f0']
0x40723e: ['0x4072b4']
0x4072b4: ['0x4075f9', '0x4071aa']
0x407437: ['0x407437', '0x4064a5']
0x4074ca: ['0x404ec5', '0x407c6b']
0x407566: ['0x407566', '0x407a6b']
0x4075f9: ['0x4072b4', '-0x1', '0x4060e7', '0x406cfe', '0x4078e3', '0x4065c9']
0x4076bd: ['0x404ec5']
0x4077b6: ['0x406bf9', '0x4070fa']
0x4078e3: ['0x40723e']
0x407960: ['0x4081f5']
0x407a6b: ['0x4070fa', '0x406704']
0x407bc7: ['0x406a72', '0x407bc7']
0x407c6b: ['0x4069e3']
0x407d31: ['0x407d31', '0x407ebc']
0x407ebc: ['0x407ebc', '0x40617c']
0x4081f5: ['0x405b29']
0x408281: ['0x4051fe']
0x4082b6: ['0x4076bd']
************************flow******************************
0x404820: ['0x4075f9']
0x404ab8: ['0x404ab8', '0x406c94']
0x404bc4: ['0x407bc7']
0x404ca4: ['0x406bf9']
0x404ec5: ['0x4053d3']
0x404fae: ['0x406b00']
0x4051fe: ['0x40707d']
0x4053d3: ['0x406521']
0x405469: ['0x407d31']
0x4056f0: ['0x405a5f', '0x4056f0']
0x4057b8: ['0x404ab8']
0x405923: ['0x405923', '0x406e5d']
0x405a5f: ['0x4067bb']
0x405b29: ['0x406964', '0x406646']
0x405c87: ['0x405c87', '0x407437']
0x405f2a: ['0x405f2a', '0x4063b0']
0x4060e7: ['0x40723e']
0x40617c: ['0x409437']
0x40620f: ['0x405f2a']
0x406299: ['0x404bc4', '0x4057b8']
0x4063b0: ['0x4063b0', '0x405469']
0x4064a5: ['0x406704', '0x40620f']
0x406521: ['0x4074ca', '0x404bc4']
0x4065c9: ['0x40723e']
0x406646: ['0x406964']
0x406704: ['0x405c87']
0x4067bb: ['0x4082b6']
0x406964: ['0x405b29', '0x404ca4']
0x4069e3: ['0x408281']
0x406a72: ['0x404fae']
0x406b00: ['0x406299']
0x406bf9: ['0x405923']
0x406c94: ['0x4074ca']
0x406cfe: ['0x40723e']
0x406e5d: ['0x406e5d', '0x4077b6']
0x406f5f: ['0x406f5f', '0x407566']
0x40707d: ['0x40707d', '0x407960']
0x4070fa: ['0x406f5f']
0x4071aa: ['0x4056f0']
0x40723e: ['0x4072b4']
0x4072b4: ['0x4075f9', '0x4071aa']
0x407437: ['0x407437', '0x4064a5']
0x4074ca: ['0x404ec5', '0x407c6b']
0x407566: ['0x407566', '0x407a6b']
0x4075f9: ['0x4072b4', '-0x1', '0x4060e7', '0x406cfe', '0x4078e3', '0x4065c9']
0x4076bd: ['0x404ec5']
0x4077b6: ['0x406bf9', '0x4070fa']
0x4078e3: ['0x40723e']
0x407960: ['0x4081f5']
0x407a6b: ['0x4070fa', '0x406704']
0x407bc7: ['0x406a72', '0x407bc7']
0x407c6b: ['0x4069e3']
0x407d31: ['0x407d31', '0x407ebc']
0x407ebc: ['0x407ebc', '0x40617c']
0x4081f5: ['0x405b29']
0x408281: ['0x4051fe']
0x4082b6: ['0x4076bd']
def patch_branches(self, bb, va_targets):
  va_start, size = self.get_patchable_from_relblk(bb)
  if size < PatchHelper.JMP_SIZE:
    print("[Warning] patch_jmp at block %x may fail. size: %d."%(bb.address, size))
  org_start = va_start
  print(f"va_start: {hex(va_start)}, bb addr: {hex(bb.address)}, size: {size}")
  ## `cmp esi, v` instr takes 3 bytes while `je xxx` takes 6 bytes
  ## And the last jmp instr takes 5 bytes.
  total_size = 9 * len(va_targets) - 4
  if size < total_size:
    ## If the nop block at the end of current block is not large enough,
       ## try to find another nop block and then jump to it.
    nx_va_start, nx_size = self.get_nop_by_size(total_size)
    if nx_size == 0:
      print("[Error] `patch_branches` needs a nop block with size larger than %d."%(total_size))
    self.patch_jmp(va_start, nx_va_start)
    va_start, size = nx_va_start, nx_size
 
  for i, t in enumerate(va_targets[:-1]):
    cmp_instr = bytes([0x83,0xfe,i])
    self.do_patch(va_start, cmp_instr)
    va_start += len(cmp_instr)
    cj_instr = bytes([PatchHelper.opcode['j'],PatchHelper.opcode['e']])
    if t == -1:
      ## -1 represent that we do not know the flow for this selector value for now.
      cj_instr += struct.pack('<i', self.func_terminate-va_start-6)
      # cj_instr = asm(f"je {hex(self.func_terminate)}", vma=va_start)
    else:
      cj_instr += struct.pack('<i', t-va_start-6)
      # cj_instr = asm(f"je {hex(t)}", vma=va_start)
    self.do_patch(va_start, cj_instr)
    va_start += len(cj_instr)
  va_start += self.patch_jmp(va_start, va_targets[-1])
  if va_start > org_start+size:
    print("[Warning] patches at (%x, %x) overlaps next blk. "%(org_start, va_start))
def patch_branches(self, bb, va_targets):
  va_start, size = self.get_patchable_from_relblk(bb)
  if size < PatchHelper.JMP_SIZE:
    print("[Warning] patch_jmp at block %x may fail. size: %d."%(bb.address, size))
  org_start = va_start
  print(f"va_start: {hex(va_start)}, bb addr: {hex(bb.address)}, size: {size}")
  ## `cmp esi, v` instr takes 3 bytes while `je xxx` takes 6 bytes
  ## And the last jmp instr takes 5 bytes.
  total_size = 9 * len(va_targets) - 4
  if size < total_size:
    ## If the nop block at the end of current block is not large enough,
       ## try to find another nop block and then jump to it.
    nx_va_start, nx_size = self.get_nop_by_size(total_size)
    if nx_size == 0:
      print("[Error] `patch_branches` needs a nop block with size larger than %d."%(total_size))
    self.patch_jmp(va_start, nx_va_start)
    va_start, size = nx_va_start, nx_size
 
  for i, t in enumerate(va_targets[:-1]):
    cmp_instr = bytes([0x83,0xfe,i])
    self.do_patch(va_start, cmp_instr)
    va_start += len(cmp_instr)
    cj_instr = bytes([PatchHelper.opcode['j'],PatchHelper.opcode['e']])
    if t == -1:
      ## -1 represent that we do not know the flow for this selector value for now.
      cj_instr += struct.pack('<i', self.func_terminate-va_start-6)
      # cj_instr = asm(f"je {hex(self.func_terminate)}", vma=va_start)
    else:
      cj_instr += struct.pack('<i', t-va_start-6)
      # cj_instr = asm(f"je {hex(t)}", vma=va_start)
    self.do_patch(va_start, cj_instr)
    va_start += len(cj_instr)
  va_start += self.patch_jmp(va_start, va_targets[-1])
  if va_start > org_start+size:
    print("[Warning] patches at (%x, %x) overlaps next blk. "%(org_start, va_start))
## filename: deflat.py
from ast import Tuple
from xmlrpc.client import Boolean
from barf.barf import BARF
import angr
import struct
import sys
from pwnlib import elf
from queue import SimpleQueue
# from pwn import *
 
class PatchHelper:
  opcode = {'a' :0x87, 'ae':0x83, 'b' :0x82, 'be':0x86, 'c' :0x82, 'e' :0x84, 'z' :0x84, 'g' :0x8F,
            'ge':0x8D, 'l' :0x8C, 'le':0x8E, 'na':0x86, 'nae':0x82,'nb':0x83, 'nbe':0x87,'nc':0x83,
            'ne':0x85, 'ng':0x8E, 'nge':0x8C,'nl':0x8D, 'nle':0x8F,'no':0x81, 'np':0x8B, 'ns':0x89,
            'nz':0x85, 'o' :0x80, 'p' :0x8A, 'pe':0x8A, 'po':0x8B, 's' :0x88, 'nop':0x90,'jmp':0xE9, 'j':0x0F}
  JMP_SIZE = 5
 
  def is_unreachable(self, bb):
    if isinstance(bb, int):
      bb = self.block(bb)
    for i in range(len(bb.instrs)):
      if bb.instrs[i].mnemonic != "call":
        continue
      target = bb.instrs[i].operands[0].immediate
      if target == self.func_terminate:
        return True
 
  def block(self, addr):
    bb = self.cfg.find_basic_block(addr)
    if bb is None:
      bb = barf.bb_builder.strategy._disassemble_bb(addr, barf.binary.ea_end, {})
    return bb
 
  @staticmethod
  def is_imm(operand):
    return (hasattr(operand, "_X86ImmediateOperand__key"))
 
  @staticmethod
  def is_reg(operand):
    return (hasattr(operand, "_X86RegisterOperand__key"))
 
  def is_call_throw(self, instr):
    return instr.mnemonic == "call" and \
        self.is_imm(instr.operands[0]) and\
        instr.operands[0].immediate == self.func_throw
 
  def is_call_allocate_exception(self, instr):
    return instr.mnemonic == "call" and \
        self.is_imm(instr.operands[0]) and\
        instr.operands[0].immediate == self.func_allocate_exception
 
  def is_call_obf_exception(self, instr):
    return instr.mnemonic == "call" and \
        self.is_imm(instr.operands[0]) and\
        instr.operands[0].immediate == self.func_obf_exception
 
 
  def skip_call_args(self, bb, i):
    while ((bb.instrs[i].mnemonic in ["xor","mov","lea"]) and\
      (len(bb.instrs[i].operands) > 0) and (self.is_reg(bb.instrs[i].operands[0])) and\
      (bb.instrs[i].operands[0].name in ["edx", "rdx", "esi", "rsi", "edi", "rdi"])) or \
      bb.instrs[i].mnemonic == "nop":
      i -= 1
    return i
 
  def get_patchable_from_relblk(self, bb):
    i = 0
    end = bb.start_address + bb.size
    while i < len(bb.instrs) and not self.is_call_throw(bb.instrs[i]):
      i += 1
    i = self.skip_call_args(bb, i-1)
    if i == len(bb.instrs) - 1:
      start = end
    else:
      start = bb.instrs[i+1].address
    self.fill_nops(start, end)
    return (start, end-start)
 
  def __init__(self, proj, elf, barf, cfg) -> None:
    self.p = proj
    obj = proj.loader.main_object
    self.func_terminate = obj.symbols_by_name["__clang_call_terminate"].rebased_addr
    self.func_throw = obj.plt["__cxa_throw"]
    self.func_allocate_exception = obj.plt["__cxa_allocate_exception"]
    self.func_obf_exception = obj.symbols_by_name["_ZN18StdSubObfExceptionC2Ec"].rebased_addr
    self.elf = elf
    self.elfData = bytearray(self.elf.data)
    self.barf = barf
    self.cfg = cfg
    self.nops = []
 
  def append_nop(self, nopblk):
    if nopblk[1] > 0:
      self.nops.append(nopblk)
 
  def finalize(self):
    self.nops.sort()
    idx = 0
    while idx < len(self.nops) - 1:
      if self.nops[idx][0] + self.nops[idx][1] != self.nops[idx+1][0]:
        idx += 1
        continue
      self.nops[idx]=(self.nops[idx][0], self.nops[idx][1]+self.nops[idx+1][1])
      del self.nops[idx+1]
 
  def fill_nops(self, va_start, va_end):
    assert not self.elf is None
    start = self.elf.vaddr_to_offset(va_start)
    end   = self.elf.vaddr_to_offset(va_end)
    for i in range(start, end):
      self.elfData[i] = PatchHelper.opcode['nop']
 
  def get_nop_by_size(self, min_size):
    for idx, nop in enumerate(self.nops):
      if nop[1] > min_size:
        del self.nops[idx]
        return nop
    return (-1, 0)
 
  def do_patch(self, va_start, codes):
    start = self.elf.vaddr_to_offset(va_start)
    for i in range(len(codes)):
      self.elfData[start+i] = codes[i]
 
  def patch_jmp(self, va_start, va_target):
    offset = va_target - va_start - PatchHelper.JMP_SIZE
    jmp = bytes([PatchHelper.opcode['jmp']])+struct.pack('<i', offset)
    self.do_patch(va_start, jmp)
    return PatchHelper.JMP_SIZE
 
  def patch_branches(self, bb, va_targets):
    va_start, size = self.get_patchable_from_relblk(bb)
    if size < PatchHelper.JMP_SIZE:
      print("[Warning] patch_jmp at block %x may fail. size: %d."%(bb.address, size))
    org_start = va_start
    print(f"va_start: {hex(va_start)}, bb addr: {hex(bb.address)}, size: {size}")
    ## `cmp esi, v` instr takes 3 bytes while `je xxx` takes 6 bytes
    ## And the last jmp instr takes 5 bytes.
    total_size = (3+6) * len(va_targets) - 4
    if size < total_size:
      ## If the nop block at the end of current block is not large enough,
         ## try to find another nop block and then jump to it.
      nx_va_start, nx_size = self.get_nop_by_size(total_size)
      if nx_size == 0:
        print("\033[31m[Error]\033[0m `patch_branches` needs a nop block with size larger than %d."%(total_size))
      self.patch_jmp(va_start, nx_va_start)
      va_start, size = nx_va_start, nx_size
    for i, t in enumerate(va_targets[:-1]):
      cmp_instr = bytes([0x83,0xfe,i])
      self.do_patch(va_start, cmp_instr)
      va_start += len(cmp_instr)
      cj_instr = bytes([PatchHelper.opcode['j'],PatchHelper.opcode['e']])
      if t == -1:
        ## -1 represent that we do not know the flow for this selector value for now.
        cj_instr += struct.pack('<i', self.func_terminate-va_start-6)
        # cj_instr = asm(f"je {hex(self.func_terminate)}", vma=va_start)
      else:
        cj_instr += struct.pack('<i', t-va_start-6)
        # cj_instr = asm(f"je {hex(t)}", vma=va_start)
      self.do_patch(va_start, cj_instr)
      va_start += len(cj_instr)
    va_start += self.patch_jmp(va_start, va_targets[-1])
    if va_start > org_start+size:
      print("[Warning] patches at (%x, %x) overlaps next blk. "%(org_start, va_start))
 
def get_relevant_blocks(cfg, patch_helper, main_dispatcher):
  isCmpRI = lambda instr: instr.mnemonic == "cmp" and\
    hasattr(instr.operands[0], "_X86RegisterOperand__key") and\
    hasattr(instr.operands[1], "_X86ImmediateOperand__key")
  isCJmp = lambda instr: instr.mnemonic.startswith("j") and \
    instr.mnemonic != "jmp"
  isSubDispatcher = lambda bb: (len(bb.instrs) == 2) and\
     isCmpRI(bb.instrs[0]) and isCJmp(bb.instrs[1])
  relevant_blocks = []
  visited = set()
  q = SimpleQueue()
  q.put(patch_helper.block(main_dispatcher))
  while not q.empty():
    bb = q.get()
    if isSubDispatcher(bb):
      patch_helper.append_nop((bb.start_address, bb.size))
      for succ, cond in bb.branches:
        if succ in visited:
          continue
        q.put(patch_helper.block(succ))
        visited.add(succ)
    else:
      relevant_blocks.append(bb)
  return relevant_blocks
 
 
def parse_logs(logfn, prologue, patch_helper):
  with open(logfn, "r") as f:
    t = f.readlines()
  i = 0
  selector_s = "selector: "
  landingpad_s = "landingPad: "
  relations = set()
  laddr = prologue
  lselector = 0
  landingpad = 0
  while i < len(t):
    try:
      addr = int(t[i], 16)
    except:
      i += 1
      continue
    if not laddr is None:
      relations.add((laddr, lselector, addr))
    if t[i+1].startswith(selector_s):
      selector = int(t[i+1][len(selector_s):], 16)
      i += 2
    elif t[i+1].startswith(landingpad_s):
      landingpad = int(t[i+1][len(landingpad_s):], 16)
      relations.add((addr, -1, landingpad))
      addr = landingpad
      while not patch_helper.is_unreachable(patch_helper.block(addr).direct_branch):
        addr = patch_helper.block(addr).direct_branch
      if t[i+2].startswith(selector_s):
        selector = int(t[i+2][len(selector_s):], 16)
      i += 3
    elif t[i+1].startswith("[Inferior "):
      i += 1
    else:
      print("Warning: %x doesn't have selector. "%addr)
      exit(0)
    laddr = addr
    lselector = selector
  return list(relations)
 
 
def generate_gdb_script(relevant_blocks):
  cmds = """\
set pagination off
 
b *0x40A3D4
commands
  silent
  printf "landingPad: %x\n", $rdx
  continue
end
 
b _ZN18StdSubObfExceptionC2Ec
commands
  silent
  printf "selector: %x\n", $rsi
  continue
end
 
define mytrace
  break $arg0
  commands
    silent
    printf "%x\\n", $pc
    python gdb.execute('continue')
  end
end
"""
  for bb in relevant_blocks:
    cmds += (f"mytrace *{hex(bb.address)} \n")
  cmds += "run\n"
  with open("test.gdb", "w") as f:
    f.write(cmds)
 
 
if __name__ == '__main__':
    if len(sys.argv) < 3:
        print('Usage: python deflat.py filename function_address(hex) [logfile]')
        exit(0)
 
    # context.arch = "amd64"
    # context.os = "linux"
    # context.endian = "little"
 
    filename = sys.argv[1]
    start = int(sys.argv[2], 16)
 
    origin = elf.ELF(filename)
    b = angr.Project(filename, load_options={'auto_load_libs': False, 'main_opts':{'custom_base_addr': 0}})
    barf = BARF(filename)
    cfg = barf.recover_cfg(start=start)
    patch_helper = PatchHelper(b, origin, barf, cfg)
    blocks = cfg.basic_blocks
 
    prologue = start
    main_dispatcher = patch_helper.block(prologue).direct_branch
    relevant_blocks = get_relevant_blocks(cfg, patch_helper, main_dispatcher)
    nop = patch_helper.get_patchable_from_relblk(patch_helper.block(prologue))
    patch_helper.append_nop(nop)
 
    print('*******************relevant blocks************************')
    print('main_dispatcher:%#x' % main_dispatcher)
    print('relevant_blocks:', [hex(bb.address) for bb in relevant_blocks])
 
 
    if len(sys.argv) < 4:
      generate_gdb_script(relevant_blocks)
      exit(0)
 
    print('************************flow******************************')
    relations = parse_logs(sys.argv[3], prologue, patch_helper)
    relations.sort(key = lambda x:x)
    flow = {}
    for bb, selector, child in relations:
      if bb in flow:
        while len(flow[bb]) < selector:
          flow[bb].append(-1)
        flow[bb].append(child)
        assert(len(flow[bb]) == selector+1)
      else:
        flow[bb] = [child]
    for (k, v) in list(flow.items()):
        print('%#x:' % k, [hex(child) for child in v])
 
    print('************************patch*****************************')
    patch_helper.finalize()
    for (parent, childs) in list(flow.items()):
      ## Patch jmps
      blk = patch_helper.block(parent)
      patch_helper.patch_branches(blk, childs)
      ## Nop call allocate_exception and call obf_exception
      for idx, instr in enumerate(blk.instrs):
        if patch_helper.is_call_allocate_exception(instr) or\
          patch_helper.is_call_obf_exception(instr):
          # si = patch_helper.skip_call_args(blk, idx-1)+1
          # start = blk.instrs[si].address
          start = instr.address
          end = instr.address + instr.size
          patch_helper.fill_nops(start, end)
 
    with open(filename + '.recovered', 'wb') as f:
        f.write(bytes(patch_helper.elfData))
    print('Successful! The recovered file: %s' % (filename + '.recovered'))
## filename: deflat.py
from ast import Tuple
from xmlrpc.client import Boolean
from barf.barf import BARF
import angr
import struct
import sys
from pwnlib import elf
from queue import SimpleQueue
# from pwn import *
 
class PatchHelper:
  opcode = {'a' :0x87, 'ae':0x83, 'b' :0x82, 'be':0x86, 'c' :0x82, 'e' :0x84, 'z' :0x84, 'g' :0x8F,
            'ge':0x8D, 'l' :0x8C, 'le':0x8E, 'na':0x86, 'nae':0x82,'nb':0x83, 'nbe':0x87,'nc':0x83,
            'ne':0x85, 'ng':0x8E, 'nge':0x8C,'nl':0x8D, 'nle':0x8F,'no':0x81, 'np':0x8B, 'ns':0x89,
            'nz':0x85, 'o' :0x80, 'p' :0x8A, 'pe':0x8A, 'po':0x8B, 's' :0x88, 'nop':0x90,'jmp':0xE9, 'j':0x0F}
  JMP_SIZE = 5
 
  def is_unreachable(self, bb):
    if isinstance(bb, int):
      bb = self.block(bb)
    for i in range(len(bb.instrs)):
      if bb.instrs[i].mnemonic != "call":
        continue
      target = bb.instrs[i].operands[0].immediate
      if target == self.func_terminate:
        return True
 
  def block(self, addr):
    bb = self.cfg.find_basic_block(addr)
    if bb is None:
      bb = barf.bb_builder.strategy._disassemble_bb(addr, barf.binary.ea_end, {})
    return bb
 
  @staticmethod
  def is_imm(operand):
    return (hasattr(operand, "_X86ImmediateOperand__key"))
 
  @staticmethod
  def is_reg(operand):
    return (hasattr(operand, "_X86RegisterOperand__key"))
 
  def is_call_throw(self, instr):
    return instr.mnemonic == "call" and \
        self.is_imm(instr.operands[0]) and\
        instr.operands[0].immediate == self.func_throw
 
  def is_call_allocate_exception(self, instr):
    return instr.mnemonic == "call" and \
        self.is_imm(instr.operands[0]) and\
        instr.operands[0].immediate == self.func_allocate_exception
 
  def is_call_obf_exception(self, instr):
    return instr.mnemonic == "call" and \
        self.is_imm(instr.operands[0]) and\
        instr.operands[0].immediate == self.func_obf_exception
 
 
  def skip_call_args(self, bb, i):
    while ((bb.instrs[i].mnemonic in ["xor","mov","lea"]) and\
      (len(bb.instrs[i].operands) > 0) and (self.is_reg(bb.instrs[i].operands[0])) and\
      (bb.instrs[i].operands[0].name in ["edx", "rdx", "esi", "rsi", "edi", "rdi"])) or \
      bb.instrs[i].mnemonic == "nop":
      i -= 1
    return i
 
  def get_patchable_from_relblk(self, bb):
    i = 0
    end = bb.start_address + bb.size
    while i < len(bb.instrs) and not self.is_call_throw(bb.instrs[i]):
      i += 1
    i = self.skip_call_args(bb, i-1)
    if i == len(bb.instrs) - 1:
      start = end
    else:
      start = bb.instrs[i+1].address
    self.fill_nops(start, end)
    return (start, end-start)
 
  def __init__(self, proj, elf, barf, cfg) -> None:
    self.p = proj
    obj = proj.loader.main_object
    self.func_terminate = obj.symbols_by_name["__clang_call_terminate"].rebased_addr
    self.func_throw = obj.plt["__cxa_throw"]
    self.func_allocate_exception = obj.plt["__cxa_allocate_exception"]
    self.func_obf_exception = obj.symbols_by_name["_ZN18StdSubObfExceptionC2Ec"].rebased_addr
    self.elf = elf
    self.elfData = bytearray(self.elf.data)
    self.barf = barf
    self.cfg = cfg
    self.nops = []
 
  def append_nop(self, nopblk):
    if nopblk[1] > 0:
      self.nops.append(nopblk)
 
  def finalize(self):
    self.nops.sort()
    idx = 0
    while idx < len(self.nops) - 1:
      if self.nops[idx][0] + self.nops[idx][1] != self.nops[idx+1][0]:
        idx += 1
        continue
      self.nops[idx]=(self.nops[idx][0], self.nops[idx][1]+self.nops[idx+1][1])
      del self.nops[idx+1]
 
  def fill_nops(self, va_start, va_end):
    assert not self.elf is None
    start = self.elf.vaddr_to_offset(va_start)
    end   = self.elf.vaddr_to_offset(va_end)
    for i in range(start, end):
      self.elfData[i] = PatchHelper.opcode['nop']
 
  def get_nop_by_size(self, min_size):
    for idx, nop in enumerate(self.nops):
      if nop[1] > min_size:
        del self.nops[idx]
        return nop
    return (-1, 0)
 
  def do_patch(self, va_start, codes):
    start = self.elf.vaddr_to_offset(va_start)
    for i in range(len(codes)):
      self.elfData[start+i] = codes[i]
 
  def patch_jmp(self, va_start, va_target):
    offset = va_target - va_start - PatchHelper.JMP_SIZE
    jmp = bytes([PatchHelper.opcode['jmp']])+struct.pack('<i', offset)
    self.do_patch(va_start, jmp)
    return PatchHelper.JMP_SIZE
 
  def patch_branches(self, bb, va_targets):
    va_start, size = self.get_patchable_from_relblk(bb)
    if size < PatchHelper.JMP_SIZE:
      print("[Warning] patch_jmp at block %x may fail. size: %d."%(bb.address, size))
    org_start = va_start
    print(f"va_start: {hex(va_start)}, bb addr: {hex(bb.address)}, size: {size}")
    ## `cmp esi, v` instr takes 3 bytes while `je xxx` takes 6 bytes
    ## And the last jmp instr takes 5 bytes.
    total_size = (3+6) * len(va_targets) - 4
    if size < total_size:
      ## If the nop block at the end of current block is not large enough,
         ## try to find another nop block and then jump to it.
      nx_va_start, nx_size = self.get_nop_by_size(total_size)
      if nx_size == 0:
        print("\033[31m[Error]\033[0m `patch_branches` needs a nop block with size larger than %d."%(total_size))
      self.patch_jmp(va_start, nx_va_start)
      va_start, size = nx_va_start, nx_size
    for i, t in enumerate(va_targets[:-1]):
      cmp_instr = bytes([0x83,0xfe,i])
      self.do_patch(va_start, cmp_instr)
      va_start += len(cmp_instr)
      cj_instr = bytes([PatchHelper.opcode['j'],PatchHelper.opcode['e']])
      if t == -1:
        ## -1 represent that we do not know the flow for this selector value for now.
        cj_instr += struct.pack('<i', self.func_terminate-va_start-6)
        # cj_instr = asm(f"je {hex(self.func_terminate)}", vma=va_start)
      else:
        cj_instr += struct.pack('<i', t-va_start-6)
        # cj_instr = asm(f"je {hex(t)}", vma=va_start)
      self.do_patch(va_start, cj_instr)
      va_start += len(cj_instr)
    va_start += self.patch_jmp(va_start, va_targets[-1])
    if va_start > org_start+size:
      print("[Warning] patches at (%x, %x) overlaps next blk. "%(org_start, va_start))
 
def get_relevant_blocks(cfg, patch_helper, main_dispatcher):
  isCmpRI = lambda instr: instr.mnemonic == "cmp" and\
    hasattr(instr.operands[0], "_X86RegisterOperand__key") and\
    hasattr(instr.operands[1], "_X86ImmediateOperand__key")
  isCJmp = lambda instr: instr.mnemonic.startswith("j") and \
    instr.mnemonic != "jmp"
  isSubDispatcher = lambda bb: (len(bb.instrs) == 2) and\
     isCmpRI(bb.instrs[0]) and isCJmp(bb.instrs[1])
  relevant_blocks = []
  visited = set()
  q = SimpleQueue()
  q.put(patch_helper.block(main_dispatcher))
  while not q.empty():
    bb = q.get()
    if isSubDispatcher(bb):
      patch_helper.append_nop((bb.start_address, bb.size))
      for succ, cond in bb.branches:
        if succ in visited:
          continue
        q.put(patch_helper.block(succ))
        visited.add(succ)
    else:
      relevant_blocks.append(bb)
  return relevant_blocks
 
 
def parse_logs(logfn, prologue, patch_helper):
  with open(logfn, "r") as f:
    t = f.readlines()
  i = 0
  selector_s = "selector: "
  landingpad_s = "landingPad: "
  relations = set()
  laddr = prologue
  lselector = 0
  landingpad = 0
  while i < len(t):
    try:
      addr = int(t[i], 16)
    except:
      i += 1
      continue
    if not laddr is None:
      relations.add((laddr, lselector, addr))
    if t[i+1].startswith(selector_s):
      selector = int(t[i+1][len(selector_s):], 16)
      i += 2
    elif t[i+1].startswith(landingpad_s):
      landingpad = int(t[i+1][len(landingpad_s):], 16)
      relations.add((addr, -1, landingpad))
      addr = landingpad
      while not patch_helper.is_unreachable(patch_helper.block(addr).direct_branch):
        addr = patch_helper.block(addr).direct_branch
      if t[i+2].startswith(selector_s):
        selector = int(t[i+2][len(selector_s):], 16)
      i += 3
    elif t[i+1].startswith("[Inferior "):
      i += 1
    else:
      print("Warning: %x doesn't have selector. "%addr)
      exit(0)
    laddr = addr
    lselector = selector
  return list(relations)
 
 
def generate_gdb_script(relevant_blocks):
  cmds = """\
set pagination off
 
b *0x40A3D4
commands
  silent
  printf "landingPad: %x\n", $rdx
  continue
end
 
b _ZN18StdSubObfExceptionC2Ec
commands
  silent
  printf "selector: %x\n", $rsi
  continue
end
 
define mytrace
  break $arg0
  commands
    silent
    printf "%x\\n", $pc
    python gdb.execute('continue')
  end
end
"""
  for bb in relevant_blocks:
    cmds += (f"mytrace *{hex(bb.address)} \n")
  cmds += "run\n"
  with open("test.gdb", "w") as f:
    f.write(cmds)
 
 
if __name__ == '__main__':
    if len(sys.argv) < 3:
        print('Usage: python deflat.py filename function_address(hex) [logfile]')
        exit(0)
 
    # context.arch = "amd64"
    # context.os = "linux"
    # context.endian = "little"
 
    filename = sys.argv[1]
    start = int(sys.argv[2], 16)
 
    origin = elf.ELF(filename)
    b = angr.Project(filename, load_options={'auto_load_libs': False, 'main_opts':{'custom_base_addr': 0}})
    barf = BARF(filename)
    cfg = barf.recover_cfg(start=start)
    patch_helper = PatchHelper(b, origin, barf, cfg)
    blocks = cfg.basic_blocks
 
    prologue = start
    main_dispatcher = patch_helper.block(prologue).direct_branch
    relevant_blocks = get_relevant_blocks(cfg, patch_helper, main_dispatcher)
    nop = patch_helper.get_patchable_from_relblk(patch_helper.block(prologue))
    patch_helper.append_nop(nop)

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2022-7-21 22:31 被P.Z编辑 ,原因:
上传的附件:
收藏
免费 5
支持
分享
打赏 + 100.00雪花
打赏次数 1 雪花 + 100.00
 
赞赏  Editor   +100.00 2022/08/10 恭喜您获得“雪花”奖励,安全圈有你而精彩!
最新回复 (11)
雪    币: 39
活跃值: (2846)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
来,挑战一下我的ARealBug。
2022-7-21 20:27
0
雪    币: 2594
活跃值: (1593)
能力值: ( LV8,RANK:125 )
在线值:
发帖
回帖
粉丝
3

我会一直研究的!!

最后于 2022-7-21 22:29 被P.Z编辑 ,原因:
2022-7-21 21:36
0
雪    币: 2594
活跃值: (1593)
能力值: ( LV8,RANK:125 )
在线值:
发帖
回帖
粉丝
4
NutCracker 来,挑战一下我的ARealBug。
我会一直研究的!
2022-7-21 21:39
0
雪    币: 15
活跃值: (1853)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
师傅写的够详细,很保姆,就是有两图显示不出来了2333
2022-7-21 21:54
0
雪    币: 2594
活跃值: (1593)
能力值: ( LV8,RANK:125 )
在线值:
发帖
回帖
粉丝
6
mb_rhynjqzk 师傅写的够详细,很保姆,就是有两图显示不出来了2333
已修正)
2022-7-21 22:10
0
雪    币: 2594
活跃值: (1593)
能力值: ( LV8,RANK:125 )
在线值:
发帖
回帖
粉丝
7

在bilibili上录制的一期这题的视频讲解

https://www.bilibili.com/video/BV1ad4y1S74W?spm_id_from=333.999.0.0&vd_source=f4fa3266c2bdd0e06034b3230cd27c93

最后于 2022-8-9 20:23 被P.Z编辑 ,原因:
2022-8-9 20:23
1
雪    币: 309
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
8
2022-8-12 16:27
0
雪    币: 8745
活跃值: (5673)
能力值: ( LV13,RANK:296 )
在线值:
发帖
回帖
粉丝
9
好文
2022-8-12 16:48
0
雪    币: 2594
活跃值: (1593)
能力值: ( LV8,RANK:125 )
在线值:
发帖
回帖
粉丝
10
2022-8-16 14:45
0
雪    币: 2594
活跃值: (1593)
能力值: ( LV8,RANK:125 )
在线值:
发帖
回帖
粉丝
11
sunfishi 好文[em_63]
Mas0n
2022-8-16 14:49
0
雪    币: 906
活跃值: (1773)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
12
牛皮皮
2023-12-7 21:44
0
游客
登录 | 注册 方可回帖
返回
//