[原创]记一次基于unidbg模拟执行的去除ollvm混淆-Android安全-看雪-安全社区|安全招聘|kanxue.com

参考上面的博客进行操作。这里记录操作细节

配置unidbg框架

git clone –recursivehttps://github.com/zhaoboy9692/unidbgweb.git

然后在unidbg-android中添加自定义类

文中需要添加5个system的lib,和5个lib64,因为文中的代码说了androidapi=26,所以从下面谷歌官网下载android8.0的镜像。

https://developers.google.com/android/ota?hl=zh-cn#sailfish

https://dl.google.com/dl/android/aosp/sailfish-ota-opr3.170623.013-3c966cab.zip?hl=zh-cn

下载之后里面是一个payload.bin,使用payload-dumperX64.exe对这个文件解密,用7zip打开解压出来的system.img,找到需要的这10个文件,放到lib和libs两个文件夹中。

测试jni调用记录

import capstone.Capstone;import capstone.api.Instruction;import com.github.unidbg.AndroidEmulator;import com.github.unidbg.Module;import com.github.unidbg.arm.backend.Backend;import com.github.unidbg.arm.backend.CodeHook;import com.github.unidbg.arm.backend.UnHook;import com.github.unidbg.arm.backend.Unicorn2Factory;import com.github.unidbg.linux.android.AndroidEmulatorBuilder;import com.github.unidbg.linux.android.AndroidResolver;import com.github.unidbg.linux.android.dvm.DalvikModule;import com.github.unidbg.linux.android.dvm.VM;import com.github.unidbg.memory.Memory;import keystone.Keystone;import keystone.KeystoneArchitecture;import keystone.KeystoneEncoded;import keystone.KeystoneMode;import unicorn.Arm64Const;import java.io.File;import java.io.FileInputStream;import java.io.FileOutputStream;import java.math.BigInteger;import java.util.ArrayList;import java.util.List;import java.util.Locale;import java.util.Stack;public class AntiOllvm1 {private AndroidEmulator emulator;private VM vm;private DalvikModule dm;private Module module;private long start= 0x5E388;private long end = 0x5E7A0;private static final String inName = "C:\\Users\\qd_er\\Desktop\\Android\\ollvm\\deobfuscation\\libtprt-1.so";private static final String outName = "C:\\Users\\qd_er\\Desktop\\Android\\ollvm\\deobfuscation\\libtprt-dumped.so";private static final long dispatcher = 0x5E46C;private static final long toend = 0x5E6BC;public AntiOllvm1(){//创建模拟器emulator = AndroidEmulatorBuilder.for64Bit().addBackendFactory(new Unicorn2Factory(true)).setProcessName("com.example.antiollvm").build();Memory memory = emulator.getMemory();//设置andorid系统库版本memory.setLibraryResolver(new AndroidResolver(26));//创建虚拟机vm = emulator.createDalvikVM();vm.setVerbose(true);//加载动态库vm.loadLibrary(new File("C:\\Users\\qd_er\\Desktop\\Android\\unidbg\\lib64\\libc.so"),false);vm.loadLibrary(new File("C:\\Users\\qd_er\\Desktop\\Android\\unidbg\\lib64\\libm.so"),false);vm.loadLibrary(new File("C:\\Users\\qd_er\\Desktop\\Android\\unidbg\\lib64\\libstdc++.so"),false);vm.loadLibrary(new File("C:\\Users\\qd_er\\Desktop\\Android\\unidbg\\lib64\\ld-android.so"),false);vm.loadLibrary(new File("C:\\Users\\qd_er\\Desktop\\Android\\unidbg\\lib64\\libdl.so"),false);dm = vm.loadLibrary(new File(inName), false);module = dm.getModule();}public static void main(String[] args) {AntiOllvm1 ao = new AntiOllvm1();ao.callJniOnload();}public void callJniOnload() {dm.callJNI_OnLoad(emulator);}}

成功记录所有的jni调用


模拟执行记录

import capstone.Capstone;import capstone.api.Instruction;import com.github.unidbg.AndroidEmulator;import com.github.unidbg.Module;import com.github.unidbg.arm.backend.Backend;import com.github.unidbg.arm.backend.CodeHook;import com.github.unidbg.arm.backend.UnHook;import com.github.unidbg.arm.backend.Unicorn2Factory;import com.github.unidbg.linux.android.AndroidEmulatorBuilder;import com.github.unidbg.linux.android.AndroidResolver;import com.github.unidbg.linux.android.dvm.DalvikModule;import com.github.unidbg.linux.android.dvm.VM;import com.github.unidbg.memory.Memory;import keystone.Keystone;import keystone.KeystoneArchitecture;import keystone.KeystoneEncoded;import keystone.KeystoneMode;import unicorn.Arm64Const;import java.io.File;import java.io.FileInputStream;import java.io.FileOutputStream;import java.math.BigInteger;import java.util.ArrayList;import java.util.List;import java.util.Locale;import java.util.Stack;public class AntiOllvm2 {private AndroidEmulator emulator;private VM vm;private DalvikModule dm;private Module module;private static final String inName = "C:\\Users\\qd_er\\Desktop\\Android\\ollvm\\deobfuscation\\libtprt-1.so";public AntiOllvm2(){//创建模拟器emulator = AndroidEmulatorBuilder.for64Bit().addBackendFactory(new Unicorn2Factory(true)).setProcessName("com.example.antiollvm").build();Memory memory = emulator.getMemory();//设置andorid系统库版本memory.setLibraryResolver(new AndroidResolver(26));//创建虚拟机vm = emulator.createDalvikVM();vm.setVerbose(true);//加载动态库vm.loadLibrary(new File("C:\\Users\\qd_er\\Desktop\\Android\\unidbg\\lib64\\libc.so"),false);vm.loadLibrary(new File("C:\\Users\\qd_er\\Desktop\\Android\\unidbg\\lib64\\libm.so"),false);vm.loadLibrary(new File("C:\\Users\\qd_er\\Desktop\\Android\\unidbg\\lib64\\libstdc++.so"),false);vm.loadLibrary(new File("C:\\Users\\qd_er\\Desktop\\Android\\unidbg\\lib64\\ld-android.so"),false);vm.loadLibrary(new File("C:\\Users\\qd_er\\Desktop\\Android\\unidbg\\lib64\\libdl.so"),false);dm = vm.loadLibrary(new File(inName), false);module = dm.getModule();}public static void main(String[] args) {AntiOllvm2 ao = new AntiOllvm2();ao.logIns();ao.callJniOnload();}public void callJniOnload() {dm.callJNI_OnLoad(emulator);}public void logIns(){emulator.getBackend().hook_add_new(new CodeHook() {@Overridepublic void hook(Backend backend, long address, int size, Object user){Capstone capstone = new Capstone(Capstone.CS_ARCH_ARM64,Capstone.CS_MODE_ARM);byte[] bytes = emulator.getBackend().mem_read(address, 4);Instruction[] disasm = capstone.disasm(bytes, 0);System.out.printf("%x:%s %s\n",address-module.base ,disasm[0].getMnemonic(),disasm[0].getOpStr());}@Overridepublic void onAttach(UnHook unHook) {}@Overridepublic void detach() {}}, module.base, module.base+module.size, null);}}

处理间接调用

处理间接调用导致的JNI_Onload函数无法F5的问题。

这里一直往下翻,找到RET,IDA也会标注出来这是从JNI_Onload的开始starts的。61F14这个位置就是JNI_Onload,如下面三张图所示。

这个ret也可以通过在模拟执行的时候的log的最后看到

这里我定位到ret的时候其实是看了他代码里的偏移量

private long start= 0x5E388;private long end = 0x5E7A0;

这里因为作者自己没有放出来so文件,用的下面一个评论当中的so,这里偏移的大小是不变的。大小也刚好吻合,所以确定设置方法。

这里重新设置start和end

private long start= 0x61f14;private long end = 0x6232c;

这里要把两个变量初始化,一开始忘了初始化,报的错误和应该修复的地方毫不相干。

之后运行代码,获得patch之后的so文件。可以看到整体的结构已经出来了,但是还有几个地方需要进行修复(说明刚才的策略还有问题,这里因为已经能够看到大体的结构,所以去做修复)

可以很明显看到最顶上的块存在问题。F5看一下

这几个都有问题

原因是因为在模拟执行的是后并没有走遍所有的分支,我们只是将走过的分支处理了。

接下来需要在jumpout的地方手动修改寄存器的值,来控制他的走势,让unidbg最终走完所有分支。这里下面有3个BR,上面还有3个对应异常的B。这里我们要对这里的寄存器进行控制。

比如这个BR X9的分支,在默认情况下的模拟执行,并没有跑到这个位置。(术语讲,就是代码覆盖率不高,没有覆盖到这个位置)。所以我们需要在模拟执行的时候,控制这里的W12,让他执行到没解析到的块。

这里用一种不是很优雅的方式解决,也就是逐个手动选取地址修复。

更优雅的方式应该是自动发现这种没有恢复出来的BR X9的块,然后向前推导对应没有执行到的条件,并且再进行一次程序运行,对每个BR X9块进行修复。修复过程中注意路径爆炸问题和死循环问题。路径爆炸问题可以结合实际功能复杂度设计调用深度,死循环可以设置强行patch位,当发现程序又走了一次之后,直接强行patch掉。

可以看到这一处的跳转和w12有关,我们找到给W8赋值为W12的地方。使用下面的idapython脚本进行查找。

import reimport idaapi# 定义你的正则表达式pattern = re.compile(r".*W8.*W12.*")# 定义起始和结束地址start_addr =0x61f14end_addr =0x6232cprint("vvvvvvvvvvvv")# 遍历每个地址for ea in range(start_addr, end_addr, 4):# 获取该地址的汇编指令disasm = idaapi.generate_disasm_line(ea, 0)# 使用你的正则表达式匹配指令if pattern.search(disasm):# 如果匹配,打印出地址print(hex(ea),disasm)print("^^^^^^^^^^^^^")

在给定范围内找到了唯一的赋值指令。发现这个唯一的语句就在这两个块的地址连续区。

B.LT是带符号数小于。

所以我们跟踪0x62138时的W8和0的CMP,以及对下面B.LT前面的CMP的影响。

public void do_processbr(){Instruction ins = instructions.peek().getIns();if(instructions.peek().getAddr() - module.base == 0x62138){System.out.print("0X62138: W8 =");System.out.println(getRegValue("w8",instructions.peek().getRegs()).intValue());//emulator.getBackend().reg_write(Arm64Const.UC_ARM64_REG_W8, 1);}if(instructions.peek().getAddr() - module.base == 0x62144){System.out.print("0X62144: W8 =");System.out.println(getRegValue("w8",instructions.peek().getRegs()).intValue());System.out.print("0X62144: W12=");System.out.println(getRegValue("w12",instructions.peek().getRegs()).intValue());}......}

显然只执行了一次,然后漏掉了一个分支。

这里可以看出,W8<-W11时,之后的W8<W12不成立。所以根据下一句CSEL …,EQ的这个EQ条件取反,让W8!=0,就可以走到另一个路径上。

这里看着看着才明白,读编译器源码/LLVM及其pass源码的重要。

所以我们让W8=1;

public void do_processbr(){Instruction ins = instructions.peek().getIns();if(instructions.peek().getAddr() - module.base == 0x62138){emulator.getBackend().reg_write(Arm64Const.UC_ARM64_REG_W8, 1);}.......}

可以看出已经走到了另一个分支

因为改了分支之后,他原本的Jni_OnLoad函数就是会返回-1。但是unidbg源码里面如果jni_onload返回-1,就抛异常。这个可以修改BaseVM.java第210行那里,在jni_onload返回-1的时候,让他打一条log而不是抛异常

进行修改:

可以看到已经成功patch了一个分支。之后的分支需要采用同样的办法。

在逐个对每个JUMPOUT修复时,目前只能修一个,生成一次patched文件,其实是因为这个代码,运行一次之后,只过了一次程序流程,应该可以让每一个reg_write单独工作或者协同工作,这个如果后需要写自动化工具的话都需要注意。

到这里就可以恢复出f5的完整形状了


对OLLVM结构进行恢复

考虑到编译的结果要在逻辑上和原来的代码完全一致,所以一个重要的结论是——真实的代码块一定在cmp eq 之后。(switch case 的一个case)

控制流平坦化对于顺序执行的处理比较简单,直接在前一个真实块的末尾,将索引值改为下一个真实块的索引,然后跳转到主分发器即可。

对于分支执行(判断,循环),在条件的部分会有一个条件选择指令CSEl,将索引寄存器的值根据条件结果设置为不同的索引,然后跳转到主分发器。

所以我们可以按照以下算法对控制流平坦化进行还原:

1.建立一个指令栈,每条指令执行前,保留该指令的地址,指令内容,当前所有寄存器的值。然后将当前指令的信息push进指令栈。

2.对指令进行回溯,如果栈顶指令是b.eq,则进行3(收集真实块),如果栈顶指令是直接跳转指令b,则进行5(处理分支块),否则继续执行下一条指令。

3.向上回溯指令栈,找到第一条cmp 指令,判断是否为与索引寄存器w8 比较,如果是,则进行4,否则继续执行下一条指令。

4.获取与w8比较的另一个寄存器的值,获取b.eq的目标地址,组成一个(索引,真实块)对。继续执行下一条指令。

5.判断上一条指令是否为CSEL W8。如果是,则记录一个(条件成立块索引,条件不成立块索引)对。否则,继续执行下一条指令。

同时,我们在主分发器处hook 指令,记录每次经过主分发器时的索引寄存器的值为索引值顺序。

在模拟执行收集完成信息之后,做以下处理:

1.去除索引值顺序中为条件块的索引。

2.获取索引值顺序中的第一个值,找到对应的真实块。将主分发器处patch为跳转向第一个真实块的跳转指令。

3.根据真实块结束时的新的索引值,寻找到对应的真实块,将结尾处patch为跳转到下一个真实块。

4.在CSEL指令处,根据条件成立块索引和条件不成立索引找到对应的真实块。将CSEL指令和后面的跳转向主分发器的指令patch为两条指令:

第一条为条件成立时的条件跳转,第二条为条件不成立时的直接跳转。

import capstone.Capstone;import capstone.api.Instruction;import com.github.unidbg.AndroidEmulator;import com.github.unidbg.Module;import com.github.unidbg.arm.backend.Backend;import com.github.unidbg.arm.backend.CodeHook;import com.github.unidbg.arm.backend.UnHook;import com.github.unidbg.arm.backend.Unicorn2Factory;import com.github.unidbg.linux.android.AndroidEmulatorBuilder;import com.github.unidbg.linux.android.AndroidResolver;import com.github.unidbg.linux.android.dvm.DalvikModule;import com.github.unidbg.linux.android.dvm.VM;import com.github.unidbg.memory.Memory;import keystone.Keystone;import keystone.KeystoneArchitecture;import keystone.KeystoneEncoded;import keystone.KeystoneMode;import unicorn.Arm64Const;import java.io.File;import java.io.FileInputStream;import java.io.FileOutputStream;import java.math.BigInteger;import java.util.ArrayList;import java.util.List;import java.util.Locale;import java.util.Stack;public class AntiOllvm5 {private AndroidEmulator emulator;private VM vm;private DalvikModule dm;private Module module;private long start= 0x61f14;private long end = 0x6232c;private static final String inName = "C:\\Users\\qd_er\\Desktop\\Android\\ollvm\\deobfuscation\\libtprt-dumped-5.so";private static final String outName = "C:\\Users\\qd_er\\Desktop\\Android\\ollvm\\deobfuscation\\libtprt-repaired.so";private Stack instructions;private List patchs;private static final long dispatcher = 0x61FF8;//private static final long toend = 0x5E6BC;//记录真实块private Listtbs;//记录条件块private List sbs ;//记录索引顺序private List indexOrder;public AntiOllvm5(){//创建模拟器instructions = new Stack();patchs = new ArrayList();tbs = new ArrayList();sbs = new ArrayList();indexOrder = new ArrayList();emulator = AndroidEmulatorBuilder.for64Bit().addBackendFactory(new Unicorn2Factory(true)).setProcessName("com.example.antiollvm").build();Memory memory = emulator.getMemory();//设置andorid系统库版本memory.setLibraryResolver(new AndroidResolver(26));//创建虚拟机vm = emulator.createDalvikVM();vm.setVerbose(true);//加载动态库vm.loadLibrary(new File("C:\\Users\\qd_er\\Desktop\\Android\\unidbg\\lib64\\libc.so"),false);vm.loadLibrary(new File("C:\\Users\\qd_er\\Desktop\\Android\\unidbg\\lib64\\libm.so"),false);vm.loadLibrary(new File("C:\\Users\\qd_er\\Desktop\\Android\\unidbg\\lib64\\libstdc++.so"),false);vm.loadLibrary(new File("C:\\Users\\qd_er\\Desktop\\Android\\unidbg\\lib64\\ld-android.so"),false);vm.loadLibrary(new File("C:\\Users\\qd_er\\Desktop\\Android\\unidbg\\lib64\\libdl.so"),false);dm = vm.loadLibrary(new File(inName), false);module = dm.getModule();}public static void main(String[] args) {AntiOllvm5 ao = new AntiOllvm5();ao.processFlt();ao.callJniOnload();System.out.println("vvvvvvvvvvv");for (selectBr sb : ao.sbs) {System.out.println("insaddr: 0x" + Long.toHexString(sb.getInsaddr()));System.out.println("trueindex: 0x" + Long.toHexString(sb.getTrueindex()));System.out.println("falseindex: 0x" + Long.toHexString(sb.getFalseindex()));System.out.println("cond: " + sb.getCond());System.out.println();}for (TrueBlock tb : ao.tbs) {System.out.println("index: 0x" + Long.toHexString(tb.getIndex()));System.out.println("startAddr: 0x" + Long.toHexString(tb.getStartAddr()));}System.out.println("^^^^^^^^^");ao.reorderblock();ao.patch();}public void callJniOnload() {dm.callJNI_OnLoad(emulator);}//遍历patch表,执行patch,生成新的so,使用Ketstone将汇编转为机器码。public void patch(){try {System.out.println("Begin patch...");File f = new File(inName);FileInputStream fis = new FileInputStream(f);byte[] data = new byte[(int) f.length()];fis.read(data);fis.close();for(PatchIns pi:patchs){System.out.println("procrss addr:"+Integer.toHexString((int) pi.addr)+",code:"+pi.getIns());Keystone ks = new Keystone(KeystoneArchitecture.Arm64, KeystoneMode.LittleEndian);KeystoneEncoded assemble = ks.assemble(pi.getIns());for(int i=0;i regs;public long getAddr() {return addr;}public void setAddr(long addr) {this.addr = addr;}public void setIns(Instruction ins) {this.ins = ins;}public Instruction getIns() {return ins;}public void setRegs(List regs) {this.regs = regs;}public List getRegs() {return regs;}}//patch类class PatchIns{long addr;//patch 地址String ins;//patch的指令public long getAddr() {return addr;}public void setAddr(long addr) {this.addr = addr;}public String getIns() {return ins;}public void setIns(String ins) {this.ins = ins;}}//保存指令寄存器环境public List saveRegs(Backend bk){List nb = new ArrayList();for(int i=0;i<29;i++){nb.add(bk.reg_read(i+Arm64Const.UC_ARM64_REG_X0));}nb.add(bk.reg_read(Arm64Const.UC_ARM64_REG_FP));nb.add(bk.reg_read(Arm64Const.UC_ARM64_REG_LR));return nb;}public Number getRegValue(String reg,List regsaved){if(reg.equals("xzr")){return 0;}return regsaved.get(Integer.parseInt(reg.substring(1)));}public long readInt64(Backend bk,long addr){byte[] bytes = bk.mem_read(addr, 8);long res = 0;for (int i=0;i<bytes.length;i++){res =((bytes[i]&0xffL) << (8*i)) + res;}return res;}}

这样执行一段之后会报错。

true block index 22f0693f,addr 62134true block index 6e221a17,addr 622f0true block index 44b82855,addr 62098true block index 3b21f150,addr 62268true block index 30fae711,addr 621f0true block index 83a9af56,addr 62308select block inds addr: 6213c,cond: eq . true for 6e221a17,false for 6e142ec8select block inds addr: 62300,cond: eq . true for f07b1447,false for 44b82855select block inds addr: 620fc,cond: eq . true for 3b21f150,false for 5d7b4e5aselect block inds addr: 622cc,cond: eq . true for 30fae711,false for 5ad22f2findex order:22f0693findex order:6e221a17index order:44b82855index order:3b21f150index order:30fae711index order:83a9af56not found addr for index:6e142ec8,result may be wrong!not found addr for index:f07b1447,result may be wrong!not found addr for index:5d7b4e5a,result may be wrong!not found addr for index:5ad22f2f,result may be wrong!Begin patch...procrss addr:6213c,code:beq 0x1b4procrss addr:62140,code:b 0xfff9debfkeystone.exceptions.AssembleFailedKeystoneException: Error while assembling `b 0xfff9debf` : OK (KS_ERR_OK)at keystone.Keystone.assemble(Keystone.java:101)at keystone.Keystone.assemble(Keystone.java:80)at AntiOllvm5.patch(AntiOllvm5.java:119)at AntiOllvm5.main(AntiOllvm5.java:99)

发现有一些索引值没有找到对应的块:

查看他们的索引,发现刚好是条件块中的索引。这个也好理解,因为在模拟执行的时候我们只走了条件的一个分支,没有走另一个分支,所以就没有记录下对应的真实块。这里我们手动根据b.eq和寄存器的值,添加对应的真实块:

最后这块实在不知道toend和0x5E674L是怎么算的,实在无语,写博客也不详细一点,没有源文件,没有说明。。我到这里都没法分析了。。。

 PatchIns pi = new PatchIns();pi.setAddr(dispatcher);pi.setIns("b 0x" + Integer.toHexString((int) (getIndexAddr(0x22f0693f) - dispatcher)));//获取索引值顺序中的第一个值,找到对应的真实块。将主分发器处patch为跳转向第一个真实块的跳转指令。patchs.add(pi);PatchIns pie = new PatchIns();pie.setAddr(toend);pie.setIns("b 0x" + Integer.toHexString((int) (getIndexAddr(0x83a9af56L) - toend)));patchs.add(pie);PatchIns pie1 = new PatchIns();pie1.setAddr(0x5E674L);pie1.setIns("b 0x"+Integer.toHexString((int) (getIndexAddr(0x83a9af56L) - 0x5E674L)));patchs.add(pie1);

先留着 之后弄明白了再来写。