ACTF2022-inflated复现
总结来说,利用去除控制流平坦化的步骤可以归结为三个步骤:
- 分析CFG得到序言/入口块(Prologue)、主分发器(Main dispatcher)、子分发器/无用块(Sub dispatchers)、真实块(Relevant blocks)、预分发器(PreDispatcher)和返回块(Return)
- 利用恢复真实块的前后关系,重建控制流
- 根据第二步重建的控制流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
先来了解一下异常处理流程吧
异常抛出流程
-
调用 __cxa_allocate_exception 函数,分配一个异常对象。 返回值为rax,指向该异常对象地址 和malloc类似
-
调用 __cxa_throw 函数,传入异常结构体,这个函数会将异常对象做一些初始化。
-
__cxa_throw() 调用 Itanium ABI 里的 _Unwind_RaiseException() 从而开始 unwind。
-
_Unwind_RaiseException() 对调用链上的函数进行 unwind 时,调用 personality routine(即__gxx_personality_v0)。
-
该异常如能被处理(有相应的 catch),则 personality routine 会依次对调用链上的函数进行清理。
-
_Unwind_RaiseException() 将控制权转到相应的catch代码。
-
unwind 完成,用户代码继续执行。
异常抛出后
根据 c++ 的标准,异常抛出后如果在当前函数内没有被捕捉(catch),它就要沿着函数的调用链继续往上抛,直到走完整个调用链,或者在某个函数中找到相应的 catch。
如果走完调用链都没有找到相应的 catch,那么 std::terminate() 就会被调用,这个函数默认是把程序 abort,
而如果最后找到了相应的 catch,就会进入该 catch 代码块,执行相应的操作。
程序中的 catch 那部分代码有一个专门的名字叫作:Landingpad Landingpad指catch代码块地址指针
从抛异常开始到执行 landing pad 里的代码这中间的整个过程叫作 stack unwind,这个过程包含了两个阶段:
-
从抛异常的函数开始,对调用链上的函数逐个往前查找 landing pad。
-
如果没有找到 landing pad 则把程序 abort,
-
如果找到则记下 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;
};
其中 _cxa_exception 就是头部,exception_obj 则是 "throw xxx" 中的 xxx,这两部分在内存中是连续的。
scan_eh_tab
回忆__gxx_personality_v0函数功能
-
检查当前函数是否有相应的 catch 语句。
-
清理当前函数中的局部变量。
关于相关数据结构的解析:https://www.cnblogs.com/catch/p/3619379.html
该函数中我们最关心两个值 也即魔改处
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
以找到callsitptr
与action指针
学术不精 心力交瘁 所以放弃了这种做法
跟着官方wp复现吧家人们
寻找有用的块(Relevant_blocks )
下面就是纯纯复读官方wp了
恢复控制流第一步 先要知道什么块是有用的
在普通的控制流平坦化中
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部分
累麻了 不写了!