一
分析混淆的模式
CMP X19, #0
MOV W9, #0x40 ; '@' ; X9 = 0x40
MOV W10, #0x38 ; '8' ; X10 = 0x38
ADRP X25, #off_212C70@PAGE
CSEL X9, X10, X9, EQ ; if X19 == 0 then X9 = X10
ADD X25, X25, #off_212C70@PAGEOFF ; X25 = 0x212C70
LDR X8, [X25,X9] ; X8 = qword[X25 + X9]
MOV W9, #0xFE53 ; X9 = 0xFE53
MOV W10, #0x82B4 ; X10 = 0x82B4
CSEL X9, X10, X9, EQ ; if X19 == 0 then X9 = X10
SUB X8, X8, X9 ; X8 = X8 - X9
BR X8 ; 跳转到X8
beq addr1
b addr2
二
去混淆的方式
三
写脚本
3.1 加载so
def load_elf(filename):
global img_size
global out_data
segs = []
with open(filename, 'rb') as f:
out_data = f.read()
for seg in ELFFile(f).iter_segments('PT_LOAD'):
print('file_off:%s, va: %s, size: %s' %(hex(seg['p_offset']), hex(seg['p_vaddr']), hex(seg['p_filesz'])))
segs.append((seg['p_offset'],seg['p_vaddr'], seg['p_filesz'], seg.data()))
img_size = segs[-1][1] + segs[-1][2]
byte_arr = bytearray([0] * img_size)
for seg in segs:
vaddr = seg[1]
size = seg[2]
data = seg[3]
byte_arr[vaddr: vaddr + size] = bytearray(data)
return byte_arr
3.2 初始化unicorn
def init_unicorn(file_name):
global bin_data
global uc
#装载一下so到内存
bin_data = bytes(load_elf(file_name))
uc = Uc(UC_ARCH_ARM64, UC_MODE_ARM)
uc.mem_map(0x80000000, 8 * 0x1000 * 0x1000)
uc.mem_map(0, 8 * 0x1000 * 0x1000)
# 写入so数据
uc.mem_write(0, bin_data)
#设置sp寄存器
uc.reg_write(UC_ARM64_REG_SP, 0x80000000 + 0x1000 * 0x1000 * 6)
#设置指令执行hook,执行每条指令都会走hook_code
uc.hook_add(UC_HOOK_CODE, hook_code)
#设置非法内存访问hook
uc.hook_add(UC_HOOK_MEM_UNMAPPED, hook_mem_access)
barr = uc.mem_read(0x144320, 8)
print(barr)
3.3 如何跑通一个函数的所有路径
def deobf():
# 初始化unicorn
filename = 'libxxxx.so'
patched_filename = 'out.so'
start_addr = 0x0103168
init_unicorn(filename)
q = queue.Queue()
q.put((start_addr, None)) # 入口函数是第一个节点,放到队列中去,队列中是(地址,上下文)
traced = {} # 跑过的节点
while not q.empty(): #一直循环,直到队列为空
addr, context = q.get()
traced[addr] = 1 # 跑过了
s = run(addr, context) #开始模拟执行,找br reg
if s is None:
continue
if len(s) == 2: #单分支
if s[0] not in traced:
q.put(s) #将分支节点放到队列中
else: #双分支
if s[0] not in traced:
q.put((s[0], s[2]))#将分支节点放到队列中
if s[1] not in traced:
q.put((s[1], s[2]))#将分支节点放到队列中
def run(addr, context):
global uc
global is_success
global block_flow
#开始模拟执行,函数返回说明在hook_code中执行了emu_stop
set_context(uc, context)#设置寄存器环境
uc.emu_start(addr, 0x10000)
if is_success == True:
is_success = False
return block_flow[addr] #返回分支信息和context
3.4 hook_code
保存指令栈
def hook_code(uc, address, size, user_data):
global ins_stack
global is_success
if is_success == True:
uc.emu_stop()
return
ins_help = InsHelp()
code = uc.mem_read(address, size)
ins = list(ins_help.disasm(code, address, False))[0]
print("[+] tracing instructiont0x%x:t%st%s" % (ins.address, ins.mnemonic, ins.op_str))
#记录指令和上下文环境
ins_stack.append((address, get_context(uc)))
遇到ret或者bl .__stack_chk_fail需要停止
#遇到ret直接挺停止
if ins.mnemonic.lower() == 'ret':
#uc.reg_write(UC_ARM64_REG_PC, 0)
print("[+] encountered ret, stop")
ins_stack.clear()
uc.emu_stop()
return
#遇到bl .__stack_chk_fail停止
if ins.mnemonic.lower() == 'bl' and ins.operands[0].imm == 0x237C0:
#uc.reg_write(UC_ARM64_REG_PC, 0)
print("[+] encountered bl .__stack_chk_fail, stop")
ins_stack.clear()
uc.emu_stop()
return
跳过函数调用、非栈或者so本身的内存访问、svc
#跳过bl、非栈、so本身内存访问、svc
if ins.mnemonic.lower().startswith('bl') or is_ref_ilegel_emm(uc, ins) or ins.mnemonic.lower().startswith('svc'):
print("[+] pass instruction 0x%xt%st%s" % (ins.address, ins.mnemonic, ins.op_str))
uc.reg_write(UC_ARM64_REG_PC, address + size)
return
def is_ref_ilegel_emm(mu, ins):
if ins.op_str.find('[') != -1:
if ins.op_str.find('[sp') == -1: # 不是通过sp访问内存
for op in ins.operands:
if op.type == ARM64_OP_MEM:
addr = 0
if op.value.mem.base != 0:
addr += mu.reg_read(reg_ctou(ins.reg_name(op.value.mem.base)))
if op.value.mem.index != 0:
addr += mu.reg_read(reg_ctou(ins.reg_name(op.value.mem.index)))
if op.value.mem.disp != 0:
addr += op.value.mem.disp
if 0x0 <= addr <= img_size: # 访问so中的数据,允许
return False
elif 0x80000000 <= addr < 0x80000000 + 0x1000 * 0x1000 * 8: #访问栈中的数据,允许
return False
else:
return True
else:# 是通过sp的内存访问,允许
return False
else:
return False
特征是否到达混淆块、计算分支地址
if ins.mnemonic == "br":
#判断是否到达间接跳转
is_success = True
block_base = ins_stack[0][0]
jmp_addr = ins_stack[-1][0]
ret = get_double_branch(uc, ins_stack)
if ret != None:
print('find double branch: %x => %x, %x' % (block_base, ret[0], ret[1]))
block_flow[ins_stack[0][0]] = ret
patch_double_branch(uc, jmp_addr, ret)
else:
ret = get_single_branch(uc, ins_stack)
if ret == None:
print("[+] find dest failed 0x%xt%st%s" % (ins.address, ins.mnemonic, ins.op_str))
is_success = False
else:
print('find single branch: %x => %x' % (block_base, ret[0]))
block_flow[block_base] = ret
patch_single_branch(jmp_addr, ret[0])
ins_stack.clear()
uc.emu_stop()
return
def get_double_branch(uc, ins_stack):
#变量声明略
ins_help = InsHelp()
cond = ''
for tup in ins_stack[::-1]:
addr = tup[0]
context = tup[1]
ins = list(ins_help.disasm(bin_data[addr: addr+5], addr, False))[0]
# BR X8
if ins.mnemonic.lower() == 'br' and flag_br == False:
flag_br = True
br_reg = ins.operands[0].reg
# SUB X8, X8, X9
if flag_br == True and (ins.mnemonic.lower() == 'add' or ins.mnemonic.lower() == 'sub')
and ins.operands[0].reg == br_reg and flag_sub_add == False:
if ins.operands[1].type == 1 and ins.operands[2].type == 1:
op_reg1 = ins.operands[1].reg
op_reg2 = ins.operands[2].reg
flag_sub_add = True
# CSEL X9, X10, X9, EQ
if flag_sub_add == True and ins.mnemonic.lower() == 'csel' and ins.operands[0].reg == op_reg2
and flag_csel1 == False:
cond = ins.op_str.split(', ')[-1]
regname1 = ins.reg_name(ins.operands[1].reg)
regname2 = ins.reg_name(ins.operands[2].reg)
# index1 = reg_ctou(regname1) - arm64_const.UC_ARM64_REG_X0
# index2 = reg_ctou(regname2) - arm64_const.UC_ARM64_REG_X0
reg2_value1 = 0 if regname1.lower() == 'xzr' else context[reg_ctou(regname1) - arm64_const.UC_ARM64_REG_X0]
reg2_value2 = 0 if regname2.lower() == 'xzr' else context[reg_ctou(regname2) - arm64_const.UC_ARM64_REG_X0]
flag_csel1 = True
# LDR X8, [X25,X9]
if flag_sub_add == True and ins.mnemonic.lower() == 'ldr' and ins.operands[0].reg == op_reg1
and flag_ldr == False:
pattern = r'[(.*?)]'
matches = re.findall(pattern, ins.op_str)
assert len(matches) == 1, 'not find []: %xt%st%s' % (addr, ins.mnemonic, ins.op_str)
op2_str = matches[0]
regs = op2_str.split(', ')
assert len(regs) == 2, 'ins invalid!: %xt%st%s' % (addr, ins.mnemonic, ins.op_str)
table_base = context[reg_ctou(regs[0]) - arm64_const.UC_ARM64_REG_X0]
op_reg3 = reg_ctou(regs[1])
flag_ldr = True
# CSEL X9, X10, X9, EQ
if flag_ldr == True and ins.mnemonic.lower() == 'csel' and reg_ctou(ins.reg_name(ins.operands[0].reg)) == op_reg3
and flag_csel2 == False:
regname1 = ins.reg_name(ins.operands[1].reg)
regname2 = ins.reg_name(ins.operands[2].reg)
# index1 = reg_ctou(regname1) - arm64_const.UC_ARM64_REG_X0
# index2 = reg_ctou(regname2) - arm64_const.UC_ARM64_REG_X0
reg3_value1 = 0 if regname1.lower() == 'xzr' else context[reg_ctou(regname1) - arm64_const.UC_ARM64_REG_X0]
reg3_value2 = 0 if regname2.lower() == 'xzr' else context[reg_ctou(regname2) - arm64_const.UC_ARM64_REG_X0]
flag_csel2 = True
if flag_csel1 == True and flag_csel2 == True:
# 满足条件时走的分支
barr1 = uc.mem_read(table_base + reg3_value1, 8) #直接从文件中读数据,注意内存偏移和文件偏移的转换
base1 = struct.unpack('q',barr1)
offset1 = base1[0] - reg2_value1
# 不满足条件时走的分支
barr2 = uc.mem_read(table_base + reg3_value2, 8)
base2 = struct.unpack('q',barr2)
offset2 = base2[0] - reg2_value2
return (offset1, offset2, get_context(uc), cond)
else:
return None
所以如果上述特征匹配不成功,则认为是单分支。
3.5 patch
def patch_double_branch(uc, addr, branch):
global out_data
nop_addr = find2nop(uc)
assert nop_addr is not None, 'no find 2 nop'
offset1 = branch[0]
offset2 = branch[1]
cond = branch[3]
ks = keystone.Ks(keystone.KS_ARCH_ARM64, keystone.KS_MODE_LITTLE_ENDIAN)
# 1. 把bx reg修改成跳转到nop_addr
jmp1_asm = 'b ' + hex(nop_addr)
jmp1_bin = ks.asm(jmp1_asm, addr)[0]
# 2. bcond addr1
jmp2_asm = 'b' + cond + ' ' + hex(offset1)
jmp2_bin = ks.asm(jmp2_asm, nop_addr)[0]
#3. b addr2
jmp3_asm = 'b ' + hex(offset2)
jmp3_bin = ks.asm(jmp3_asm, nop_addr + 4)[0]
#print(jmp3_bin)
#patching
print('patch code: %xt%s => %s' % (addr, list(out_data[addr: addr + 4]), jmp1_bin))
out_data = patch_bytes(out_data, bytearray(jmp1_bin), addr, 4)
print('patch code: %xt%s => %s' % (nop_addr, list(out_data[nop_addr: nop_addr + 4]), jmp2_bin))
out_data = patch_bytes(out_data, bytearray(jmp2_bin), nop_addr, 4)
print('patch code: %xt%s => %s' % (nop_addr + 4, list(out_data[nop_addr + 4: nop_addr + 8]), jmp3_bin))
out_data = patch_bytes(out_data, bytearray(jmp3_bin) , nop_addr + 4, 4)
#保存patch后的so
with open(patched_filename, 'wb') as f:
f.write(out_data)
四
效果
五
问题
参考:
看雪ID:st0ne
https://bbs.kanxue.com/user-home-887003.htm
# 往期推荐
2、在Windows平台使用VS2022的MSVC编译LLVM16
3、神挡杀神——揭开世界第一手游保护nProtect的神秘面纱
球分享
球点赞
球在看
原文始发于微信公众号(看雪学苑):使用unicorn模拟执行去除混淆