-
-
[原创] angr符号执行对抗ollvm
-
发表于: 1小时前 11
-
ollvm一直是程序保护中的一种强有力的手段,将原本的控制流打散,用一套分发器去控制流程(网上资料很多,这里不再赘述,请了解ollvm的基础知识再来阅读此文章,比如啥是序言,啥是后继,啥是分发器),本文将着重于一些被别人忽略的细节,让你能够用一套通法根据不同的变异ollvm特征写出不同的脚本
静态的对抗手段有一些ida插件,比如d810,但是这些静态处理的方法在面对变异ollvm时就显得有些乏力,比如双循环头,汇聚块与循环头合并等问题
我们不经想既然是控制流混淆,那么动态执行一遍找到正确的执行顺序不久行了嘛?
第一步我们就是要找出所有的代码,也就是真实块,我们可以根据一套固定流程来确定寻找
先祭出老生常谈的一张图
假如说又回到了B,那么B就是循环头,我们根据BFS算法可以找出来
循环头有两个前驱,一个是序言,用与开辟栈空间,初始化变量之类的,一个是汇聚块
怎么判断哪个是汇聚块呢?汇聚块有很多前驱,而哪些前驱就是我们最关心的————真实快
ps:其实序言也是真实块之一,别忘了(
这里也是考虑了多循环头的情况,可以自己看情况改
我们以[RoarCTF 2019]polyre 为例子,附件和所有脚本我都放github上了
dfbK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6c8L8h3g2A6L8h3g2A6x3e0l9H3z5o6k6Q4x3V1k6V1k6h3k6D9j5i4c8Q4x3X3c8S2L8X3N6J5
ida运行结果是
这就到了本文最精髓的地方了,我们要找到每个块的后继
对于每一个真实块,我们遵循一套这样的法则
序言执 -> 主分发器 -> 直接跳到我们要的真实块 -> 继续执行看会到哪一个块
重点:
cmovxx通过修改寄存器的值,影响上面分发器的分发,达到不同的块
但是我们angr遇到不会cmovxx这些汇编分裂出两个分支,而是通过积累一个约束实现,所以我们要手动分裂两个状态,一个执行这条,一个不执行,就会到达两不同的块,然后我们这样储存
左边放我们zf=1的,右边放zf=0的,方便我们接下来patch
几个关键点
我们通过hook主分发器的最后一个指令,运行到直接跳到我们要的块,然后记得unhook,不然到时候执行回来又跳到那里去了,就循环了
为什么要 -6,我只能说理论上应该不用加偏移,但是实际上不加的化angr不能正确的识别指令,capstone解码出来的混乱的,这个-6是我摸索出来的,如果不行你们可以试试别的,记得把汇编输出出来对一下就行(我把它注释了)
运行结果
剩下的地方用nop填充
然后再后面把没用到的块给nop了
这里一个小细节,我们先找出所有的无用块列表,得到开始地址与结束地址,然后在进行控制流的patch,最后根据前面获取的无用块列表,nop无用块,如果先patch控制流在寻找无用块,找出来的无用块是错误的
恢复完的到代码
最后exp:
A -> B -> C -> D -> B
# ida python
def find_loop_head(start_ea):
loop_heads = set()
queue = deque() # BFS 队列
blcok = get_basic_block(start_ea) # 获取起始地址所在的基本块
queue.append((blcok,[]))
while len(queue) > 0:
cur_block, path = queue.popleft()
if cur_block.start_ea in path:
loop_heads.add(cur_block.start_ea) # 找到循环头
continue
path = path + [cur_block.start_ea] # 更新路径
queue.extend((s, path) for s in cur_block.succs()) # 将后继加入队列
all_loop_heads = list(loop_heads)
all_loop_heads.sort() # 升序排序,确保主循环头在第一个
print("[+]Find loop heads:",[hex(lh) for lh in all_loop_heads]," -- total:",len(all_loop_heads))
return all_loop_heads
def find_converge_addr(loop_head_addr):
converge_addr = 0
block = get_basic_block(loop_head_addr) # 循环头
preds = block.preds() # 获取前驱基本块
pred_list = list(preds)
if len(pred_list) == 2: # 标准 ollvm:循环头有两个前驱,一个序言块一个汇聚块
for pred in pred_list:
tmp_list = list(pred.preds())
if len(tmp_list) > 1: # 有多个前驱的块是汇聚块
converge_addr = pred.start_ea
print("[+]Find converge_addr:",hex(converge_addr))
return converge_addr
for loop_head_addr in loop_heads:
loop_head_block = get_basic_block(loop_head_addr)
converge_addr = find_converge_addr(loop_head_addr)
real_blocks = []
#找出序言
loop_head_preds = list(loop_head_block.preds())
loop_head_preds_addr = [b.start_ea for b in loop_head_preds]
if loop_head_addr != converge_addr:
loop_head_preds_addr.remove(converge_addr)
print("序言块:",[hex(x) for x in loop_head_preds_addr])
def find_ret_block(blocks):
for block in blocks:
succs = list(block.succs()) # 获取后继块
succs_list = list(succs)
end_ea = block.end_ea # end_ea 指向基本块最后一条指令的下一个地址
last_inst_ea = idc.prev_head(end_ea) # 获取基本块最后一条指令地址
mnem = idc.print_insn_mnem(last_inst_ea) # 获取指令助记符
if len(succs_list) == 0: # 没有后继块
if mnem == "retn": # 最后一条指令是 ret 指令
ori_ret_block = block
# 向上寻找更合适的 ret 块
while True:
tmp_block = block.preds()
pred_list = list(tmp_block)
if len(pred_list) == 1: # 只有一个前驱
block = pred_list[0]
if get_block_size(block) == 4: # 单指令块
continue
else:
break
else: # 多个前驱或者无前驱
break
# 处理子分发器情况
block2 = block
num = 0
i = 0
while True:
i += 1
succs_block = block2.succs()
for succ in succs_block:
child_succs = succ.succs()
succ_list = list(child_succs)
if len(succ_list) != 0:
block2 = succ
num += 1
if num > 2:
block = ori_ret_block
break
if i > 2:
break
print("[+]ret块",hex(block.start_ea))
return block.start_ea
# 这里是是考虑了可能ret块有前驱,保险起见算一起,可以不用这么多,当然这个代码你直接抄去就行,你可以先自己观察一下哦,有时候自己找ret块也行
def find_all_real_blocks(fun_ea):
blocks = idaapi.FlowChart(idaapi.get_func(fun_ea))
loop_heads = find_loop_head(fun_ea)
all_real_blocks = []
for loop_head_addr in loop_heads:
loop_head_block = get_basic_block(loop_head_addr)
converge_addr = find_converge_addr(loop_head_addr)
real_blocks = []
#找出序言
loop_head_preds = list(loop_head_block.preds())
loop_head_preds_addr = [b.start_ea for b in loop_head_preds]
if loop_head_addr != converge_addr:
loop_head_preds_addr.remove(converge_addr)
print("序言块:",[hex(x) for x in loop_head_preds_addr])
real_blocks.extend(loop_head_preds_addr)
converge_block = get_basic_block(converge_addr)
list_preds = list(converge_block.preds())
for pred in list_preds:
end_ea = pred.end_ea
last_inst_ea = idc.prev_head(end_ea)
mnem = idc.print_insn_mnem(last_inst_ea)
size = get_block_size(pred)
if size > 5: # 大于单指令块且不是跳转指令
start_ea = pred.start_ea
real_blocks.append(start_ea)
real_blocks.sort() # 排序,第一个是序言块
all_real_blocks.append(real_blocks)
print("子循环头及其子真实块", [hex(child_block_ea) for child_block_ea in real_blocks])
ret_addr = find_ret_block(blocks)
all_real_blocks.append(ret_addr)
print("all_real_blocks:",all_real_blocks)
all_real_block_list = []
for real_blocks in all_real_blocks:
if isinstance(real_blocks,list):
all_real_block_list.extend(real_blocks)
else:
all_real_block_list.append(real_blocks)
print(f"\n所有真实块获取完成 真实块数量: {len(all_real_block_list)}")
print(all_real_block_list)
[+]Find loop heads: ['0x40063f'] -- total: 1
[+]Find converge_addr: 0x4020cc
序言块: ['0x400620']
子循环头及其子真实块 ['0x400620', '0x401121', '0x401198', '0x4011de', '0x40124f', '0x40125e', '0x4012a4', '0x4012f6', '0x401305', '0x401326', '0x40136c', '0x4013b2', '0x4013cf', '0x4013ef', '0x401435', '0x401481', '0x401490', '0x4014ae', '0x4014d2', '0x4014e8', '0x4014f7', '0x401506', '0x401521', '0x401567', '0x4015b6', '0x4015c5', '0x4015d4', '0x4015ed', '0x4015fc', '0x401642', '0x401691', '0x4016a0', '0x4016e6', '0x401739', '0x401748', '0x401765', '0x4017ab', '0x4017fc', '0x40180b', '0x401830', '0x401849', '0x401861', '0x4018a7', '0x4018fa', '0x401909', '0x401926', '0x401940', '0x401960', '0x40197d', '0x40199b', '0x4019e1', '0x401a3d', '0x401a4c', '0x401a73', '0x401a8d', '0x401ad3', '0x401b25', '0x401b34', '0x401b4e', '0x401b5d', '0x401b75', '0x401bbb', '0x401c0d', '0x401c1c', '0x401c2b', '0x401c46', '0x401c69', '0x401caf', '0x401d03', '0x401d12', '0x401d2d', '0x401d45', '0x401d54', '0x401d9a', '0x401e00', '0x401e0f', '0x401e2d', '0x401e73', '0x401eb9', '0x401ed6', '0x401efa', '0x401f09', '0x401f2d', '0x401f3c', '0x401f60', '0x401f97', '0x401fa6', '0x401fb5', '0x401fcd', '0x401fe5', '0x401ff4', '0x40200c', '0x40201b', '0x402033', '0x40204d', '0x402072', '0x402096', '0x4020b3', '0x4020c2']
[+]ret块 0x401f54
all_real_blocks: [[4195872, 4198689, 4198808, 4198878, 4198991, 4199006, 4199076, 4199158, 4199173, 4199206, 4199276, 4199346, 4199375, 4199407, 4199477, 4199553, 4199568, 4199598, 4199634, 4199656, 4199671, 4199686, 4199713, 4199783, 4199862, 4199877, 4199892, 4199917, 4199932, 4200002, 4200081, 4200096, 4200166, 4200249, 4200264, 4200293, 4200363, 4200444, 4200459, 4200496, 4200521, 4200545, 4200615, 4200698, 4200713, 4200742, 4200768, 4200800, 4200829, 4200859, 4200929, 4201021, 4201036, 4201075, 4201101, 4201171, 4201253, 4201268, 4201294, 4201309, 4201333, 4201403, 4201485, 4201500, 4201515, 4201542, 4201577, 4201647, 4201731, 4201746, 4201773, 4201797, 4201812, 4201882, 4201984, 4201999, 4202029, 4202099, 4202169, 4202198, 4202234, 4202249, 4202285, 4202300, 4202336, 4202391, 4202406, 4202421, 4202445, 4202469, 4202484, 4202508, 4202523, 4202547, 4202573, 4202610, 4202646, 4202675, 4202690], 4202324]
loc_40199B:
mov eax, ds:dword_603054
mov ecx, ds:dword_603058
mov edx, eax
sub edx, 1
imul eax, edx
and eax, 1
cmp eax, 0
setz sil
cmp ecx, 0Ah
setl dil
or sil, dil
test sil, 1
mov eax, 0F37184F0h
mov ecx, 0A105D2C4h
cmovnz ecx, eax
mov [rbp+var_114], ecx
jmp loc_4020CC
{'0xaaaa':['0xbbb','0xccc']}
import logging
import angr
from tqdm import tqdm
logging.getLogger('angr').setLevel(logging.ERROR)
def capstone_decode_cmovxx(insn):
operands = insn.op_str.replace(" ", "").split(",")
dst_reg = operands[0]
src_reg = operands[1]
print(f"cmovxx解析结果: 目标寄存器:{dst_reg}, 源寄存器:{src_reg}")
return dst_reg, src_reg
def find_state_succ_cmovxx(proj, base, local_state, flag, real_blocks, real_block_addr, path):
# 仅在 find_block_succ 识别为 cmov 时调用
ins = local_state.block().capstone.insns[0]
dst_reg, src_reg = capstone_decode_cmovxx(ins)
# 逻辑修正:
# flag == True -> ZF=1 (Zero) -> cmovnz (Not Zero) 条件不满足 -> 不执行 Move -> Pass
# flag == False -> ZF=0 (Not Zero) -> cmovnz (Not Zero) 条件满足 -> 执行 Move
if not flag: # 需要执行 Move
try:
# 修正:state.regs 没有 .get() 方法,用 getattr
src_val = getattr(local_state.regs, src_reg)
setattr(local_state.regs, dst_reg, src_val)
except Exception as e:
print(f"寄存器访问错误: {e}")
# 关键修正:手动跳过这条 cmov 指令!防止 Angr 再次执行它
local_state.regs.ip += ins.size
sm = proj.factory.simgr(local_state)
while(len(sm.active)):
for active_state in sm.active:
try:
ins_offset = active_state.addr - base
if ins_offset in real_blocks:
value = path[real_block_addr]
if ins_offset not in value:
value.append(ins_offset)
return ins_offset
except:
pass
sm.step(num_inst=1)
def find_block_succ(proj, base, func_offset, state, real_block_addr, real_blocks, path):
msm = proj.factory.simgr(state) # 构造模拟器
while len(msm.active):
for active_state in msm.active:
#print(active_state.block().capstone.insns[0])
offset = active_state.addr - base
#print("当前偏移地址:", hex(offset),"寻找真实块:", hex(real_block_addr))
if offset == real_block_addr: # 找到真实块
print("找到真实块:", hex(real_block_addr))
mstate = active_state.copy() # 复制state,为后继块的获取做准备
msm2 = proj.factory.simgr(mstate)
msm2.step(num_inst=1) # 让状态进到块内的下一条指令位置,避免和外层状态混淆
while len(msm2.active):
for mactive_state in msm2.active:
#print(mactive_state.block().capstone.insns[0])
ins_offset = mactive_state.addr - base
if ins_offset in real_blocks: # 无分支块(或无条件跳转)
# 在无条件跳转中,并且有至少两条路径同时执行到真实块时,取非ret块的真实块
msm2_len = len(msm2.active)
if msm2_len > 1:
tmp_addrs = []
for s in msm2.active:
moffset = s.addr - base
tmp_value = path[real_block_addr]
if moffset in real_blocks and moffset not in tmp_value:
tmp_addrs.append(moffset)
if len(tmp_addrs) > 1:
print("当前至少有两个路径同时执行到真实块:", [hex(tmp_addr) for tmp_addr in tmp_addrs])
ret_addr = real_blocks[len(real_blocks) - 1]
if ret_addr in tmp_addrs:
tmp_addrs.remove(ret_addr)
ins_offset = tmp_addrs[0]
print("两个路径同时执行到真实块最后取得:", hex(ins_offset))
value = path[real_block_addr]
if ins_offset not in value:
value.append(ins_offset)
print(f"无条件跳转块关系:{hex(real_block_addr)}-->{hex(ins_offset)}")
return
# 可能是 cmovnz 分支指令
ins = mactive_state.block().capstone.insns[0]
if ins.mnemonic == 'cmovnz' or ins.mnemonic == 'cmovne':
print("发现 cmovnz/cmovne 指令,进行分支处理:", hex(ins_offset))
#分裂双情况
state_true = mactive_state.copy()
state_true_succ_addr = find_state_succ_cmovxx(proj, base, state_true, True, real_blocks, real_block_addr, path)
state_false = mactive_state.copy()
state_false_succ_addr = find_state_succ_cmovxx(proj, base, state_false, False, real_blocks, real_block_addr, path)
if state_true_succ_addr is None or state_false_succ_addr is None:
print("cmovnz/cmovne错误指令地址:", hex(ins_offset))
print(f"cmovnz/cmovne后继有误:{hex(real_block_addr)}-->{hex(state_true_succ_addr) if state_true_succ_addr is not None else state_true_succ_addr},"
f"{hex(state_false_succ_addr) if state_false_succ_addr is not None else state_false_succ_addr}")
return "erro"
#cmovne
print(f"cmovnz/cmovne分支跳转块关系:{hex(real_block_addr)}-->{hex(state_true_succ_addr)} zf = 1, {hex(state_false_succ_addr)} zf != 1")
#print(f"csel分支跳转块关系:{hex(real_block_addr)}-->{hex(state_true_succ_addr)},{hex(state_false_succ_addr)}")
return
#与cmovnz相反
if ins.mnemonic == 'cmovz' or ins.mnemonic == 'cmove':
print("发现 cmovz/cmove 指令,进行分支处理:", hex(ins_offset))
state_true = mactive_state.copy()
state_true_succ_addr = find_state_succ_cmovxx(proj, base, state_true, False, real_blocks, real_block_addr, path)
state_false = mactive_state.copy()
state_false_succ_addr = find_state_succ_cmovxx(proj, base, state_false, True, real_blocks, real_block_addr, path)
if state_true_succ_addr is None or state_false_succ_addr is None:
print("cmovz/cmove误指令地址:", hex(ins_offset))
print(f"cmovz/cmove后继有误:{hex(real_block_addr)}-->{hex(state_true_succ_addr) if state_true_succ_addr is not None else state_true_succ_addr},"
f"{hex(state_false_succ_addr) if state_false_succ_addr is not None else state_false_succ_addr}")
return "erro"
#cmovne
print(f"cmovz/cmove分支跳转块关系:{hex(real_block_addr)}-->{hex(state_true_succ_addr)} zf = 1, {hex(state_false_succ_addr)} zf != 1")
#print(f"csel分支跳转块关系:{hex(real_block_addr)}-->{hex(state_true_succ_addr)},{hex(state_false_succ_addr)}")
return
msm2.step(num_inst=1)
# 真实块集合中的最后一个基本块如果最后没找到后继,说明是return块,直接返回
return
msm.step(num_inst=1)
def angr_main(real_blocks,func_offset,file_path):
proj = angr.Project(file_path, auto_load_libs=False)
base = 0
func_addr = base + func_offset
init_state = proj.factory.blank_state(addr=func_addr)
init_state.options.add(angr.options.CALLLESS)
path = {addr: [] for addr in real_blocks}
ret_addr = real_blocks[len(real_blocks) - 1]
first_block = proj.factory.block(func_addr)
first_block_insns = first_block.capstone.insns
# 主序言的最后一条指令
first_block_last_ins = first_block_insns[len(first_block_insns) - 1]
print(hex(first_block_last_ins.address))
for real_block_addr in tqdm(real_blocks):
if ret_addr == real_block_addr:
continue
state = init_state.copy()
print("正在寻找:",hex(real_block_addr))
def jump_to_address(state):
#print(state.regs.pc)
state.regs.pc = base + real_block_addr - 6
print("跳转到地址:", hex(base + real_block_addr - 6))
proj.unhook(0x400675)
print(hex(real_block_addr),hex(func_offset))
if real_block_addr != func_offset:
print("序言结束")
proj.hook(0x400675, jump_to_address, first_block_last_ins.size)
ret = find_block_succ(proj, base, func_offset, state, real_block_addr, real_blocks, path)
if ret == "erro":
return
hex_dict = {
hex(key): [hex(value) for value in values]
for key, values in path.items()
}
for i in hex_dict.keys():
print(f"{i}: {hex_dict[i]}")
print(hex_dict)
return hex_dict
all_real_blocks: list[int] =[4195872, 4198689, 4198808, 4198878, 4198991, 4199006, 4199076, 4199158, 4199173, 4199206, 4199276, 4199346, 4199375, 4199407, 4199477, 4199553, 4199568, 4199598, 4199634, 4199656, 4199671, 4199686, 4199713, 4199783, 4199862, 4199877, 4199892, 4199917, 4199932, 4200002, 4200081, 4200096, 4200166, 4200249, 4200264, 4200293, 4200363, 4200444, 4200459, 4200496, 4200521, 4200545, 4200615, 4200698, 4200713, 4200742, 4200768, 4200800, 4200829, 4200859, 4200929, 4201021, 4201036, 4201075, 4201101, 4201171, 4201253, 4201268, 4201294, 4201309, 4201333, 4201403, 4201485, 4201500, 4201515, 4201542, 4201577, 4201647, 4201731, 4201746, 4201773, 4201797, 4201812, 4201882, 4201984, 4201999, 4202029, 4202099, 4202169, 4202198, 4202234, 4202249, 4202285, 4202300, 4202336, 4202391, 4202406, 4202421, 4202445, 4202469, 4202484, 4202508, 4202523, 4202547, 4202573, 4202610, 4202646, 4202675, 4202690, 4202324]
angr_main(all_real_blocks, 0x400620, "D:\\reverse\\Angr\\polyre")
def jump_to_address(state):
#print(state.regs.pc)
state.regs.pc = base + real_block_addr - 6
print("跳转到地址:", hex(base + real_block_addr - 6))
proj.unhook(0x400675)