逆向so,unidbg这种模拟器必不可少,其优势:
- ida、frida遇到了严重的反调试
- 生产环境生成sign字段(配合springboot尤其方便,有现成的框架可以直接拿来用了:https://github.com/anjia0532/unidbg-boot-server)
- 可以打印JNIEnv成员函数的调用日志,比如registerNatives、GetStringUTFChars等;
这里列举一些常见的unidbg功能供大伙逆向的时候参考;
1、hook代码:这是逆向最基本的功能之一,frida的hook代码都不陌生吧?unidbg底层用了hookZz的框架,所以hook的代码长这样的:
public void hook(){ //unidbg集成了HookZz框架 HookZz hook = HookZz.getInstance(emulator); //直接hook add函数的地址,比通过符号hook更具有“普适性” hook.replace(module.base + 0x3DC + 1, new ReplaceCallback() { @Override public HookStatus onCall(Emulator<?> emulator, HookContext context, long originFunction) { //R2和R3才是参数,R0是env,R1是object System.out.println(String.format("R2: %d, R3: %d",context.getIntArg(2),context.getIntArg(3))); //把第二个参数R3改成5 emulator.getBackend().reg_write(Unicorn.UC_ARM_REG_R3,5); return super.onCall(emulator, context, originFunction); } @Override public void postCall(Emulator<?> emulator, HookContext context) { emulator.getBackend().reg_write(Unicorn.UC_ARM_REG_R0,10); //返回值放R0,这里直接修改返回值 super.postCall(emulator, context); } }, true); }
代码整体的结构和frinda的hook是不是很类似了?onCall就是刚进入函数时候的回调(本质就是在函数入口处hook),onPost就是在函数ret前的hook回调!
2、打patch方法:hook本质也是patch,还有很多关键的跳转代码(android下的B、BL等)可能也要NOP掉才能按照我们自己的逻辑执行!最原始打patch的办法就是在IDA或010editor更改,为了更好地逆向so,unidbg也提供了打patch的方法,如下:
public void patch(){ UnidbgPointer pointer = UnidbgPointer.pointer(emulator,module.base + 0x3E8); byte[] code = new byte[]{(byte) 0xd0, 0x1a};//直接用硬编码改原so的代码:subs r0,r2,r3 pointer.write(code); } public void patch2(){ UnidbgPointer pointer = UnidbgPointer.pointer(emulator,module.base + 0x3E8); Keystone keystone = new Keystone(KeystoneArchitecture.Arm, KeystoneMode.ArmThumb); String s = "subs r0, r2, r3"; byte[] machineCode = keystone.assemble(s).getMachineCode(); //byte[] code = ; pointer.write(machineCode); }
代码很简单,可以直接在目标位置写硬编码,也可以借助keystone写汇编代码!
3、hook的时候需要知道so的基址和代码偏移,unidbg提供的方法如下:
// 加载so到虚拟内存 DalvikModule dm = vm.loadLibrary("libnative-lib.so", true); // 得到模块对象,然后根据导出的函数名找到函数入口偏移,比直接在代码写死地址灵活一些 module = dm.getModule(); int address = (int) module.findSymbolByName("funcNmae").getAddress();
4、有一点可能会超出初学入门者的想象和预期,就是unidbg也支持单步调试,叫console debug,就是在console下输入各种命令调试!操作也简单:
(1)先下个断点:当然这里也能制定特定的偏移地址
emulator.attach().addBreakPoint(module.findSymbolByName("funName").getAddress());
(2)代码运行到断点后正常情况下会停下,然后逆向人员就可以在console下输入各种命令操作了,原理和hyperpwn、gbd等类似,如下:
比如r是删除断点,b是增加断点,n是步过等!其他写方面的操作命令如下:
wr0-wr7, wfp, wip, wsp <value>: write specified register wb(address), ws(address), wi(address) <value>: write (byte, short, integer) memory of specified address, address must start with 0x wx(address) <hex>: write bytes to memory at specified address, address must start with 0x
如果命中断点后想做一个个性化的操作,但是又觉得在console上挨个敲命令麻烦,也可以写代码固化下来,比如这样:
public void ReplaceArgByConsoleDebugger(){ emulator.attach().addBreakPoint(module.findSymbolByName("funName").getAddress(), new BreakPointCallback() { @Override public boolean onHit(Emulator<?> emulator, long address) { RegisterContext context = emulator.getContext(); String fakeInput = "hello world"; int length = fakeInput.length(); // 修改r1值为新长度 emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_R1, length); MemoryBlock fakeInputBlock = emulator.getMemory().malloc(length, true); fakeInputBlock.getPointer().write(fakeInput.getBytes(StandardCharsets.UTF_8)); // 修改r0为指向新字符串的新指针 emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_R0, fakeInputBlock.getPointer().peer); Pointer buffer = context.getPointerArg(2); // OnLeave emulator.attach().addBreakPoint(context.getLRPointer().peer, new BreakPointCallback() { @Override public boolean onHit(Emulator<?> emulator, long address) { String result = buffer.getString(0); System.out.println("base64 result:"+result); return true; } }); return true; } }); }
个人觉得和hook某个地址本质上是一样的,这种方式供参考!
5、内存检索:搜索某些sign字段、字符串的时候特别重要,如下:
private Collection<Pointer> searchMemory(long start, long end, byte[] data) { List<Pointer> pointers = new ArrayList<>(); for (long i = start, m = end - data.length; i < m; i++) { byte[] oneByte = emulator.getBackend().mem_read(i, 1); if (data[0] != oneByte[0]) { continue; } if (Arrays.equals(data, emulator.getBackend().mem_read(i, data.length))) { pointers.add(UnidbgPointer.pointer(emulator, i)); i += (data.length - 1); } } return pointers; }
6、条件断点:为了避免被过多信息干扰,很多时候的断点或hook是需要设置条件的,符合了条件才需要进一步打印出来查看结果,unidbg也不例外,也是这个思路。举个例子:比如strcat、strstr、strcmp这种函数,每时每刻都在被大量的模块调用,直接hook打印会产生大量无用日志,严重影响排查。同时大量日志得打印也会严重拖慢运行速度,所以需要自己写条件判断是否需要打印日志!比如这种:
(1)只打印某个特定so调用的strcat函数:
public void hookstrcmp(){ long address = module.findSymbolByName("strcat").getAddress(); emulator.attach().addBreakPoint(address, new BreakPointCallback() { @Override public boolean onHit(Emulator<?> emulator, long address) { RegisterContext registerContext = emulator.getContext(); String arg1 = registerContext.getPointerArg(0).getString(0); String moduleName = emulator.getMemory().findModuleByAddress(registerContext.getLRPointer().peer).name; if(moduleName.equals("libxxx.so")){ System.out.println("strcat arg1:"+arg1); } return true; } }); }
(2)只打印某个特定函数中调用的strcat函数:
// 早先声明全局变量 public boolean show = false; public void hookstrcat(){ emulator.attach().addBreakPoint(module.findSymbolByName("targetfunName").getAddress(), new BreakPointCallback() { @Override public boolean onHit(Emulator<?> emulator, long address) { RegisterContext registerContext = emulator.getContext(); show = true;//进入目标函数就把show设置为true,下面才好打印日志 emulator.attach().addBreakPoint(registerContext.getLRPointer().peer, new BreakPointCallback() { @Override public boolean onHit(Emulator<?> emulator, long address) { show = false;//离开目标函数就把show设置为false,下面才知道不打印日志 return true; } }); return true; } }); emulator.attach().addBreakPoint(module.findSymbolByName("strcat").getAddress(), new BreakPointCallback() { @Override public boolean onHit(Emulator<?> emulator, long address) { RegisterContext registerContext = emulator.getContext(); String arg1 = registerContext.getPointerArg(0).getString(0); if(show){ System.out.println("strcmp arg1:"+arg1); } return true; } }); }
总结:
1、个人感受,逆向调试时还是IDA的图形化界面更方便,所以不到万不得已,我一般首选IDA调试分析!
2、一旦后期要在生产线上生成sign字段,这时再用unidbg就更合适了!
参考:
1、unidbg常见方法和frida对照: https://reao.io/archives/90/
2、frida长用的脚本代码:https://codeshare.frida.re/
https://github.com/iddoeldor/frida-snippets