测试程序是2022 TX的一道安卓赛题,主函数以及所有的子函数都被混淆了。
去混淆思路
- 首先需要找出被混淆函数中所有的控制流基本块
- 根据真实块的特点,从所有的控制流基本块中筛选出所有真实块(实际筛选出来的真实块中存在非子分发器的虚拟块对程序的最后的逻辑是没有什么影响的)
- 利用符号执行或者模拟执行找到所有真实块能够跳转到的目标块(路径),如果基本快中有条件判断并可能会修改状态变量的指令其路径就有两条,否则只有一条。
- 根据所有真实块以及其对应的路径去patch程序,构造具体的跳转指令来恢复程序原来的控制流。
- nop所有的虚假块
去混淆具体过程
main函数去混淆前的流程图如下
利用反汇编引擎找到左右的控制流基本块
控制流基本块的划分是通过b,bge,bgt,ble,blt,bne,beq,beq.w
等跳转指令(不包含函数调用bl/blx)分割的,同时注意ret块比较特殊是以pop popeq
等指令划分的。
#saddress 块的 起始地址
#eaddress 块的 结束地址
#naddress 块的 后继地址
isNew = True
insStr = ''
for i in md.disasm(bin1[offset:end],offset):
insStr += "0x%x:%s%s\n" %(i.address, i.mnemonic, i.op_str)
if isNew:
isNew = False
block_item = {}
block_item["saddress"] = i.address
if (i.mnemonic == 'b' or \
i.mnemonic == 'beq.w' or \
i.mnemonic == 'beq' or \
i.mnemonic == 'bne' or \
i.mnemonic == 'bgt' or \
i.mnemonic == 'bge' or \
i.mnemonic == 'ble' or \
i.mnemonic == 'blt' or \
i.mnemonic == 'popeq' or \
i.mnemonic == 'pop'): #所有的跳转指令作为基本块区分
isNew = True
block_item["eaddress"] = i.address
block_item['ins'] = insStr
print(insStr)
insStr = ''
for op in i.operands: #获取指令操作数
if op.type == ARM_OP_IMM: #如果是立即数
block_item["naddress"] = op.value.imm
if op.value.imm not in processors: #计算后继块的引用计数
processors[op.value.imm] = 1
else:
processors[op.value.imm] += 1
if "naddress" not in block_item: #如果立即数不在所有的基本块中则后继为None(ret块)
block_item['naddress'] = None
list_blocks[block_item["saddress"]] = block_item #保存所有的块的信息
找到所有基本块中的真实块
我们寻找此混淆代码中真实块的特点发现,凡是有内存操作的以及有bl/blx函数调用的都是真实块,其中有一些指向主分发器的虚拟块也会包含内存操作。
这里说一下过滤的真实块中包含一些像这种指向主分发器的虚拟块是不会影响最后恢复的程序逻辑的,因为虚拟块本身执行的指令就不能影响源程序的逻辑。
例如下面一个基本块,其虽然包含内存访问,但是这条指令貌似没有什么实际意义,因为其对r0赋值后,有再次将状态变量对R0赋值,然后跳到主分发器中。所以实质上其应该是虚假块才对
我们过滤的时候会将此块作为真实块,但是在寻找路径的时候并没有影响。当有一个A真实块指向此块时,再以此块作为下一个真实块去寻找另一个真实块C,最后我们在patch时,会直接向此真实块尾部填充b--->C, 而此虚拟块中的指令又不会影响源程序逻辑,所以实际上此块相当于A--->C的一个跳板,自然对程序最后的整体逻辑没有任何影响。
过滤真实块的逻辑代码如下(其他混淆中可能情况不一样,具体情况具体分析)
利用unicorn寻找真实块的路径
此混淆代码中真实块中的状态变量条件控制指令如下。状态变量为R1,我们通过unicron模拟执行的时候主动控制状态变量值的修改,进而寻找不同路径下对应的真实块。
通过判断movwne/movtne r1指令的地址是否大于mov r1,如果大于就说明此真实块会有两条路径去指向两个基本块。对于有两条路径的真实块就需要寻找两次分别去寻找两条路径下对应的真实块,而对于没有两条路径的真实块就直接寻找一次路径就ok了。
num = 0 #指令位置
mov_num = 0 #mov指令在代码块的位置
movxx_num = 0 #movxxx指令在代码块的位置
for i in insnum:
num = num + 1
if (i.find('movweqr0') != -1) or \
(i.find('movteqr0') != -1) or \
(i.find('moveqr0') != -1) or \
(i.find('movwner0') != -1) or \
(i.find('movtner0') != -1) or \
(i.find('movner0') != -1) or \
(i.find('movwhir0') != -1) or \
(i.find('movthir0') != -1) or \
(i.find('movhir0') != -1) or \
(i.find('movwltr0') != -1) or \
(i.find('movtltr0') != -1) or \
(i.find('movltr0') != -1) or \
(i.find('movwlor0') != -1) or \
(i.find('movtlor0') != -1) or \
(i.find('movlor0') != -1):
movxx_num = num
elif(i.find('movwr0') != -1) or \
(i.find('movtr0') != -1):
mov_num = num
#只有当movxxx指令的位置大于mov指令的位置,代码块才可能包含两条路径
if (movxx_num != 0) and (movxx_num > mov_num):
ctx = get_context() #得到寄存器环境
flag_twoswitch = True
print("cmp is here")
p1 = find_path(pc,0) #寻找路径0
if p1 != None:
queue.append((p1,get_context()))#寻找到的路径0块加入到queue中
flow[pc].append(p1) #添加路径0
flag_twoswitch = True
set_context(ctx) #设置寄存器环境
p2 = find_path(pc,1) #寻找路径1
if p1 == p2: #如果路径0块 = 路径1块,则 路径1->none
p2 = None
if p2 != None: #如果是新路径块,加入到queue中,添加路径1
queue.append((p2,get_context()))
flow[pc].append(p2)
else: #如果块中不包含itt指令,则说明只有一个后继,找到唯一的路径并添加
flag_twoswitch = False
p = find_path(pc)
if p != None:
queue.append((p,get_context()))
flow[pc].append(p)
在利用unicorn寻找路径的时候只关心控制块能够寻找到的路径,其他非我们预留堆栈内存操作的指令都直接pass。
#指令如果是一些非堆栈的内存操作直接pass不执行
if ins.op_str.find('[') != -1:
if ins.op_str.find('[sp') == -1:
flag_pass = True #如果是非栈内存访问pass
for op in ins.operands:
if op.type == ARM_OP_MEM: #如果是内存操作数
addr = 0
if op.value.mem.base != 0:
addr += mu.reg_read(reg_ctou(ins.reg_name(op.value.mem.base)))
elif op.value.index != 0:
addr += mu.reg_read(reg_ctou(ins.reg_name(op.value.mem.index)))
elif op.value.disp != 0:
addr += op.value.disp
if addr >= 0x80000000 and addr < 0x80000000 + 0x10000 * 8:
flag_pass = False #如果是我们自己的栈内存操作就no pass
利用unicorn模拟执行的过程中如果碰见如果遇到movxx带条件的指令,如果当前块有两条路径需要寻找,我们就需要主动控制此指令对状态变量的修改从而索引两条路径,从而找到两条路径对应的两个真实块。
patch程序重建控制流
- 对于不包含条件判断的真实块(只有一条路径),直接在真实块尾部将构建b指令跳转到目的真实块
- 对于包含条件判断的真实块(包含两条路径),利用跳转指令(要看其在修改状态变量对应的条件判断指令来选择对应的跳转指令)跳转到第一条路径,然后利用b指令跳转到第二条路径。
- 其他虚拟块还需要nop掉
修复后的控制流程图
总结
去混淆的难点在于:
- 如何寻找到所有的真实块。
- 如果在不影响源程序逻辑的情况下构建跳转指令恢复源程序逻辑,在哪里path,如何path跳转指令也是一个难点