前言
上一篇中介绍了IDA Python去混淆的基本思路,本篇将以腾讯免费御安全为例进行脚本反混淆.(以x86_32指令为例)
思路
首先分析出控制块的特征,将可能是控制块的块去掉,之后连接所有真实的块.
实战
实验环境:ida7.5+雷电模拟器(64位)4
Jadx打开样本,发现加载了libshellx-super.2019.so文件
IDA加载libshellx-super.2019.so,发现JNI_OnLoad被混淆
发现是个魔改的OLLVM.
找出疑似控制块的代码
-
-
-
-
可以分析出,上面四种类型的块与真实逻辑毫无关系
去混淆脚本
对疑似控制块的块不下断点,其余块结尾下断点
#---------------------------------------------------------------------
# Debug notification hook test
#
# This script start the executable and steps through the first five
# instructions. Each instruction is disassembled after execution.
#
# Original Author: Gergely Erdelyi <gergely.erdelyi@d-dome.net>
#
# Maintained By: IDAPython Team
#
#---------------------------------------------------------------------
from idaapi import *
import keypatch
patcher=keypatch.Keypatch_Asm()
fun_offset=0x25D0 #函数地址
class MyDbgHook(DBG_Hooks):
""" Own debug hook class that implementd the callback functions """
def dbg_process_start(self, pid, tid, ea, name, base, size):
print("Process started, pid=%d tid=%d name=%s" % (pid, tid, name))
self.dbg_process_attach(pid, tid, ea, name, base, size)
def dbg_process_exit(self, pid, tid, ea, code):
print("Process exited pid=%d tid=%d ea=0x%x code=%d" % (pid, tid, ea, code))
for sub_dict in self.related_dict:
if len(self.related_dict[sub_dict])==1:
for sub_sub_dict_key in self.related_dict[sub_dict]:
if idc.print_insn_mnem(sub_dict).startswith('j'):
disasm='jmp'+' '+hex(self.related_dict[sub_dict][sub_sub_dict_key].pop())
patcher.patch_code(sub_dict,disasm,patcher.syntax,True,False)
def dbg_process_attach(self, pid, tid, ea, name, base, size):
print("Process attach pid=%d tid=%d ea=0x%x name=%s base=%x size=%x" % (pid, tid, ea, name, base, size))
self.pre_blcok=None
self.ZF_flag=None
self.related_dict=dict()
self.block_addr_dict=dict()
so_base=idaapi.get_imagebase()
self.f_blocks = idaapi.FlowChart(idaapi.get_func(so_base+fun_offset), flags=idaapi.FC_PREDS)
for block in self.f_blocks:
start=block.start_ea
end=idc.prev_head(block.end_ea)
if (idc.print_insn_mnem(start)=='cmp' and idc.print_insn_mnem(idc.next_head(start)).startswith('j')) or \
(idc.print_insn_mnem(start)=='mov' and idc.print_insn_mnem(idc.next_head(start))=='cmp' and idc.print_insn_mnem(idc.next_head(idc.next_head(start))).startswith('j')) or \
idc.print_insn_mnem(start)=='jmp' or \
idc.print_insn_mnem(start)=='nop' or \
idc.print_insn_mnem(start).startswith('cmov'):
continue
add_bpt(end,0,BPT_SOFT)
while start<block.end_ea:
if idc.print_insn_mnem(start).startswith('ret'):
add_bpt(start,0,BPT_SOFT)
break
start=idc.next_head(start)
def dbg_bpt(self, tid, ea):
print ("Break point at 0x%x pid=%d" % (ea, tid))
if not self.block_addr_dict:
so_base=idaapi.get_imagebase()
blocks=idaapi.FlowChart(idaapi.get_func(so_base+fun_offset), flags=idaapi.FC_PREDS)
for block in blocks:
start=block.start_ea
end=idc.prev_head(block.end_ea)
self.block_addr_dict[end]=start
if not self.pre_blcok==None:
if self.pre_blcok in self.related_dict:
ori_dict=self.related_dict[self.pre_blcok]
if self.ZF_flag in ori_dict:
sub_set=ori_dict[self.ZF_flag]
sub_set.add(self.block_addr_dict[ea])
else:
sub_set=set()
sub_set.add(self.block_addr_dict[ea])
ori_dict[self.ZF_flag]=sub_set
else:
# 不存在
sub_set=set()
sub_set.add(self.block_addr_dict[ea])
sub_dict={self.ZF_flag:sub_set}
self.related_dict.update({self.pre_blcok:sub_dict})
self.pre_blcok=ea
self.ZF_flag = get_reg_value("ZF")
if idc.print_insn_mnem(ea).startswith('ret'):
return 0
else:
idaapi.continue_process()
# return values:
# -1 - to display a breakpoint warning dialog
# if the process is suspended.
# 0 - to never display a breakpoint warning dialog.
# 1 - to always display a breakpoint warning dialog.
return 0
# Remove an existing debug hook
try:
if debughook:
print("Removing previous hook ...")
debughook.unhook()
except:
pass
# Install the debug hook
debughook = MyDbgHook()
debughook.hook()
debughook.steps = 0
# Stop at the entry point
ep = get_inf_attr(INF_START_IP)
request_run_to(ep)
# Step one instruction
request_step_over()
# Start debugging
run_requests()
for block in self.f_blocks:
start=block.start_ea
end=idc.prev_head(block.end_ea)
if (idc.print_insn_mnem(start)=='cmp' and idc.print_insn_mnem(idc.next_head(start)).startswith('j')) or \
(idc.print_insn_mnem(start)=='mov' and idc.print_insn_mnem(idc.next_head(start))=='cmp' and idc.print_insn_mnem(idc.next_head(idc.next_head(start))).startswith('j')) or \
idc.print_insn_mnem(start)=='jmp' or \
idc.print_insn_mnem(start)=='nop' or \
idc.print_insn_mnem(start).startswith('cmov'):
continue
add_bpt(end,0,BPT_SOFT)
while start<block.end_ea: #对retn块下断点
if idc.print_insn_mnem(start).startswith('ret'):
add_bpt(start,0,BPT_SOFT)
break
start=idc.next_head(start)
这里对所有疑似真实块的地址下断点
def dbg_process_exit(self, pid, tid, ea, code):
print("Process exited pid=%d tid=%d ea=0x%x code=%d" % (pid, tid, ea, code))
for sub_dict in self.related_dict:
if len(self.related_dict[sub_dict])==1:
for sub_sub_dict_key in self.related_dict[sub_dict]:
if idc.print_insn_mnem(sub_dict).startswith('j'):
disasm='jmp'+' '+hex(self.related_dict[sub_dict][sub_sub_dict_key].pop())
patcher.patch_code(sub_dict,disasm,patcher.syntax,True,False)
记录所有真实块的下一个真实块.
我这里用一个dict嵌套dict来记录,
大致是这样
{0x2638:{0:xxxxx,1:xxxxx}} 这里的0和1是我对ZF标志的判断,xxxxx是下一个真实块的首地址.
得到所有真实块的流程后,就可以进行跳转的修复
我这里仅仅只是对真实块中只有一个后继真实块.
且结尾为jxx的块做了修复
IDA动态调试
-
打开模拟器,将调试文件传进模拟器,执行
adb shell /data/local/tmp/android_x86_server
-
端口转发
adb forward tcp:23946 tcp:23946
-
打开ddms
-
调试模式启动app
adb shell am start -D -n com.shocker.jiagutest/.MainActivity
-
IDA先载入脚本,后附加到app上
-
jdb方式启动
jdb -connect com.sun.jdi.SocketAttach:hostname=localhost,port=8700
-
F9运行直到运行至retn,继续F9
-
退出程序
-
将Patch后的so拖进apk对应的目录中,重装app(因为patch后有些块会连在一起,所以需要再次执行脚本)
-
删除所有断点,或者重新载入IDA,执行一遍脚本.
-
运行ida-nop脚本,将没执行的块全部nop.
结果
去混淆后的结果
F5后的结果
相关示例代码见
https://github.com/PShocker/de-ollvm/