最后更新于 23-3-18
基本操作
安装与基本操作可以看肉丝的教程 1 2 还是比较详细的 ## 主动调用 native 方法
public void callCheckCode() {DvmObject<?> dvmObject = vm.resolveClass( 需要调用的方法所在类).newObject(null);
dvmObject.callJniMethodObject(emulator, 调用函数的 signature,......);
}
HOOK
32 位使用 HookZz,64 位下使用 dobby hook hook 可以进行对函数的替换 注意,R2 和 R3 才是参数,R0 是 env,R1 是 object 同时 unidbg 提供了 plt hook,名为 xhook,当然因为是 plt hook 所以只能 hook 符号表之中的函数 还有个叫 CodeHook 的,但是网上的资料比较少,好像它的功能和 hookzz 也差不了多少,所以这里就不再提及了 ## Console Debugger
先下断点和进行 attach
emulator.attach().addBreakPoint(module.findSymbolByName(funName).getAddress());
常用命令
continue(简写 c):继续执行,到下一个断点处(或运行结束)next:(简写 n),单步跟踪程序,当遇到函数调用时,也不进入此函数体;此命令同 step 的主要区别是,step 遇到用户自定义的函数,将步进到函数中去运行,而 next 则直接调用函数,不会进入到函数体内。step(简写 s):单步调试如果有函数调用,则进入函数;与命令 n 不同,n 是不进入调用的函数的
m: 读取内存,例如 mr1 可以读取 r1 寄存器,m0x40000 可以读取内存中的指定位置
不过感觉不是很熟悉 gdb 的人没太大用处,除非 ida 没法调试否则用 ida 会比较方便 ## 内存修改
要修改内存需要使用 UnidbgPointer 就可以了 首先让程序断下来,然后重写 onHit 方法
@Override
public boolean onHit(Emulator<?> emulator, long address){RegisterContext registerContext = emulator.getContext();
pointer = registerContext.getPointerArg(0);
}
接下来就可以通过 pointer 进行修改内存的操作了
环境修补
当运行报错的时候需要进行环境修补,举个例子
1. 调用 java 层的方法
public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList)
还有 callStaicObjectMethod,和很多调用情况,具体来说就是看报错的调用,然后重写同返回值和区分是否静态的函数,使用 switch 加 signature 来分别区分每个方法即可,一般来说代码可以直接复制粘贴反编译之后的 java 代码 然后之后,如果遇到某些奇怪的代码,需要进行打印参数进行调试区分。
list.get(vaList.getIntArg(<span class="hljs-number">0</span>))
通过上面的代码就能拿到第一个参数,是对象的话强制转换就可以了,暂时也没发现有什么问题 之后需要 return,一般来说通过 resolve class 就可以了
return vm.resolveClass(android/content/Context).newObject(null);
之所以一些函数看起来依赖于 Android 系统却能够被像上面那样子给 resolve 掉,是因为这是一些常用类,而且被 unidbg 所实现了,因此可以进行正常的返回。但是有些类却不是由 unidbg 实现的,因此可能不可以直接通过上面的方法进行调用 然后返回 String 的时候要使用 StringObject. pid 的话可以用 emulator.getPid() ### 2. 取 java 层的 field
同样需要区分是否静态,取得是什么类型的 field 举个例子 public DvmObject<?> getStaticObjectField(BaseVM vm, DvmClass dvmClass, String signature) 同样是通过 signature 来进行区分 ## Block Hook
常常被用于 fuction trace,可以看出函数调用链以便 ida 进行接下来的分析 其中需要注意的是 arm 和 thumb 模式的切换问题,还有如果需要 hook 代码块需要大于 20 字节,否则添加 hook 不成功(code from fenfei)
// 保存需要用 frida Hook 的 Block 的地址
public static Map<Integer, Integer> subTraceMap = new HashMap<Integer, Integer>();
// 保存命中的 Block 地址的次数,命中次数太多的就忽略掉。public static Map<Integer, Integer> calcMap = new HashMap<Integer, Integer>();
// 入参是 so 基地址,需要 Trace 代码的开始地址和结束地址
private void traceBlock(final long baseAddr, final long starAddr, final long endAddr) {emulator.getBackend().hook_add_new(new BlockHook() {
@Override
public void hookBlock(Backend backend, long address, int size, Object user) {
// 代码块需要大于 20 个字节,块太小 影响 frida 的 hook
if (size > 20) {Instruction[] insns = emulator.disassemble(address, 4, 0);
int iSize = insns[0].getSize();
int iUseAddr = 0;
if (iSize == 4) {
// ARM 模式 4 字节
iUseAddr = (int) (address - baseAddr);
} else {
// THUMB 模式 2 字节,hook 的时候 + 1,iUseAddr = (int) (address - baseAddr) + 1;
}
if (calcMap.containsKey(iUseAddr)) {
// 保存命中次数
int iValue = calcMap.get(iUseAddr);
calcMap.put(iUseAddr, iValue + 1);
// 4 次以上的调用就不显示了, 也不用 frida Trace 了
if (iValue > 3) {subTraceMap.remove(iUseAddr);
} else {System.out.println( + sub_ + Integer.toHexString((int) (address - baseAddr)) + );
}
} else {calcMap.put(iUseAddr, 1);
subTraceMap.put(iUseAddr, 1);
System.out.println(+ sub_ + Integer.toHexString((int) (address - baseAddr)) + );
}
}
}
}, starAddr, endAddr, 0);
}
IO 重定向
有些反调试会通过读取系统文件来实现反调试的效果,因为 unidbg 在无法找到需要的文件的时候会返回空值,并且产生像这样子的一条 info
INFO [com.github.unidbg.linux.ARM64SyscallHandler] (ARM64SyscallHandler:1309) - openat dirfd=-100, pathname=/proc/xxx/cmdline, oflags=0x0, mode=0
看到这个可以试试直接 hook 修改反调试代码,也可以添加一个 IOResolver 来返回真机上面获取到的内容以达到对抗风控的目的 父类需要实现 IOResolver 接口,并且实现所需要的方法
emulator.getSyscallHandler().addIOResolver(<span class="local-variable">this</span>);
之后重写 resolve 方法,按照 pathname 来解析并且返回需要的值即可 # Trace
据说比 unicorn 好用?
emulator.traceCode(start , end);
这样就能直接 trace 出执行的 code 和寄存器的变化了
String traceFile = /unidbg/unidbg-android/src/test/java/com/tracecode.txt;
PrintStream traceStream;
try {traceStream = new PrintStream(Files.newOutputStream(Paths.get(traceFile)), true);
} catch (IOException e) {throw new RuntimeException(e);
}
emulator.traceCode(module.base, module.base+module.size).setRedirect(traceStream);
上面这个更加直接一点
call native function(23/6/24 更新)
可以 call 一个需要 java 参数但是没有注册的函数
Number number = module.callFunction(
emulator,
0x2414,
vm.getJNIEnv(),
vm.addLocalObject(vm.resolveClass("com/yoloho/libcore/util/Crypt")),
0,
vm.addLocalObject(new StringObject(vm, "64e6176e45397c5...lKpHjtL0AQ==")),
85
);
int result = number.intValue();
String v = (String) vm.getObject(result).getValue();
System.out.println(v);
也可以 call 一个 c 函数
public void call_1() {
int v7 = 0;
UnidbgPointer v9 = memory.malloc(100, false).getPointer();
v9.write("64e6176e45397c5989504e76f98ecf2e63b2679euser/login15131255555A4rE0CKaCsUMlKpHjtL0AQ==".getBytes());
int v8 = 85;
UnidbgPointer v11 = memory.malloc(100, false).getPointer();
module.callFunction(
emulator,
0x1DA0,
v7,
v9,
v8,
v11
);
System.out.println(v11.getString(0));
// byte[] bArr = v11.getByteArray(0,100);
// Inspector.inspect(bArr," 结果 ");
}
你好强