Loading

ACTF2022-inflated复现

image-20220710161319433

总结来说,利用去除控制流平坦化的步骤可以归结为三个步骤:

  1. 分析CFG得到序言/入口块(Prologue)、主分发器(Main dispatcher)、子分发器/无用块(Sub dispatchers)、真实块(Relevant blocks)、预分发器(PreDispatcher)和返回块(Return)
  2. 利用恢复真实块的前后关系,重建控制流
  3. 根据第二步重建的控制流Patch程序,输出恢复后的可执行文件

在这里插入图片描述

https://blog.csdn.net/qq_45323960/article/details/124440184

https://code.woboq.org/llvm/libcxxabi/src/cxa_personality.cpp.html#__cxxabiv1::scan_eh_tab

https://github.com/Lnkvct/CTF-for-Fun/blob/main/Challenges/Inflated-ACTF2022/writeup.md

inflated

前言 总体分析

关键词: 控制流平坦化 异常处理 abi库函数魔改

都什么年代了 还在做传统的控制流平坦化

**所以选择运用魔改异常处理来控制跳转哒! **

传统控制流平坦化(-fla编译)使用cmp等数据比较 进行各种块之间的跳转

在本题中出题人使用了自定义的异常和魔改的异常处理abi函数来操控跳转

所以尝试使用各种deflated脚本均报错 )(因为throw函数会造成路径爆炸 angr无法确定各种块之间的关系 尽管throw函数最后的行为只是为edx赋值 但该值来源却很复杂(

小tip:异常处理中多个catch块时运用edx比较来确定调用哪个catch

异常处理abi库函数的魔改导致catch段与各个block间的关系识别的不正确

综上我只能使用动态调试来解决以上问题

(本篇文章按照时间顺序进行)

尝试

从_getc函数开始

main函数中一共有三处getc函数

000407629的getc

.text:0000000000407629 call    _getc
.text:000000000040762E mov     [rsp+5E8h+var_58C], eax 
                       eax就是输入的单个字符 我输入了'1' 即0x31
.text:0000000000407632 shl     eax, 18h
                       左移为0x31000000
.text:0000000000407635 xor     ebp, ebp
					   ebp置0
.text:0000000000407637 cmp     eax, 1B000000h
.text:000000000040763C setz    bpl
					   当eax==1B000000时bpl(ebp)为1
.text:0000000000407640 cmp     eax, 31000000h
.text:0000000000407645 mov     ecx, 2
.text:000000000040764A cmovz   ebp, ecx
					   当eax==31000000时ebp为2
.text:000000000040764D cmp     eax, 37000000h
.text:0000000000407652 mov     ecx, 3
.text:0000000000407657 cmovz   ebp, ecx
                       当eax==37000000时ebp为3
.text:000000000040765A cmp     eax, 33000000h
.text:000000000040765F mov     ecx, 4
.text:0000000000407664 cmovz   ebp, ecx
					   当eax==33000000时ebp为4
.text:0000000000407667 cmp     eax, 34000000h
.text:000000000040766C mov     eax, 5
.text:0000000000407671 cmovz   ebp, eax
                       当eax==34000000时ebp为5
.text:0000000000407674 mov     edi, 1          ; thrown_size
.text:0000000000407679 call    ___cxa_allocate_exception
					   ___cxa_allocate_exception为创建一个异常对象 开辟一个内存 rax为该异常对象地                        址 和malloc类似
.text:000000000040767E mov     r12, rax
	                   将该异常指针存在r12
.text:0000000000407681 movzx   esi, bpl        ; char
.text:0000000000407685 mov     rdi, rax        ; this
.text:0000000000407688 call    _ZN18StdSubObfExceptionC2Ec ;     StdSubObfException::StdSubObfException(char)
.text:0000000000407688 ;   } // starts at 4075F4
                       创建一个异常结构体 bpl值为异常类型 rdi值为该结构体指针
 
.text:000000000040768D ;   try {
.text:000000000040768D mov     [rsp+5E8h+var_570], r15
.text:0000000000407692 mov     [rsp+5E8h+var_568], rbx
.text:000000000040769A mov     [rsp+5E8h+var_5E0], rbx
.text:000000000040769F mov     [rsp+5E8h+var_4F0], rbx
                       保存寄存器?
.text:00000000004076A7 mov     rdi, r12        ; void *
.text:00000000004076AA mov     rsi, offset _ZTI6aQxES2 ; lptinfo
.text:00000000004076B1 xor     edx, edx        ; void (__fastcall *)(void *)
.text:00000000004076B3 call    ___cxa_throw
                       根据异常类型进行异常处理分发
.text:00000000004076B3 ;   } // starts at 40768D

伪代码

inputchar=input()
bugtype=0
if inputchar==0x1b:"输入上下左右箭头时为该分支"
    bugtype=1
elif inputchar==0x31:'1'
    bugtype=2
elif inputchar==0x37:'7'
    bugtype=3	
elif inputchar==0x33:'3'
    bugtype=4
elif inputchar==0x34:'4'
    bugtype=5
else:
    bugtype=0

动调探测 发现输入上下左右箭头时命中0x1b分支

上下左右箭头 ^[[A ^[[B ^[[C ^[[D

好像这个叫三字节编码?

0000405676的getc

不接受数字与字母输入 只接受上下左右箭头

分别对应A B C D ascii码

对于该处的throw 异常类型即为ABCD的ascii码

0040553A的getc

不接受数字与字母输入

检测的是三字节编码的第二位或者第三位?

.text:000000000040553A call    _getc
.text:000000000040553F xor     ebp, ebp
.text:0000000000405541 cmp     eax, 5Bh ; '['
.text:0000000000405544 setz    bpl
.text:0000000000405548 mov     edi, 1          ; thrown_size
.text:000000000040554D call    ___cxa_allocate_exception
.text:0000000000405552 mov     rbx, rax
.text:0000000000405555 mov     rdi, rax        ; this
.text:0000000000405558 mov     esi, ebp        ; char
.text:000000000040555A call    _ZN18StdSubObfExceptionC2Ec ; StdSubObfException::StdSubObfException(char)
.text:000000000040555A ;   } // starts at 4054D6

如果是上下左右箭头 异常类型bugtype 是1 否则是0

getc总结

000407629既能处理箭头输入又能处理正常ascii码输入

0000405676和0040553A专门用来处理箭头输入

gdb反向执行

针对00407629下的throw 到底跳转到哪个路径 用gdb进行调试

在0004089d1 断点 使用record

在4093C9下断点 因为该块为PreDispatcher 与其逻辑相连的块为Relevant_blocks

输入 类型 jmp地址 块地址

上 1 408b50 0408B4B

1 2 408b1c 0408B17

7 3 408ae8 00408AE3

3 4 408ab4 000408AAF

4 5 408a80 000408A7B

其他 408b81 000408B55

如果你看到这了(就可以用lchild的wp复现啦 手动根据跳转patch几百个throw 我愿称为官方wp的手动版

再看异常处理

想抽象异常处理过程 用于angr hook相关函数

但是才疏学浅 当阅读源码到需要解析.gcc_except_table结构后 就放弃了 但是学到的异常处理流程让我受益匪浅

最后还是选择复现了官方wp中的gdb流追踪技术

以下文章写的很好!:

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

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

https://zhuanlan.zhihu.com/p/478589988

先来了解一下异常处理流程吧

异常抛出流程

  1. 调用 __cxa_allocate_exception 函数,分配一个异常对象。 返回值为rax,指向该异常对象地址 和malloc类似

  2. 调用 __cxa_throw 函数,传入异常结构体,这个函数会将异常对象做一些初始化。

  3. __cxa_throw() 调用 Itanium ABI 里的 _Unwind_RaiseException() 从而开始 unwind。

  4. _Unwind_RaiseException() 对调用链上的函数进行 unwind 时,调用 personality routine(即__gxx_personality_v0)。

  5. 该异常如能被处理(有相应的 catch),则 personality routine 会依次对调用链上的函数进行清理。

  6. _Unwind_RaiseException() 将控制权转到相应的catch代码。

  7. unwind 完成,用户代码继续执行。

异常抛出后

根据 c++ 的标准,异常抛出后如果在当前函数内没有被捕捉(catch),它就要沿着函数的调用链继续往上抛,直到走完整个调用链,或者在某个函数中找到相应的 catch。

如果走完调用链都没有找到相应的 catch,那么 std::terminate() 就会被调用,这个函数默认是把程序 abort,

而如果最后找到了相应的 catch,就会进入该 catch 代码块,执行相应的操作。

程序中的 catch 那部分代码有一个专门的名字叫作:Landingpad Landingpad指catch代码块地址指针

从抛异常开始到执行 landing pad 里的代码这中间的整个过程叫作 stack unwind,这个过程包含了两个阶段:

  1. 从抛异常的函数开始,对调用链上的函数逐个往前查找 landing pad。

  2. 如果没有找到 landing pad 则把程序 abort,

  3. 如果找到则记下 landing pad 的位置,再重新回到抛异常的函数那里开始,一帧一帧地清理调用链上各个函数内部的局部变量,直到 landing pad 所在的函数为止。(即恢复现场

简而言之,正常情况下,stack unwind 所要做的事情就是从抛出异常的函数开始,沿着调用链向上找 catch 所在的函数,然后从抛异常的地方开始,清理调用链上各栈帧内已经创建了的局部变量。

Unwind_RaiseException()

源码:https://code.woboq.org/llvm/libunwind/src/UnwindLevel1.c.html#347

其中的 Unwind_RaiseException() 函数用于进行 stack unwind,它在用户执行 throw 时被调用,主要功能是从当前函数开始,对调用链上每个函数都调用一个叫作 personality routine 的函数(__gxx_personality_v0),该函数由上层的语言定义及提供实现,Unwind_RaiseException() 会在内部把当前函数栈的调用现场重建,然后传给 personality routine,

因此我们不需要关心Unwind_RaiseException的实现细节

__gxx_personality_v0 (即personality routine)

源码:https://code.woboq.org/llvm/libcxxabi/src/cxa_personality.cpp.html#918

在gcc中 personality routine指__gxx_personality_v0函数

personality routine 则主要负责做两件事情:

1)检查当前函数是否含有相应 catch 可以处理上面抛出的异常。

2)清掉调用栈上的局部变量。

显然,我们可以发现 personality routine 所做的这两件事情和前面所说的 stack unwind 所要经历的两个阶段一一对应起来了,因此也可以说,stack unwind 主要就是由 personality routine 来完成,它相当于一个 callback。

在ida中修改函数

_Unwind_Reason_Code __fastcall _gxx_personality_v0(
        int Version,
        _Unwind_Action actions,
        __int64 exceptionClass,
        _Unwind_Exception *exceptionObject,
        _Unwind_Context *context)

actions用来告诉 __gxx_personality_v0 ,当前处于 stack unwind 的哪个阶段,

Unwind_Context 用于表示程序运行时的上下文,主要就是一些寄存器的值,函数返回地址等,它由接口实现者来定义及创建,在 gcc 的源码里的定义

struct _Unwind_Context
{
  void *reg[DWARF_FRAME_REGISTERS+1];
  void *cfa;
  void *ra;
  void *lsda;
  struct dwarf_eh_bases bases;
  _Unwind_Word args_size;
};

exceptionObject 用来存储异常 在下面结构题中的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;
};

img

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

scan_eh_tab

回忆__gxx_personality_v0函数功能

  1. 检查当前函数是否有相应的 catch 语句。

  2. 清理当前函数中的局部变量。

关于相关数据结构的解析:https://www.cnblogs.com/catch/p/3619379.html

该函数中我们最关心两个值 也即魔改处

image-20220712213429334

image-20220712213452099

landingpad指向catch处 也就是拿到landingpad我们可以知道接下来该执行哪 有助于恢复控制流

要求必须是StdObfException异常

*adjustedPtr指向异常结构体的第一个参数 也即异常类型

typeIndex的值为最后rdx的值 该值直接返回到main函数内 在main函数内用于分发异常 判断最后前往哪个catch块

才疏学浅而半途而废的想法

通过以上的解析 熟悉angr的当然想法是hook throw函数以应对路径爆炸 更改rip为landingpad的值 更改rdx为ttypeIndex的值即可

但有以下难点

关于相关的值存储在.gcc_except_table结构中 需要先解析.eh_frame结构 找到CIE 后搜索相应的 FDE,并执行DWARF 字节码,从而得到当前函数的调用函数的现场即

struct _Unwind_Context
{
  void *reg[DWARF_FRAME_REGISTERS+1];  //必要的寄存器。
    void *cfa; // canoniacl frame address, 前面提到过,基地址。
    void *ra;// 返回地址。
    void *lsda;// 该函数对应的language specific data,如果存在的话。
    struct dwarf_eh_bases bases;
  _Unwind_Word args_size;
};

根据上述结构的lsda

img

以找到callsitptrimage-20220712215034315

与action指针

image-20220712215111852

学术不精 心力交瘁 所以放弃了这种做法

跟着官方wp复现吧家人们

寻找有用的块(Relevant_blocks )

下面就是纯纯复读官方wp了

恢复控制流第一步 先要知道什么块是有用的

在普通的控制流平坦化中

image-20220710161319433

prologue即为入口块 不用找 它的直接后驱块为MainDispatcher MainDispatcher的另一个前驱块为PreDispatcher

而PreDispatcher的所有直接前驱块均为Relevant_blocks 即我们想要的有用的block

但在该题中异常处理导致MainDispatcher有多个前驱块 但是我们可以肉眼识别PreDispatcher 反正就一个块)

但是异常分发是根据throw后的rdx值进行分发 在判断rdx时会有很多相关判断块 导致PreDispatcher的直接前驱块并不一定是Relevant_blocks,我们需要寻找逻辑的前驱块

在官方wp中

一眼丁真法看出所有的sub_dispatcher均为以下格式

cmp .... ....
jz或者jnz啥的 

首先判断了是否为sub_dispatcher 然后用排除法找到了所有的Relevant_blocks

class PatchHelper:
  ## ......
  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()
    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

寻找控制流(Find the flow)

官方wp曾言到: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).

但是我是真抽象不来 寄

根据上面的异常处理部分 我们只关系landingpad和异常类型(selector)了

为什么不关心ttypeIndex呢 因为分发的异常一定会到达某个Relevant_block 中间的所有过程我们都不关心

因此我们使用gdb脚本来记录执行到哪个Relevant_block,landingpad和selector

  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)

然后使用命令gdb inflated.bak -x test.gdb > testout

输入上下左右箭头 1473啥的之后 (因为前面getc分析过了这几个会触发不同的selector

然后解析testout即可恢复部分控制流

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)

if __name__ == '__main__':
    ...
    ...
    ...
    ...
    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']

patch

接下来的patch就是二进制操作部分啦!

只有一个分支的直接patch为jmp

多个分支的用cmp esi ...,jz 即可

原来的异常相关函数如_cxa_allocate_exception __cxa_throw等全都可以nop掉了

这里贴一下我的patch相关函数

def fill_nops(addr, size):
    offset = addr - base_addr
    content[offset:offset + size] = b'\x90' * size


def fill_jmp(src, dest):
    offset = src - base_addr
    if dest != src + 5:
        content[offset] = 0xE9
        content[offset + 1:offset + 5] = (dest - src - 5).to_bytes(4, 'little', signed=True)
    else:
        fill_nops(src, 5)


def get_jx_opcode(jx_type):
    ks = Ks(KS_ARCH_X86, KS_MODE_32)
    code, count = ks.asm(f'{jx_type} 0xFFFFFFFF')
    return b''.join(map(lambda x: x.to_bytes(1, sys.byteorder), code[0:2]))


def fill_jx(src, dest, cmov_type):
    offset = src - base_addr
    content[offset:offset + 2] = get_jx_opcode(cmov_type.replace('cmov', 'j'))
    content[offset + 2:offset + 6] = (dest - src - 6).to_bytes(4, 'little', signed=True)


    
    
...
    with open(filename, 'rb') as file:
        content = bytearray(file.read())

官方脚本

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'))
$ python deflat.py inflated 0x404820
$ gdb inflated -x test.gdb --batch < testin > testout
$ python deflat.py inflated 0x404820 testout

ctf部分

累麻了 不写了!

posted @ 2022-07-12 23:09  FW_ltlly  阅读(343)  评论(0编辑  收藏  举报