日常学习unidbg笔记。感谢龙哥、小肩膀两位大咖的指引。本文只做简单记录
emulator 的操作
1 | // 获取内存操作接口 |
unidbg打印调用栈的API
1 | emulator.getUnwinder().unwind(); |
emulator.getUnwinder().unwind();
Console Debugger
1 | Debugger attach = emulator.attach(); |
监控内存读写
1 | String traceFile = "myMonitorFile"; |
监控内存读
1 | emulator.traceRead(module.base, module.base + module.size).setRedirect(traceStream); |
监控内存写
1 | emulator.traceWrite(module.base, module.base + module.size).setRedirect(traceStream); |
trace
如果代码被混淆,其中90%都是无用的代码,直接看汇编代码,分析会很困难。trace可以将程序运行后,实际用到的汇编代码输出到指定文件中,再去分析。
1 | String traceFile = "myTraceCodeFile"; |
unidbg so层添加initarray初始化函数
1 |
|
日常报错函数处理:
libencrypt.so load dependency libandroid.so failed
1 | new AndroidModule(emulator, vm).register(memory); |
注意:一定要在样本SO加载前加载它(也就是vm.loadLibrary之前),道理也很简单,系统SO肯定比用户SO加载早
[crash]A/libc: Invalid address 0x40175000 passed to free: value not allocated
此提示是free 函数释放内存的时候出现了问题。最快解决问题的方式就是替换free函数,不进行内存释放
patch free
1 | emulator.attach().addBreakPoint(dm.getModule().findSymbolByName("free").getAddress(), new BreakPointCallback() { |
[main]W/libc: pthread_create failed: clone failed: Out of memory
pthread_create 的问题。需要新版然后开启多线程解决
1 | emulator.getSyscallHandler().setEnableThreadDispatcher(true); |
context 构造
DvmObject<?> context = vm.resolveClass("android/content/Context").newObject(null);// context
或者
DvmClass context = vm.resolveClass("android/content/Context"); DvmClass ContextWrapper = vm.resolveClass("android/content/ContextWrapper", context); DvmClass Application = vm.resolveClass("android/app/Application",ContextWrapper); return Application.newObject(signature);
补环境的时候要注意
@Override public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) { switch (signature){ case "android/app/ActivityThread- >getApplication()Landroid/app/Application;":{ DvmClass context = vm.resolveClass("android/content/Context"); DvmClass Application = vm.resolveClass("android/app/Application",context); return Application.newObject(signature); } case "android/content/Context- >getContentResolver()Landroid/content/ContentResolver;":{ return vm.resolveClass("android/content/ContentResolver").newObject(signature); } } return super.callObjectMethodV(vm, dvmObject, signature, vaList); }
字符串类型如何构造,字节数组如何构造,对象数组如何构造
传入Native的JAVA参数,除了八个基本类型外(byte、char、short、int、long、float、double、boolean),都必须vm.addLocalObject添加到局部引用中去。其他的对象类型一律要手动 addLocalObject。
- 字符串
list.add(vm.addLocalObject(new StringObject(vm, "12345"))); list.add(vm.addLocalObject(new StringObject(vm, "gladlywang")));
- 字节数组
ByteArray plainText = new ByteArray(vm, "gladlywang".getBytes(StandardCharsets.UTF_8)); list.add(vm.addLocalObject(plainText));
- 对象数组
public static native Object[] main(int i,Object[] objarr);
参数1是203 参数2是一个对象数组
9b69f861-e054-4bc4-9daf-d36ae205ed3e (String)
GET /aggroup/homepage/display __xxxxx(byte数组形式); 2 (int包装类)
1 | public String main203(){ |
参数2的实例对象怎么传?填0,这是偷懒并且有风险的做法,还是建议老老实实初始化类或对象,传hashCode进去,代码如下
1 | public DvmClass cNative; |
代码是Thumb模式,调用的时候别忘了+1,Thumb的+1只在运行和Hook时需要考虑,打Patch和下断点不用
1 | Number number = module.callFunction(emulator, 0x1E7C + 1, list.toArray())[0]; |
代码patch的两种方法
- 直接修改内存指令(Patch)
1 | public void patchVerify(){ |
————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
- 通过 Unicorn Hook 代码拦截功能,在目标地址执行时修改其行为。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public void patchVerify1(){
Pointer pointer = UnidbgPointer.pointer(emulator, module.base + 0x1E86);
assert pointer != null;
byte[] code = pointer.getByteArray(0, 4);
if (!Arrays.equals(code, new byte[]{ (byte)0xFF, (byte) 0xF7, (byte) 0xEB, (byte) 0xFE })) { // BL sub_1C60
throw new IllegalStateException(Inspector.inspectString(code, "patch32 code=" + Arrays.toString(code)));
}
try (Keystone keystone = new Keystone(KeystoneArchitecture.Arm, KeystoneMode.ArmThumb)) {
KeystoneEncoded encoded = keystone.assemble("mov r0,1");
byte[] patch = encoded.getMachineCode();
if (patch.length != code.length) {
throw new IllegalStateException(Inspector.inspectString(patch, "patch32 length=" + patch.length));
}
pointer.write(0, patch, 0, patch.length);
}
};
unidbg各种hook例子
hookZz例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public void HookMDStringold(){
// 加载HookZz
IHookZz hookZz = HookZz.getInstance(emulator);
hookZz.wrap(module.base + 0x1BD0 + 1, new WrapCallback<HookZzArm32RegisterContext>() { // inline wrap导出函数
@Override
// 类似于 frida onEnter
public void preCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
// 类似于Frida args[0]
Pointer input = ctx.getPointerArg(0);
System.out.println("input:" + input.getString(0));
};
@Override
// 类似于 frida onLeave
public void postCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
Pointer result = ctx.getPointerArg(0);
System.out.println("input:" + result.getString(0));
}
});
}Inline hook例子
1
2
3
4
5
6
7
8
9
10
11
12
13public void hook_315B0(){
IHookZz hookZz = HookZz.getInstance(emulator);
hookZz.enable_arm_arm64_b_branch();
hookZz.instrument(module.base + 0x315B0 + 1, new InstrumentCallback<Arm32RegisterContext>() {
@Override
public void dbiCall(Emulator<?> emulator, Arm32RegisterContext ctx, HookEntryInfo info) { // 通过base+offset inline wrap内部函数,在IDA看到为sub_xxx那些
System.out.println("R2:"+ctx.getR2Long());
}
});
}
Unidbg自带API hook
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38public void hookByUnicorn(){
emulator.getBackend().hook_add_new(new CodeHook() {
@Override
public void hook(Backend backend, long address, int size, Object user) {
if (address == module.base + 0x9D24){
System.out.println("Hook By Unicorn");
RegisterContext ctx = emulator.getContext();
Pointer input1 = ctx.getPointerArg(0);
Pointer input2 = ctx.getPointerArg(1);
Pointer input3 = ctx.getPointerArg(2);
// getString的参数i代表index,即input[i:]
System.out.println("参数1:"+input1.getString(0));
System.out.println("参数2:"+input2.getString(0));
System.out.println("参数3:"+input3.getString(0));
buffer = ctx.getPointerArg(3);
}
if(address == (module.base + 0x9d28)){
Inspector.inspect(buffer.getByteArray(0,0x100), "Unicorn hook EncryptWallEncode");
}
}
@Override
public void onAttach(Unicorn.UnHook unHook) {
System.out.println("onAttach");
}
@Override
public void detach() {
System.out.println("detach");
}
},module.base + 0x9D24,module.base + 0x9D28,null);
}
Unidbg Console Debugger
public void HookByConsoleDebugger(){
Debugger debugger = emulator.attach();
debugger.addBreakPoint(module.base+0x9d24);
debugger.addBreakPoint(module.base+0x9d28);
}
加载so到虚拟内存
DalvikModule dm = vm.loadLibrary(new File(“unidbg-android\src\test\java\com\zuiyou\libnet_crypto.so”), true);
如果在加载so到虚拟内存的步骤中,参数二设为false(即不执行init相关函数),会出现乱码。其实其中的道理并不复杂,甚至可以说很简单——SO样本做了字符串的混淆或加密,以此来对抗分析人员,但字符串总是要解密的,不然怎么用呢?这个解密一般发生在Init array节或者JNI OnLoad中,又或者是该字符串使用前的任何一个时机。
各种补环境
- 补Context
1
2
3
4
5
6
7
8
9
10
11
12@Override
public DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
switch (signature) {
case "com/izuiyou/common/base/BaseApplication->getAppContext()Landroid/content/Context;":
return vm.resolveClass("android/content/Context").newObject(null);
case "java/util/UUID->randomUUID()Ljava/util/UUID;":
return dvmClass.newObject(UUID.randomUUID());
}
return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList);
}
————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
- 补一个空类
1
2
3
4
5
6
7
8
9
10
@Override
public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
switch (signature) {
case "android/content/Context->getClass()Ljava/lang/Class;":{
return dvmObject.getObjectType();
}
}
return super.callObjectMethodV(vm, dvmObject, signature, vaList);
};
————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
- 补具体类名
1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
switch (signature) {
case "android/content/Context->getClass()Ljava/lang/Class;":{
return dvmObject.getObjectType();
}
case "java/lang/Class->getSimpleName()Ljava/lang/String;":{
return new StringObject(vm, "AppController");
}
}
return super.callObjectMethodV(vm, dvmObject, signature, vaList);
};
————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
- 补文件路径
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
switch (signature) {
case "android/content/Context->getClass()Ljava/lang/Class;":{
return dvmObject.getObjectType();
}
case "java/lang/Class->getSimpleName()Ljava/lang/String;":{
return new StringObject(vm, "AppController");
}
case "android/content/Context->getFilesDir()Ljava/io/File;":
case "java/lang/String->getAbsolutePath()Ljava/lang/String;": {
return new StringObject(vm, "/data/user/0/cn.xiaochuankeji.tieba/files");
}
}
return super.callObjectMethodV(vm, dvmObject, signature, vaList);
};
————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
- 检测是否有调试
1
2
3
4
5
6
7
8
9
10
11
@Override
public boolean callStaticBooleanMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
switch (signature){
case "android/os/Debug->isDebuggerConnected()Z":{
return false;
}
}
throw new UnsupportedOperationException(signature);
}
————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
- 使用Unidbg的API返回PID
1
2
3
4
5
6
7
8
9
10
11
@Override
public int callStaticIntMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
switch (signature){
case "android/os/Process->myPid()I":{
return emulator.getPid();
}
}
throw new UnsupportedOperationException(signature);
}
————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
- 判断map是否为空
1
2
3
4
5
6
7
8
9
10
@Override
public boolean callBooleanMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) {
if ("java/util/Map->isEmpty()Z".equals(signature)) {
TreeMap<String, String> treeMap = (TreeMap<String, String>)dvmObject.getValue();
return treeMap.isEmpty();
}
return super.callBooleanMethod(vm, dvmObject, signature, varArg);
}
————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
- 补map.get
1
2
3
4
5
6
7
8
9
10
11
12
13@Override
public DvmObject<?> callObjectMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) {
switch (signature) {
case "java/util/Map->get(Ljava/lang/Object;)Ljava/lang/Object;":
StringObject keyobject = varArg.getObjectArg(0);
String key = keyobject.getValue();
TreeMap<String, String> treeMap = (TreeMap<String, String>)dvmObject.getValue();
String value = treeMap.get(key);
return new StringObject(vm, value);
}
return super.callObjectMethod(vm, dvmObject, signature, varArg);
}
————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
- 补SignedQuery类的init,也就是初始化一个SignedQuery对象
1
2
3
4
5
6
7
8
9
10
11
12
13@Override
public DvmObject<?> newObject(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) {
switch (signature) {
case "com/bilibili/nativelibrary/SignedQuery-><init>(Ljava/lang/String;Ljava/lang/String;)V":
StringObject stringObject1 = varArg.getObjectArg(0);
StringObject stringObject2 = varArg.getObjectArg(1);
String str1 = stringObject1.getValue();
String str2 = stringObject2.getValue();
return vm.resolveClass("com/bilibili/nativelibrary/SignedQuery").newObject(new SignedQuery(str1, str2));
}
return super.newObject(vm, dvmClass, signature, varArg);
}
因为用jadx查看SignedQuery类的代码,有俩成员以及构造函数,所以根据他的成员和构造函数,newObject一个SignedQuery类,搞一个简化版的内部类给它用
1 | public class SignedQuery { |
————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
- 补文件访问
当样本做文件访问时,Unidbg重定向到本机的某个位置,进入
src/main/java/com/github/unidbg/file/BaseFileSystem.java
在构造函数第三行加上System.out.print("virtual path:" + rootDir);
打印虚拟路径,接下来我们按照要求,在报错提示目录下新建对应文件夹,并把我们的apk复制进去,改名成报错提示需要的apk。1
2
3
4public BaseFileSystem(Emulator<T> emulator, File rootDir) {
this.emulator = emulator;
this.rootDir = rootDir;
System.out.print("virtual path:" + rootDir);
创建模拟器实例的时候,加上
setRootDir(new File("target/rootfs")
,运行代码的时候会自动创建生成target/rootfs目录
1 emulator = AndroidEmulatorBuilder.for32Bit().setRootDir(new File("target/rootfs")).setProcessName("com.xunmeng.pinduoduo").build();
除此之外,也可以通过代码的方式进行操作
我们的类实现文件重定向的接口即可,只需要三个步骤,如下:
- 第一步 实现IOResolve
1 | public class NBridge extends AbstractJni implements IOResolver { |
第二步 绑定IO重定向接口
1
2
3
4
5
6
7
8
9
10
emulator.getSyscallHandler().addIOResolver(this);
vm.setVerbose(true); // 设置是否打印Jni调用细节
DalvikModule dm = vm.loadLibrary(new File("C:\\Users\\pr0214\\Desktop\\DTA\\unidbg\\versions\\unidbg-2021-5-17\\unidbg-master\\unidbg-android\\src\\test\\java\\com\\lession7\\libmtguard.so"), true);
module = dm.getModule(); //
vm.setJni(this);
dm.callJNI_OnLoad(emulator);
}第三步
1
2
3
4
5
6
7
8
9
@Override
public FileResult resolve(Emulator emulator, String pathname, int oflags) {
if (("/data/app/com.sankuai.meituan-TEfTAIBttUmUzuVbwRK1DQ==/base.apk").equals(pathname)) {
// 填入想要重定位的文件
return FileResult.success(new SimpleFileIO(oflags, new File("unidbg-android\\src\\test\\java\\com\\lession10\\mt.apk"), pathname));
}
return null;
}
————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
- 补文件2
1
2
3
4
5
6
7
8
@Override public FileResult resolve(Emulator emulator, String pathname, int oflags) {
if (("proc/"+emulator.getPid()+"/cmdline").equals(pathname)) {
return FileResult.success(new ByteArrayFileIO(oflags, pathname, "ctrip.android.view".getBytes()));
}
return null;
}
除此之外也可以像上面一样新建一个文件,传入文件
1 | return FileResult.success(new SimpleFileIO(oflags, new File("D:\\unidbg-teach\\unidbg- android\\src\\test\\java\\com\\lession1\\cmdline"), pathname)); |
————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
- 补签名
1 | @Override |
————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
- 初始化异常类
1 | @Override public DvmObject<?> newObject(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) { |
————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
打印地址所指向的内存,其效果类似于frida中hexdump,push保存,在后面再pop取出。
1 |
|
函数调用
地址调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public void callMd5(){
List<Object> list = new ArrayList<>(10);
// arg1
String input = "gladlywang";
// malloc memory
MemoryBlock memoryBlock1 = emulator.getMemory().malloc(16, false);
// get memory pointer
UnidbgPointer input_ptr=memoryBlock1.getPointer();
// write plainText on it
input_ptr.write(input.getBytes(StandardCharsets.UTF_8));
// arg2
int input_length = input.length();
// arg3 -- buffer
MemoryBlock memoryBlock2 = emulator.getMemory().malloc(16, false);
UnidbgPointer output_buffer=memoryBlock2.getPointer();
// 填入参入
list.add(input_ptr);
list.add(input_length);
list.add(output_buffer);
// run
module.callFunction(emulator, 0x65540 + 1, list.toArray());
// print arg3
Inspector.inspect(output_buffer.getByteArray(0, 0x10), "output");
};符号调用
只有静态函数才能符号调用。动态函数或去除符号表函数是无法进行符号调用*
1 | Module module = emulator.getMemory().findModule("libtarget.so"); // 找到目标模块 |
————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
unidbg下断点
- 普通断点
1 | emulator.attach().addBreakPoint(module.base + 0x3161E); |
- 内存写入断点
1 | emulator.traceWrite(module.base + 0x3A0C0,module.base + 0x3A0C0); |
————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
unidbg traceCode
1 | emulator.traceCode(module.base, module.base + module.size); |
- traceCode保存到文件
1 | String traceFile = "unidbg-android\\src\\test\\java\\com\\lession5\\qxstrace.txt"; |
修改unidbg源码,保存关键的寄存器值信息
找到代码文件 src/main/java/com/github/unidbg/arm/AbstractARMEmulator.java
添加值显示
1 | private void printAssemble(PrintStream out, Capstone.CsInsn[] insns, long address, boolean thumb) { |
src/main/java/com/github/unidbg/arm/ARM.java
中,新建SaveRegs方法,实际上就是showregs的代码,只不过从print改成return回来而已。
1 | public static String SaveRegs(Emulator<?> emulator, Set<Integer> regs) { |
————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
开启所有的日志
1 | Logger.getLogger("com.github.unidbg.linux.ARM32SyscallHandler").setLevel(Level.DEBUG); |
所需要的头文件
1 | import org.apache.log4j.Level; |
————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
console debugger支持如下指令
1 | c: continue |
————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
非法JNI Verision的错误
千万不要先管它,非法JNI Verision的错误往往代表JNI OnLoad的总体执行情况不符合预期,它是JNIOnLoad的最终结果,我们应该在修复完其他错误后看它是否存在。
————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
如何固定PID?
修改src/main/java/com/github/unidbg/AbstractEmulator.java
中的如下位置,使PID固定,因为PID不停变动可能会影响后续分析,但这不是必须的操作。this.pid = Integer.parseInt(pid);
修改为this.pid = 23638
————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
打印so函数获取哪些系统属性,这个对函数流程来说非常重要
1 | SystemPropertyHook systemPropertyHook = new SystemPropertyHook(emulator); |