逆向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就更合适了!
3、逆向思路整理和总结
参考:
1、unidbg常见方法和frida对照: https://reao.io/archives/90/
2、frida长用的脚本代码:https://codeshare.frida.re/
https://github.com/iddoeldor/frida-snippets