1、unidbg介绍
1.1、unicorn介绍
好比是一个CPU,可以模拟执行各种指令。提供了很多编程语言接口,可以操作内存、寄存器等,但它不是一个系统,内存管理、文件管理、系统调用等都需要自己来实现。
1.2、基于unicorn开发的框架
cemu 用来学习汇编的好工具
AndroidNativeEmu Python开发
unidbg Java开发
1.3、unidbg
支持模拟JNI调用
支持模拟系统调用指令
支持ARM32和ARM64
支持Hookzz、Dobby、xHook、原生unicorn Hook等Hook方式
支持Android、iOS
好比是在CPU上搭建了一个系统,因此可以很方便地在PC端模拟运行so
学习成本较低,不需要复现so算法,补环境后直接运行即可
2、unidbg安装
2.1、项目地址
安装地址:https://github.com/zhkl0228/unidbg
2.2、导入工程
从GitHub下载项目,解压后,使用IDEA打开
2.3、工程结构
unidbg-master -> unidbg-android -> src
main 工程源码
test 测试案例
java 测试案例的源码
resources 测试案例的资源
3、unidbg入门案例
3.1、代码基本框架
TTEncrypt(boolean logging) {
this.logging = logging;
emulator = AndroidEmulatorBuilder.for32Bit()
.setProcessName("com.qidian.dldl.official")
.addBackendFactory(new Unicorn2Factory(true))
.build(); // 创建模拟器实例,要模拟32位或者64位,在这里区分
final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口
memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析 有19和23
vm = emulator.createDalvikVM(); // 创建Android虚拟机
vm.setVerbose(logging); // 设置是否打印Jni调用细节
DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/resources/example_binaries/libttEncrypt.so"), false); // 加载libttEncrypt.so到unicorn虚拟内存,加载成功以后会默认调用init_array等函数
dm.callJNI_OnLoad(emulator); // 手动执行JNI_OnLoad函数,看后面的操作是否依赖JNI_OnLoad
module = dm.getModule(); // 加载好的libttEncrypt.so对应为一个模块,可以调用下面的方法
TTEncryptUtils = vm.resolveClass("com/bytedance/frameworks/core/encrypt/TTEncryptUtils");//加载类
}
...
...
if (logging) {
emulator.attach(DebuggerType.ANDROID_SERVER_V7); // 附加IDA android_server,可输入c命令取消附加继续运行
}
byte[] data = new byte[16];
ByteArray array = TTEncryptUtils.callStaticJniMethodObject(emulator, "ttEncrypt([BI)[B", new ByteArray(vm, data), data.length); // 执行Jni方法
return array.getValue();
}
TTEncryptUtils是前面加载进来的类,现在调用类中的方法,因为返回值是ByteArray数组,所以用callStaticJniMethodObject,第一个参数是模拟器,第二个参数是方法名A+签名,第三个参数是方法A的参数,data已经是ByteArray数组,但是需要通过unidbg库的转换才行,第四个参数也是方法A的参数。
3.2、callStaticJniMethodObject
该函数比callFunction多封装了一些代码
不需要自己寻找函数地址
不需要自己包装参数
底层都是调用callJniMethod
3.3、通过符号寻找函数地址的过程
我们得探究为什么这里callStaticJniMethodObject传入的是java层的函数名,而不是native层的函数名
我们在这里下断点进行调试
再往里面跟
调用了findNativeFunction,继续跟进
这里通过nativesMap.get获得了函数指针,跟过去看看nativesMap是什么
这里是JNI函数动态注册的部分,把注册过的函数相关信息放到了nativesMap中
在这个Map中,我们的函数名+签名组成了键,而so中的函数地址为值,所以可以直接通过get取出
不过有些函数不是动态注册的,这样找不到的函数,则按静态注册规则拼接符号,然后寻找函数地址
这里也可以看出来我们之前为什么要传入TTEncrypt完整的类路径,这样才能找得到,图中就是静态注册时的函数名
3.4、对传入的参数进行包装的过程
protected static Number callJniMethod(Emulator<?> emulator, VM vm, DvmClass objectType, DvmObject<?> thisObj, String method, Object...args) {
UnidbgPointer fnPtr = objectType.findNativeFunction(emulator, method);
vm.addLocalObject(thisObj);
List<Object> list = new ArrayList<>(10);
list.add(vm.getJNIEnv());
list.add(thisObj.hashCode());
if (args != null) {
for (Object arg : args) {
if (arg instanceof Boolean) {
list.add((Boolean) arg ? VM.JNI_TRUE : VM.JNI_FALSE);
continue;
} else if(arg instanceof Hashable) {
list.add(arg.hashCode()); // dvm object
if(arg instanceof DvmObject) {
vm.addLocalObject((DvmObject<?>) arg);
}
continue;
} else if (arg instanceof DvmAwareObject ||
arg instanceof String ||
arg instanceof byte[] ||
arg instanceof short[] ||
arg instanceof int[] ||
arg instanceof float[] ||
arg instanceof double[] ||
arg instanceof Enum) {
DvmObject<?> obj = ProxyDvmObject.createObject(vm, arg);
list.add(obj.hashCode());
vm.addLocalObject(obj);
continue;
}
list.add(arg);
}
}
return Module.emulateFunction(emulator, fnPtr.peer, list.toArray());
}
这里面主要是对参数进行封装,创建了一个object的list,加入了vm.getJNIEnv(),thisObj的传入需要自身和hashcode作为索引
这部分是对传入参数的遍历和分析
3.5、unidbg入门案例
package com.xiaojianbang.ndk;
import com.alibaba.fastjson.util.IOUtils;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
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.DvmClass;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.linux.android.dvm.array.ByteArray;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.utils.Inspector;
import java.io.File;
public class NativeHelper {
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
private final DvmClass NativeHelper;
private final boolean logging;
NativeHelper(boolean logging) {
this.logging = logging;
emulator = AndroidEmulatorBuilder.for64Bit()
.setProcessName("com.xiaojianbang.app")
.addBackendFactory(new Unicorn2Factory(true))
.build(); // 创建模拟器实例,要模拟32位或者64位,在这里区分
final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口
memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析
vm = emulator.createDalvikVM(); // 创建Android虚拟机
vm.setVerbose(logging); // 设置是否打印Jni调用细节
DalvikModule dm = vm.loadLibrary(new File("/Users/whitebird/Android/unidbg/unidbg/unidbg-android/src/test/java/com/xiaojianbang/ndk/libxiaojianbang.so"), false); // 加载libttEncrypt.so到unicorn虚拟内存,加载成功以后会默认调用init_array等函数
// dm.callJNI_OnLoad(emulator); // 手动执行JNI_OnLoad函数
module = dm.getModule(); // 加载好的libttEncrypt.so对应为一个模块
NativeHelper = vm.resolveClass("com/xiaojianbang/ndk/NativeHelper");//加载类
}
void destroy() {
IOUtils.close(emulator);
if (logging) {
System.out.println("destroy");
}
}
public static void main(String[] args) throws Exception {
NativeHelper test = new NativeHelper(true);
int revtal = test.callFunc();
System.out.println("revtal:0x"+Integer.toHexString(revtal));
test.destroy();
}
int callFunc() {
int retval = NativeHelper.callStaticJniMethodInt(emulator,"add(III)I",0x100,0x200,0x300); // 执行Jni方法
return retval;
}
}
对so中的add方法进行调用
静态注册的字符串拼接,遍历模拟器中加载的so文件,找到xiaojianbang.so
3.6、unidbg入门案例—md5调用
md5也是静态注册,所以我们按照上面的代码,只需要改callFunc就行了
void callFunc() {
StringObject tt = NativeHelper.callStaticJniMethodObject(emulator,"md5(Ljava/lang/String;)Ljava/lang/String;","whitebird"); // 执行Jni方法
System.out.println("md5encode:"+tt.getValue());
}
运行后发现报错,分析错误日志
找到了md5,但是函数内部执行时,发生了错误,我们用ida看看
在callObjectMethodV发生了错误,提示让我们执行Please vm.setJni(jni)
这样就可以了
底层实现
3.7、处理so调用系统Java类
1、unidbg实现了大部分的JNI函数,对于没有实现的,需要自己来实现
2、已经实现的,类似callObjectMethodV,需要自己分析so,做有针对性的覆写
3、通过vm.setjni(this)覆写父类AbstractJni方法
4、如果某so通过JNI访问比较多的Java类,或者IDA反编译的so,逻辑不是很清楚,比如存在混淆,这时可以借助jnitrace来补
vm.setJni(new AbstractJni(){
@Override
public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
System.out.println("signature:"+signature);
System.out.println("dvmObject:"+dvmObject.getValue());
System.out.println("vaList:"+vaList);
if (signature.equals("java/lang/String->getBytes(Ljava/lang/String;)[B")){
String args=(String) dvmObject.getValue();
byte [] bytes=args.getBytes();
return new ByteArray(vm,bytes);
}
return super.callObjectMethodV(vm, dvmObject, signature, vaList);
}
});
其实就是自己去处理一些unidbg还没实现的JNI函数
3.8、处理so调用其他的so
encode函数是动态注册的,如果我们不调用JNI_OnLoad,就会找不到encode
void callFunc() {
StringObject tt = NativeHelper.callStaticJniMethodObject(emulator,"encode()Ljava/lang/String;"); // 执行Jni方法
System.out.println("encode:"+tt.getValue());
}
如果我们此时先调用JNI_Load再看看执行结果
此时又报了一个错误
[main]D/xiaojianbang: myInit是正常执行打印的
注意这个导入函数
所以这也是导致报错的原因,所以我们要手动加载bssFunc所在的so
现在再运行一下试试
1、如果被调用的函数,需要先执行JNI_OnLoad或者其他函数,那就按顺序调用
2、 如果so中调用了其他的so,只需按顺序加载所需的so即可
3 、dlopen会自己处理,因为unidbg加载了libdl.so
4 、C/C++标准库也会自己处理,因为unidbg加载了libc.so、libc++.so
3.9、将Unidbg的日志全开
src/test/resources/log4j.properties中INFO全改成DEBUG
3.10、通过符号调用函数
在findNativeFunction中先通过 nativesMap.get(method);获取函数指针,如果没得到就拼装JNI类型的函数名通过module.findSymbolByName(symbolName, false)获得symbol,然后symbol.createPointer(emulator)获取函数指针
但是symbol中有个call方法也可以直接调用函数
先以add为例
我们由于直接从Module开始,而module.findSymbolByName是传入JNI类型的函数名了,所以我们要在so中找
void callFunc() {
Symbol symbol = module.findSymbolByName("Java_com_xiaojianbang_ndk_NativeHelper_add");
Number numbers = symbol.call(emulator, vm.getJNIEnv(),vm.addLocalObject(NativeHelper),100,200,300);
int ret =numbers.intValue();
System.out.println("findSymbolByName:"+ret);
}
先通过函数名找到symbol,然后调用symbol.call,第一个填入emulator,后面就是对应Java_com_xiaojianbang_ndk_NativeHelper_add的参数,第一个是JNIEnv,第二个是Jclass,但是要用addLocalObject将dvmclass转成jclass,后面三个就是真正要想加的值。返回值放到了numbers里,通过intValue拿到
我们再试一下_Z7_strcatP7_JNIEnvP7_jclass
void callFunc() {
Symbol symbol = module.findSymbolByName("_Z7_strcatP7_JNIEnvP7_jclass");
Number numbers = symbol.call(emulator, vm.getJNIEnv(),vm.addLocalObject(NativeHelper));
int ret =numbers.intValue();
System.out.println("_Z7_strcatP7_JNIEnvP7_jclass:"+ vm.getObject(ret).getValue());//相当于vm.addLocalObject的反过程
}
这次numbers.intValue()的返回值不是直接得到结果了,而是一个哈希值,然后通过vm.getObject,传入哈希值,得到对象,最后调用getValue获得结果。因此直接获得结果只适用于基本数据类型。
3.11、module.callFunction
可以看到module.callFunction其实就是帮我们完成了上面的操作,通过findSymbolByName找符号,然后利用
symbol.call调用
void callFunc() {
Symbol symbol = module.findSymbolByName("_Z7_strcatP7_JNIEnvP7_jclass");
Number numbers = symbol.call(emulator, vm.getJNIEnv(),vm.addLocalObject(NativeHelper));
int ret1 =numbers.intValue();
System.out.println("_Z7_strcatP7_JNIEnvP7_jclass1:"+ vm.getObject(ret1).getValue());
Number number = module.callFunction(emulator, "_Z7_strcatP7_JNIEnvP7_jclass", vm.getJNIEnv(), vm.addLocalObject(NativeHelper));
int ret2 =number.intValue();
System.out.println("_Z7_strcatP7_JNIEnvP7_jclass2:"+ vm.getObject(ret2).getValue());
}
callFunction还有个通过偏移调用的方法,只需传入偏移地址,unidbg会自动加上so的基址
Number number1 = module.callFunction(emulator, 0x1B4C, vm.getJNIEnv(), vm.addLocalObject(NativeHelper));
int ret3 =number1.intValue();
System.out.println("_Z7_strcatP7_JNIEnvP7_jclass3:"+ vm.getObject(ret3).getValue());
//MD5Init(MD5_CTX *)
UnidbgPointer MD5Ctxpointer = emulator.getMemory().malloc(200, false).getPointer();
module.callFunction(emulator,0x2230,MD5Ctxpointer);
//MD5Update(MD5_CTX *, unsigned char *, unsigned int)
UnidbgPointer plainText = emulator.getMemory().malloc(200,false).getPointer();
byte[] buffer="whitebird_unidbg".getBytes();
plainText.write(buffer);
module.callFunction(emulator,0x22A0,MD5Ctxpointer,plainText,buffer.length);
//MD5Final(MD5_CTX *, unsigned char *)
UnidbgPointer cipherText = emulator.getMemory().malloc(200,false).getPointer();
module.callFunction(emulator,0x3A78,MD5Ctxpointer,cipherText);
byte[] byteArray=cipherText.getByteArray(0,16);
Inspector.inspect(byteArray,"MD5Result");
上面代码是调用MD5算法
C语言类型的传递,通过操作内存处理
emulator.getMemory().malloc(200, false).getPointer()
memory下属有很多操作内存的方法
pointer下属有很多操作内存的方法
MD5_CTX不需要特别定义,只需给它足够的内存即可,结构体实际上就是一段连续的内存,结构体中存在内存对齐
4、unidbg中的hook
4.1、简介
- unidbg支持dobby、hookzz、whale、xhook
hookzz是dobby的前身,hookzz对32位支持较好,dobby对64位支持较好
unidbg支持Unicorn自带的Hook(指令级Hook,块级Hook,内存读写Hook,异常Hook)以及unidbg封装后的console debugger
原生unicorn hook不容易被检测,console debugger可下多个断点,用于快速验证
unidbg没法处理子线程中的操作,Hook相应位置,把子线程计算结果赋值给寄存器
4.2、hookzz
可以通过符号Hook,也可以通过地址Hook
可以发现其实两种wrap的hook最后走的都是同一个流程
void callFunc() {
IHookZz hookZz = HookZz.getInstance(emulator); // 加载HookZz,支持inline hook,文档看https://github.com/jmpews/HookZz
hookZz.enable_arm_arm64_b_branch(); // 测试enable_arm_arm64_b_branch,可有可无
hookZz.wrap(module.findSymbolByName("_Z9MD5UpdateP7MD5_CTXPhj"), new WrapCallback<RegisterContext>() {
@Override
public void preCall(Emulator<?> emulator, RegisterContext ctx, HookEntryInfo info) {
md5_ctx = ctx.getPointerArg(0);
Pointer plainText = ctx.getPointerArg(1);
int length = ctx.getIntArg(2);
Inspector.inspect(md5_ctx.getByteArray(0,64),"preCall md5_ctx");
Inspector.inspect(plainText.getByteArray(0,length),"plainText");
}
@Override
public void postCall(Emulator<?> emulator, RegisterContext ctx, HookEntryInfo info) {
Inspector.inspect(md5_ctx.getByteArray(0,64),"postCall md5_ctx");
}
});
hookZz.disable_arm_arm64_b_branch();
//MD5Init(MD5_CTX *)
UnidbgPointer MD5Ctxpointer = emulator.getMemory().malloc(200, false).getPointer();
module.callFunction(emulator,0x2230,MD5Ctxpointer);
//MD5Update(MD5_CTX *, unsigned char *, unsigned int)
UnidbgPointer plainText = emulator.getMemory().malloc(200,false).getPointer();
byte[] buffer="whitebird_unidbg".getBytes();
plainText.write(buffer);
module.callFunction(emulator,0x22A0,MD5Ctxpointer,plainText,buffer.length);
//MD5Final(MD5_CTX *, unsigned char *)
UnidbgPointer cipherText = emulator.getMemory().malloc(200,false).getPointer();
module.callFunction(emulator,0x3A78,MD5Ctxpointer,cipherText);
byte[] byteArray=cipherText.getByteArray(0,16);
Inspector.inspect(byteArray,"MD5Result");
}
4.3、hookzz.instrument
也是有两种方式,可以通过符号名或者函数地址进行hook
我们以add为例,去hook寄存器中的值
IHookZz hookZz = HookZz.getInstance(emulator);
hookZz.instrument(module.base + 0x1AEC, new InstrumentCallback<Arm64RegisterContext>() {
@Override
public void dbiCall(Emulator<?> emulator, Arm64RegisterContext ctx, HookEntryInfo info) {
System.out.println("w8=0x"+Integer.toHexString(ctx.getXInt(8))+" w9=0x"+Integer.toHexString(ctx.getXInt(9)));
}
});
Symbol symbol = module.findSymbolByName("Java_com_xiaojianbang_ndk_NativeHelper_add");
Number numbers = symbol.call(emulator, vm.getJNIEnv(),vm.addLocalObject(NativeHelper),0x100,0x200,0x300);
int ret =numbers.intValue();
System.out.println("findSymbolByName:"+Integer.toHexString(ret));
}
注意只有Arm64RegisterContext里有getXInt,里面的序号就是寄存器序号
4.4、参数的获取
Symbol symbol = module.findSymbolByName("_Z12jstring2cstrP7_JNIEnvP8_jstring");
Number number = symbol.call(emulator, vm.getJNIEnv(), vm.addLocalObject(new StringObject(vm, "whitebird 123")));
long cStringAddr = number.intValue();
byte[] bytes = emulator.getMemory().pointer(cStringAddr).getByteArray(0, 16);
Inspector.inspect(bytes, "cStringAddr:");
通过hook获取参数
IHookZz hookZz = HookZz.getInstance(emulator);
hookZz.wrap(module.findSymbolByName("_Z12jstring2cstrP7_JNIEnvP8_jstring"), new WrapCallback<RegisterContext>() {
@Override
public void preCall(Emulator<?> emulator, RegisterContext ctx, HookEntryInfo info) {
int intArg=ctx.getIntArg(1);//得到第二个参数,是hash值,按照之前的getObject取到对象
StringObject stringObject=vm.getObject(intArg);
System.out.println("preCall:"+stringObject.getValue());
}
@Override
public void postCall(Emulator<?> emulator, RegisterContext ctx, HookEntryInfo info) {
byte[] bytes = ctx.getPointerArg(0).getByteArray(0,16);//其实这里getPointerArg就是访问0号寄存器,猜测可能0号寄存器类似x86中的eax寄存器,用于存放函数调用完的返回结果
Inspector.inspect(bytes,"postCall:");
}
});
IHookZz hookZz = HookZz.getInstance(emulator);
hookZz.wrap(module.findSymbolByName("_Z12jstring2cstrP7_JNIEnvP8_jstring"), new WrapCallback<HookZzArm64RegisterContext>() {
@Override
public void preCall(Emulator<?> emulator, HookZzArm64RegisterContext ctx, HookEntryInfo info) {
int intArg=ctx.getIntArg(1);//得到第二个参数
StringObject stringObject=vm.getObject(intArg);
System.out.println("preCall:"+stringObject.getValue());
}
@Override
public void postCall(Emulator<?> emulator, HookZzArm64RegisterContext ctx, HookEntryInfo info) {
String str = ctx.getPointerArg(0).getString(0);
System.out.println("getString:"+str);
int hashcode=vm.addGlobalObject(new StringObject(vm,"this is set_Ret_Result"));
ctx.setXLong(0,hashcode);
}
});
Symbol symbol = module.findSymbolByName("_Z12jstring2cstrP7_JNIEnvP8_jstring");
Number number = symbol.call(emulator, vm.getJNIEnv(), vm.addLocalObject(new StringObject(vm, "whitebird 123")));
int hashcode=number.intValue();
StringObject stringObject=vm.getObject(hashcode);
System.out.println("this is set_Ret_Result:"+stringObject.getValue());
}
通过HookZzArm64RegisterContext下面的setXLong可以设置寄存器的值,这里对返回值设置成了jString的hashcode,最后通过getObject得到StringObject,调用getValue进行打印
4.5、hookZz.replace
可以用来直接替换要hook的函数
IHookZz hookZz = HookZz.getInstance(emulator);
hookZz.replace(module.findSymbolByName("Java_com_xiaojianbang_ndk_NativeHelper_add"), new ReplaceCallback() {
@Override
public HookStatus onCall(Emulator<?> emulator, HookContext context, long originFunction) {
System.out.println("whitebird hook this function");
//return HookStatus.RET(emulator,originFunction);
return super.onCall(emulator, context, originFunction);
}
});
Symbol symbol = module.findSymbolByName("Java_com_xiaojianbang_ndk_NativeHelper_add");
Number number = symbol.call(emulator,vm.getJNIEnv(),vm.addLocalObject(NativeHelper),0x100,0x200,0x300);
int result =number.intValue();
System.out.println("add result :"+Integer.toHexString(result));
还可以通过return HookStatus.LR(emulator,0x100);修改返回值
4.6、原生UnicornHook
emulator.getBackend().hook_add_new(new CodeHook() {
@Override
public void hook(Backend backend, long address, int size, Object user) {
RegisterContext context=emulator.getContext();
if (address==module.base+0x1FF4){
Pointer MD5_CTX=context.getPointerArg(0);
Inspector.inspect(MD5_CTX.getByteArray(0,32),"MD5_CTX");
Pointer plainText=context.getPointerArg(1);
int length=context.getIntArg(2);
Inspector.inspect(plainText.getByteArray(0,length),"plainText");
}else if (address==module.base+0x2004){
Pointer ciperText=context.getPointerArg(1);
Inspector.inspect(ciperText.getByteArray(0,16),"ciperText");
}
}
@Override
public void onAttach(UnHook unHook) {
}
@Override
public void detach() {
}
},module.base+0x01FF4,module.base+0x2004,null);
Symbol symbol=module.findSymbolByName("Java_com_xiaojianbang_ndk_NativeHelper_md5");
Number number=symbol.call(emulator,vm.getJNIEnv(),vm.addLocalObject(NativeHelper),vm.addLocalObject(new StringObject(vm,"whitebird_getBackend")));
通过emulator.getBackend().hook_add_new进行hook
- 基于原生Unicorn API进行Hook时,不需要考虑是否 + 1,会自己转换
- Unicorn原生的Hook功能强大,而且不容易被检测
- Unicorn原生API进行inline hook
4.6、打印函数调用栈
emulator.getUnwinder().unwind()
4.7、unidbg动态调试
1、基于unicorn的console debugger同样不用管地址是否 + 1,会自己转换
2、附加下断点
Debugger debugger = emulator.attach();
debugger.addBreakPoint(module.base + 0x1AF4);
debugger.addBreakPoint(module.base + 0x1AEC);
3、emulator.attach支持几种调试方式,默认是console debugger
4、类似IDA动态调试,断点触发后,会显示寄存器信息,汇编指令
5、可以通过输入命令进行打印内存、写寄存器、跳到下一个断点、打印函数栈等操作
6、回车两下或者随便输错一个指令,就会打印出命令的帮助信息
b用于下断点
blr、b0x40228000
m用于读内存
mr0、mx0、m0x40228000、msp
bt用于查看函数栈
c 跳到下一个断点
s 单步调试
当断下来的时候,会打印当前寄存器的值和指令
回车两次后弹出指令介绍
后面都有注释,这里就不展开说了
查看函数栈
4.8、监控内存读写
1、通过监控内存读写,可以从算法计算的结果,一步步定位到操作内存的关键函数
2、将信息输出到文件
String traceFile = "yourpath";
PrintStream traceStream = new PrintStream(new FileOutputStream(traceFile), true);
3、 监控内存读写
emulator.traceRead(module.base, module.base + module.size).setRedirect(traceStream);
emulator.traceWrite(module.base, module.base + module.size).setRedirect(traceStream);
4、每条读写,unidbg提供这些信息
写的位置、写了几个字节、写入什么值、发生写操作的地址、lr的值(程序的返回地址)
5、traceRead/traceWrite,也可以添加监听回调,打印调用栈
emulator.traceWrite(0x402e4009, 0x402e4052, new TraceWriteListener() {
@Override
public boolean onWrite(Emulator<?> emulator, long address, int size, long value) {
emulator.getUnwinder().unwind();
return false;
}
});
代码测试:
String traceFile = "outputlog.txt";
PrintStream traceStream = new PrintStream(new FileOutputStream(traceFile), true);
emulator.traceRead(module.base, module.base + module.size).setRedirect(traceStream);
emulator.traceWrite(module.base, module.base + module.size).setRedirect(traceStream);
//地址是从module.base到module.base+module.size,说明监控了整个so文件
测试结果:
以第一条为例,读的内存地址是0x40005f60,去ida看一下偏移0x5F60的地方
执行读的地址为0x1570,其实就是调用0x5F60函数的地址
返回值是0x1E34,也就是_Z6myInitv调用完的下一行指令地址
4.9、unidbg—trace
1、代码写法
String traceFile = "yourpath";
PrintStream traceStream = new PrintStream(new FileOutputStream(traceFile), true);
emulator.traceCode(module.base, module.base + module.size).setRedirect(traceStream);
我们以add方法为例
再看一下ida里面的汇编指令
2、源码修改
由于未修改的trace打印的东西比较少,如果我们想要打印每一条指令执行时的寄存器值,就需要对源码进行修改。
在项目中找到类AbstractARM64Emulator
这里就是控制打印的地方,我们在这修改
通过ARM下面的方法进行打印寄存器值,showRegs64和showRegs都需要两个参数,第一个参数很简单,但是第二个我们暂时还不清楚怎么传,先看看showThumbRegs
showThumbRegs其实是已经被封装好了,我们跟进去看看ARM.THUMB_REGS,上下翻找可以发现
我们模仿去实现一下
builder.append(ARM.showRegs64(),ARM64_REGS);
由于ARM64_REGS是私有的,所以会报错,因此我们再模仿showThumbRegs实现下
public static String showARM64ExportRegs(Emulator<?> emulator){
return showRegs64(emulator,ARM.ARM64_REGS);
}
由于我们直接在append中调用的showARM64ExportRegs,所以需要返回String类型,而且showRegs64返回的是void,所以也要进行修改。
并且在最后返回String类型
现在执行一下看看
已经成功打印了,但是很乱,而且有很多寄存器的值都没有变化,所以我们还需要进一步修改。
首先复制一份新的ARM64_REGS,防止修改源码后,别的方法再调用时会有影响,由于很多寄存器用不到,我们可以直接注释掉
同理复制一份showRegs64
要修改的地方如下:
创建一个HashMap用来存储寄存器的状态
魔改的部分如下,现在同理要把x0-x10都加入判断语句,其他的寄存器由于被注释了,因此不会被调用,所以我们就不用修改了。
现在已经成功实现了
这里的结果都是上一条指令执行完后产生的。
4.10、处理so调用自写Java类
将自写的Java类,放到unidbg工程中
包名最好与原包名一致,避免歧义
类中有用到android相关类,需要用Java去实现
代码不需要完全一致,只需函数处理结果符合预期即可
访问修饰符的问题
JNI调用Java函数不需要理会访问修饰符
unidbg用Java开发,在调用函数时需要注意访问修饰符
解决方法可以用反射,或者直接将private改成public
5、unidbg实践案例
a、直接调用
主动调用一下sign
package com.xiaojianbang.ndk;
import com.alibaba.fastjson.util.IOUtils;
import com.github.unidbg.*;
import com.github.unidbg.Module;
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.*;
import com.github.unidbg.memory.Memory;
import java.io.File;
import java.io.FileNotFoundException;
public class tre extends AbstractJni{
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
private final DvmClass TreUtil;
private final boolean logging;
tre(boolean logging) throws FileNotFoundException {
this.logging = logging;
emulator = AndroidEmulatorBuilder.for32Bit()
.setProcessName("com.maihan.tredian")
.addBackendFactory(new Unicorn2Factory(true))
.build(); // 创建模拟器实例,要模拟32位或者64位,在这里区分
final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口
memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析
vm = emulator.createDalvikVM(); // 创建Android虚拟机
vm.setJni( this);
vm.setVerbose(logging); // 设置是否打印Jni调用细节
DalvikModule dm = vm.loadLibrary(new File("/Users/whitebird/Android/unidbg/unidbg/unidbg-android/src/test/java/com/xiaojianbang/ndk/libtre.so"), false);
module = dm.getModule();
TreUtil =vm.resolveClass("com/maihan/tredian/util/TreUtil");
}
void destroy() {
IOUtils.close(emulator);
if (logging) {
System.out.println("destroy");
}
}
public static void main(String[] args) throws Exception {
tre test = new tre(true);
test.callFunc();
test.destroy();
}
void callFunc() {
String data ="app_ver=100&nonce=wi3nwo1632749058036×tamp=1632749058&tzrd=BwzXzSGFyiPstMIVuzTZb7LzTZzbXRJOFzpbQiIaT7tLD0cc1eFFficAAUFoQPlbwk1GAlvWKaP4ipqRnbsFguJ0fALWPNMT6vcqr2uBwiETYt29YHmXhn+VadBnDGFnvzttJTttfKExb/bBJFSuEHKUKh+upPAGYjMNZN9hK1OdN9HxyH8Nx5BM2BnciDsQzjZLg8JAmGuHYiIUedIZaiRuUL/1np58iIU3duQ3B4KPTXvpPYb97T0ARzf8TjQp//LpfYv+QBaqNu6CBaKolV77fC1pqaR7v6eai7CIAABbWAKj6xanD+GEEDzvSTz7ZxTiZrCnAf9+0dNq7QfzbI/uf9LhD+24MshM4pcKh6FpA1PwcHH9wFWofI8dw8KCcrMdLfQ4303sA4Bp/ghN9IzBBtMIj21UAcFFgkch1iKcVTbJ8dlNWk679L7bHDutTmKSZ556wvrhtklUg9yDi0KKS/3QiNVcTXfXj+N/3T4=";
StringObject stringObject = TreUtil.callStaticJniMethodObject(emulator,"sign(Ljava/lang/String;)Ljava/lang/String;",data);
System.out.println(stringObject);
}
}
b、补环境
我们知道unidbg的作用是模拟执行so中的函数,也就是使用C/C++编写的函数,它处于Native层。而Native的函数是Java层的函数通过JNI调用起来的, 那么Native也可以通过JNI这座桥梁去调用Java层的函数。在Native层调用Java层的函数的时候,unidbg中并没有这些函数的实现,那么这些so就无法正常的通过unidbg加载起来。
这边调用Java层的函数,所以我们需要补环境,直接返回一个字符串就行了。
@Override
public DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
if ("com/sichuanol/cbgc/util/LogShutDown->getAppSign()Ljava/lang/String;".equals(signature)) {
return new StringObject(vm, "0093CB6721DAF15D31CFBC9BBE3A2B79");
}
return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList);
}
这个字符串也就是App的签名可以通过frida的hook获得,或者直接看logcat
代码中已经打印出来app sign。
现在依然有问题,我们需要定位,通过下断点看堆栈的方式
Debugger debugger =emulator.attach();
debugger.addBreakPoint(0x40061b12);
Java_com_sichuanol_cbgc_util_SignManager_getSign + 0x18f输入到ida,跳到了_android_log_print这行
分析了一下代码,发现v10已经被release了,但是再下面又被调用了,我们现在需要绕过这个打印。
emulator.getBackend().hook_add_new(new CodeHook() {
@Override
public void hook(Backend backend, long address, int size, Object user) {
emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_PC,address+4+1);
}
@Override
public void onAttach(UnHook unHook) {
}
@Override
public void detach() {
}
},module.base+0xABE, module.base + 0xABE, null);
利用hook的方式,当程序执行到0xABE的时候,让pc寄存器跳过打印,直接往下执行。
需要跳过4个字节,又由于是thumb指令,还需要+1
成功得到结果
emulator.attach().addBreakPoint(module.base + 0xABE, new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_PC,address+4+1);
return true;
}
});
通过addBreakPoint可以添加一个回调函数,当断点触发时,直接让指令+5就行了,和上面的思路一样。
package com.whitebird;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
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.debugger.BreakPointCallback;
import com.github.unidbg.debugger.Debugger;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.memory.Memory;
import unicorn.ArmConst;
import java.io.File;
import java.io.IOException;
public class Wtf extends AbstractJni {
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
private final DvmClass SignManager;
private final boolean logging;
Wtf(boolean logging) {
this.logging = logging;
emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.sichuanol.cbgc").build(); // 创建模拟器实例,要模拟32位或者64位,在这里区分
final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口
memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析
vm = emulator.createDalvikVM(); // 创建Android虚拟机
vm.setJni(this);
vm.setVerbose(logging); // 设置是否打印Jni调用细节
DalvikModule dm = vm.loadLibrary(new File("/Users/whitebird/Android/unidbg/unidbg-android/src/test/java/com/whitebird/libwtf.so"), false); // 加载libttEncrypt.so到unicorn虚拟内存,加载成功以后会默认调用init_array等函数
//dm.callJNI_OnLoad(emulator); // 手动执行JNI_OnLoad函数
module = dm.getModule(); // 加载好的libttEncrypt.so对应为一个模块
SignManager = vm.resolveClass("com/sichuanol/cbgc/util/SignManager");
}
void destroy() throws IOException {
emulator.close();
if (logging) {
System.out.println("destroy");
}
}
public static void main(String[] args) throws Exception {
Wtf test = new Wtf(true);
test.callFunc();
test.destroy();
}
void callFunc() {
// Debugger debugger =emulator.attach();
// debugger.addBreakPoint(0x40061b12);
// emulator.getBackend().hook_add_new(new CodeHook() {
// @Override
// public void hook(Backend backend, long address, int size, Object user) {
// emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_PC,address+4+1);
// }
//
// @Override
// public void onAttach(UnHook unHook) {
//
// }
// @Override
// public void detach() {
//
// }
// },module.base+0xABE, module.base + 0xABE, null);
//
emulator.attach().addBreakPoint(module.base + 0xABE, new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_PC,address+4+1);
return true;
}
});
String data = "1636221462621";
StringObject strResult = SignManager.callStaticJniMethodObject(emulator, "getSign(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", new StringObject(vm, ""), new StringObject(vm, ""), new StringObject(vm, data)); // 执行Jni方法
System.out.println(strResult);
}
@Override
public DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
if ("com/sichuanol/cbgc/util/LogShutDown->getAppSign()Ljava/lang/String;".equals(signature)) {
return new StringObject(vm, "0093CB6721DAF15D31CFBC9BBE3A2B79");
}
return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList);
}
}
c、mfw
package com.whitebird;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
import com.github.unidbg.Module;
import com.github.unidbg.debugger.BreakPointCallback;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.memory.Memory;
import unicorn.ArmConst;
import java.io.File;
import java.io.IOException;
public class mfw extends AbstractJni {
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
private final DvmClass xAuthencode;
private final boolean logging;
mfw(boolean logging) {
this.logging = logging;
emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.mfw.roadbook").build(); // 创建模拟器实例,要模拟32位或者64位,在这里区分
final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口
memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析
vm = emulator.createDalvikVM(); // 创建Android虚拟机
vm.setJni(this);
vm.setVerbose(logging); // 设置是否打印Jni调用细节
DalvikModule dm = vm.loadLibrary(new File("/Users/whitebird/Android/unidbg/unidbg-android/src/test/java/com/whitebird/libmfw.so"), false); // 加载libttEncrypt.so到unicorn虚拟内存,加载成功以后会默认调用init_array等函数
//dm.callJNI_OnLoad(emulator); // 手动执行JNI_OnLoad函数
module = dm.getModule(); // 加载好的libttEncrypt.so对应为一个模块
xAuthencode = vm.resolveClass("com/mfw/tnative/AuthorizeHelper");
}
void destroy() throws IOException {
emulator.close();
if (logging) {
System.out.println("destroy");
}
}
public static void main(String[] args) throws Exception {
mfw test = new mfw(true);
test.callFunc();
test.destroy();
}
void callFunc() {
emulator.attach().addBreakPoint(module.base + 0x914C, new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_PC,address+4+1);
emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_R0,1);
return true;
}
});
String data = "PUT&https%3A%2F%2Fmapi.mafengwo.cn%2Frest%2Fapp%2Fuser%2Flogin%2F&after_style%3Ddefault%26app_code%3Dcom.mfw.roadbook%26app_ver%3D8.1.6%26app_version_code%3D535%26brand%3Dgoogle%26channel_id%3DGROWTH-WAP-LC-3%26device_id%3DAC%253A37%253A43%253AA9%253A3F%253A34%26device_type%3Dandroid%26hardware_model%3DPixel%26mfwsdk_ver%3D20140507%26oauth_consumer_key%3D5%26oauth_nonce%3Dcfa857ff-8f4c-4268-8c75-37f07c7aaccf%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1632795895%26oauth_version%3D1.0%26open_udid%3DAC%253A37%253A43%253AA9%253A3F%253A34%26put_style%3Ddefault%26screen_height%3D1794%26screen_scale%3D2.625%26screen_width%3D1080%26sys_ver%3D10%26time_offset%3D480%26x_auth_mode%3Dclient_auth%26x_auth_password%3Da12345678%26x_auth_username%3D15968079477";
StringObject strResult = xAuthencode.callStaticJniMethodObject(emulator,
"xAuthencode(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)Ljava/lang/String;",
vm.resolveClass("android/content/Context"),
new StringObject(vm,data),
new StringObject(vm,""),
new StringObject(vm,"com.mfw.roadbook"),
true
);
System.out.println(strResult);
}
}
利用emulator.getBackend().reg_write绕过调用JNI的地方,同时修改该函数的返回值即R0
d、绕过is_initialised
这边有个值如果没被初始化就会exit(1)
package com.whitebird;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
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.debugger.BreakPointCallback;
import com.github.unidbg.debugger.Debugger;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.memory.Memory;
import unicorn.ArmConst;
import java.io.File;
import java.io.IOException;
public class hoge extends AbstractJni {
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
private final DvmClass signature;
private final boolean logging;
hoge(boolean logging) {
this.logging = logging;
emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.hoge.android.app.fujian").build(); // 创建模拟器实例,要模拟32位或者64位,在这里区分
final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口
memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析
vm = emulator.createDalvikVM(); // 创建Android虚拟机
vm.setJni(this);
vm.setVerbose(logging); // 设置是否打印Jni调用细节
DalvikModule dm = vm.loadLibrary(new File("/Users/whitebird/Android/unidbg/unidbg-android/src/test/java/com/whitebird/libm2o_jni.so"), false); // 加载libttEncrypt.so到unicorn虚拟内存,加载成功以后会默认调用init_array等函数
//dm.callJNI_OnLoad(emulator); // 手动执行JNI_OnLoad函数
module = dm.getModule(); // 加载好的libttEncrypt.so对应为一个模块
signature = vm.resolveClass("com/hoge/android/jni/Utils");
}
void destroy() throws IOException {
emulator.close();
if (logging) {
System.out.println("destroy");
}
}
public static void main(String[] args) throws Exception {
hoge test = new hoge(true);
test.callFunc();
test.destroy();
}
void callFunc() {
emulator.attach().addBreakPoint(module.base + 0xA92C, new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_PC,address+8+1);
return true;
}
});
StringObject stringObject = signature.callStaticJniMethodObject(emulator,"signature(Ljava/lang/String;Ljava/lang/String)Ljava/lang/String",
new StringObject(vm,"4.0.0"),
new StringObject(vm,"1632814143009d0rkV9"));
System.out.println(stringObject);
}
}
直接利用 emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_PC,address+8+1);跳过检测
6、unidbg中的VirtualModule
某些so可能整个加载不起来,或者处理起来很麻烦
unidbg的虚拟模块功能,可以用来注册虚拟的so,自己实现so中相应的方法
libandroid.so用于读取app的assests资源
如果so中需要依赖该libandroid.so,可以注册libandroid.so到内存中:new AndroidModule(emulator, vm).register(memory);
告诉我们libxiaojianbang.so的加载需要libxiaojianbangA.so
package com.whitebird;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.memory.Memory;
import java.io.*;
public class NativeHelper extends AbstractJni {
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
private final DvmClass NativeHelper;
private final boolean logging;
NativeHelper(boolean logging) {
this.logging = logging;
emulator = AndroidEmulatorBuilder.for64Bit().setProcessName("com.xiaojianbang.app").build(); // 创建模拟器实例,要模拟32位或者64位,在这里区分
final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口
memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析
vm = emulator.createDalvikVM(); // 创建Android虚拟机
vm.setJni(this);
vm.setVerbose(logging); // 设置是否打印Jni调用细节
new XiaojianbangAModule(emulator, vm).register(memory);
DalvikModule dm = vm.loadLibrary(new File("/Users/whitebird/Android/unidbg/unidbg-android/src/test/java/com/whitebird/libxiaojianbang.so"), false); // 加载libttEncrypt.so到unicorn虚拟内存,加载成功以后会默认调用init_array等函数
module = dm.getModule(); // 加载好的 libxiaojianbang.so 对应为一个模块
dm.callJNI_OnLoad(emulator); // 手动执行JNI_OnLoad函数
NativeHelper = vm.resolveClass("com/xiaojianbang/ndk/NativeHelper");
}
void destroy() throws IOException {
emulator.close();
if (logging) {
System.out.println("destroy");
}
}
public static void main(String[] args) throws Exception {
NativeHelper test = new NativeHelper(true);
test.callFunc();
test.destroy();
}
void callFunc() {
int retval = NativeHelper.callStaticJniMethodInt(emulator, "add(III)I", 0x100, 0x200, 0x300); // 执行Jni方法
System.out.println("retval: 0x" + Integer.toHexString(retval));
}
}
XiaojianbangAModule.java
package com.whitebird;
import com.github.unidbg.Emulator;
import com.github.unidbg.arm.Arm64Svc;
import com.github.unidbg.arm.ArmSvc;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.memory.SvcMemory;
import com.github.unidbg.pointer.UnidbgPointer;
import com.github.unidbg.virtualmodule.VirtualModule;
import com.github.unidbg.virtualmodule.android.AndroidModule;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.util.Map;
public class XiaojianbangAModule extends VirtualModule<VM> {
private static final Log log = LogFactory.getLog(AndroidModule.class);
public XiaojianbangAModule(Emulator<?> emulator, VM vm) {
super(emulator, vm, "libxiaojianbangA.so");
}
@Override
protected void onInitialize(Emulator<?> emulator, final VM vm, Map<String, UnidbgPointer> symbols) {
boolean is64Bit = emulator.is64Bit();
SvcMemory svcMemory = emulator.getSvcMemory();
//要用到的函数在IDA中的符号名
symbols.put("_Z7bssFuncv", svcMemory.registerSvc(is64Bit ? new Arm64Svc() {
@Override
public long handle(Emulator<?> emulator) {
fromJava(emulator, vm);
return 0;
}
} : new ArmSvc() {
@Override
public long handle(Emulator<?> emulator) {
fromJava(emulator, vm);
return 0;
}
}));
}
private static long fromJava(Emulator<?> emulator, VM vm) {
System.out.println("libxiaojianbangA.so is virtual module");
return 0;
}
}
其实有点类似于hook
7、处理so与系统的交互
1、代码方式
之前如果我们想要unidbg在native层调用java层的函数需要补环境,现在如果我们想要访问一些文件也是要补环境的。
我们用unidbg先调用一下这个函数看看,这个函数没有参数和返回值
这里用的unidbg自带的maps,我们现在把程序运行时真正的maps提取出来
package com.whitebird;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
import com.github.unidbg.Module;
import com.github.unidbg.file.FileResult;
import com.github.unidbg.file.IOResolver;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.linux.file.SimpleFileIO;
import com.github.unidbg.memory.Memory;
import java.io.*;
public class NativeHelper extends AbstractJni implements IOResolver {
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
private final DvmClass NativeHelper;
private final boolean logging;
NativeHelper(boolean logging) {
this.logging = logging;
emulator = AndroidEmulatorBuilder.for64Bit().setProcessName("com.xiaojianbang.app").build(); // 创建模拟器实例,要模拟32位或者64位,在这里区分
final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口
memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析
vm = emulator.createDalvikVM(); // 创建Android虚拟机
vm.setJni(this);
vm.setVerbose(logging); // 设置是否打印Jni调用细节
emulator.getSyscallHandler().addIOResolver(this);
new XiaojianbangAModule(emulator, vm).register(memory);
DalvikModule dm = vm.loadLibrary(new File("/Users/whitebird/Android/unidbg/unidbg-android/src/test/java/com/whitebird/libxiaojianbang.so"), false);
module = dm.getModule(); // 加载好的 libxiaojianbang.so 对应为一个模块
NativeHelper = vm.resolveClass("com/xiaojianbang/ndk/NativeHelper");
}
void destroy() throws IOException {
emulator.close();
if (logging) {
System.out.println("destroy");
}
}
public static void main(String[] args) throws Exception {
NativeHelper test = new NativeHelper(true);
test.callFunc();
test.destroy();
}
void callFunc() {
emulator.attach().addBreakPoint(module.base+0x2138+1);
NativeHelper.callStaticJniMethod(emulator, "readSomething()"); // 执行Jni方法
}
@Override
public FileResult resolve(Emulator emulator, String pathname, int oflags) {
if (("/proc/self/maps").equals(pathname)) {
return FileResult.success(new SimpleFileIO(oflags, new File("/Users/whitebird/Android/unidbg/unidbg-android/src/test/java/com/whitebird/maps"), pathname));
//return FileResult.success(new ByteArrayFileIO(oflags, pathname, "xiaojianbangmaps".getBytes()));
}
//emulator.attach().debug();
System.out.println("whitebird: " + pathname);
return null;
}
}
通过实现IOResolver接口的resolve方法,可以进行文件的重定向,现在unidbg运行起来就是打开的我们自己指定的maps了
记得需要通过emulator.getSyscallHandler().addIOResolver(this);进行注册
2、使用rootfs虚拟文件系统
此时这个目录相当于我们手机上的根目录了,我们可以把代码中要读取的文件放进去,不过在这里maps没有用,因为unidbg内置了一个maps,这个是用代码实现的,代码的方式优先级大于使用rootfs虚拟文件系统
emulator = AndroidEmulatorBuilder.for64Bit().setProcessName("com.xiaojianbang.app").setRootDir(new File("/Users/whitebird/Android/unidbg/unidbg-android/src/test/java/com/whitebird/rootfs")).build();
3、进程pid的获取与设置
emulator.getPid()
this.pid = 6666
4、unidbg中全局变量的设置与获取
emulator.set(..., ...)
emulator.get(...)
5、arm32下的unidbg多线程
unidbg本身不支持多线程,不过有大佬开发了32位下的多线程unidbg
https://github.com/asmjmp0/unidbgMutilThread
6、获取环境变量
上图是真正的环境变量,有的时候我们的App也会去获取环境变量,所以这里也需要我们补环境。
6.1、unidbg提供了对环境变量的初始化
定义在src/main/java/com/github/unidbg/linux/AndroidElfLoader.java中
这个优先级比较高,直接改这里就行了
改完后
6.2、通过libc里的setenv设置环境变量
Symbol setenv = module.findSymbolByName("setenv", true);
setenv.call(emulator, "PATH", "xxx", 0);
不过好像改了没反应,可能优先级比较低
6.3、Hook getenv
getenv是libc里的函数,Hook这个函数,产生调用就打印日志
emulator.attach().addBreakPoint(module.findSymbolByName("getenv").getAddress(), new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
RegisterContext context = emulator.getContext();
String key = context.getPointerArg(0).getString(0);
System.out.println("whitebird getenv: " + key);
return true;
}
});
7、HookListener
package com.whitebird;
import com.github.unidbg.Emulator;
import com.github.unidbg.arm.Arm64Hook;
import com.github.unidbg.arm.ArmHook;
import com.github.unidbg.arm.HookStatus;
import com.github.unidbg.arm.backend.BackendException;
import com.github.unidbg.arm.context.RegisterContext;
import com.github.unidbg.hook.HookListener;
import com.github.unidbg.linux.android.SystemPropertyHook;
import com.github.unidbg.linux.android.SystemPropertyProvider;
import com.github.unidbg.memory.SvcMemory;
import com.sun.jna.Pointer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
public class GetEnvHook implements HookListener {
private final Emulator<?> emulator;
public GetEnvHook(Emulator<?> emulator) {
this.emulator = emulator;
}
@Override
public long hook(SvcMemory svcMemory, String libraryName, String symbolName, final long old) {
if ("libc.so".equals(libraryName)) {
if ("getenv".equals(symbolName)) {
if (emulator.is64Bit()) {
return svcMemory.registerSvc(new Arm64Hook() {
@Override
protected HookStatus hook(Emulator<?> emulator) {
RegisterContext context = emulator.getContext();
int index = 0;
Pointer pointer = context.getPointerArg(index);
String key = pointer.getString(0);
System.out.println("whitebird hooklisten:" + key);
return HookStatus.RET(emulator, old);
}
}).peer;
} else {
return svcMemory.registerSvc(new ArmHook() {
@Override
protected HookStatus hook(Emulator<?> emulator) {
RegisterContext context = emulator.getContext();
int index = 0;
Pointer pointer = context.getPointerArg(index);
String key = pointer.getString(0);
System.out.println("whitebird hooklisten:" + key);
return HookStatus.RET(emulator, old);
}
}).peer;
}
}
}
return 0; }
}
然后在NativeHelper中增加一个监听器
GetEnvHook getEnvHook =new GetEnvHook(emulator);
memory.addHookListener(getEnvHook);
建议与系统交互的函数都先Hook一下,防止出现错误而unidbg没有打印错误信息而导致无法确认错误所在
8、hook popen
这里读取了两次PATH
我们hook了getenv,确实打印了两次,但是中间的报了两个INFO,应该就是popen的问题
把调试信息全部打开
在popen下个断点
emulator.attach().addBreakPoint(module.findSymbolByName("popen").getAddress(), new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
RegisterContext context = emulator.getContext();
String command = context.getPointerArg(0).getString(0);
System.out.println("popen command: " + command);
return true;
}
});
尝试直接跳过popen
emulator.attach().addBreakPoint(module.base + 0x26E4, new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
emulator.getBackend().reg_write(Arm64Const.UC_ARM64_REG_PC, address + 4);
return true;
}
});
进入so分析一下error
我们没得到v3,但是v3在下面用到了,所以还需要跳过__fgets_chk(&v5, 512LL, v3, 512LL),不过这个是一个循环,还需要跳出循环
emulator.attach().addBreakPoint(module.base + 0x2744, new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
emulator.getBackend().reg_write(Arm64Const.UC_ARM64_REG_PC, address + 4);
return true;
}
});
emulator.attach().addBreakPoint(module.base + 0x276C, new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
if (count == 0) {
count = 1;
System.out.println("while is run 0");
} else if (count == 1) {
emulator.getBackend().reg_write(Arm64Const.UC_ARM64_REG_X8, 0);
System.out.println("while is run 1");
}
return true;
}
});
先跳过函数调用,然后在while循环判断的地方修改寄存器
现在就可以正常打印了
8、Linux内核的Syscall
1、系统调用号与对应的函数
https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md
比如0调用号,对应linux里面的read
2、unidbg中对syscall的实现
unidbg也补了一些syscall的实现
对一些调用号进行了处理,不过不是所有的调用号,因此可能需要我们自己补环境。
3、unidbg的检测点
uname -a指令可以得到设备的一些信息
特征还是比较明显的,我们可以改掉。
4、接管未处理的调用号
比如之前的popen虽然是libc.so的函数,但是底层调用了系统调用号且unidbg没有处理
480 pipe和549 fork
可以发现对于找不到的NR,会有专门的处理函数,我们的目的就是重写这个函数,自己实现调用号的处理
package com.whitebird;
import com.github.unidbg.Emulator;
import com.github.unidbg.arm.context.Arm32RegisterContext;
import com.github.unidbg.linux.ARM64SyscallHandler;
import com.github.unidbg.memory.SvcMemory;
import com.sun.jna.Pointer;
public class MyARMSyscallHandler extends ARM64SyscallHandler {
public MyARMSyscallHandler(SvcMemory svcMemory) {
super(svcMemory);
}
@Override
protected boolean handleUnknownSyscall(Emulator<?> emulator, int NR) {
if (NR == 114) {
Arm32RegisterContext context = null;
int pid = context.getR0Int();
Pointer wstatus = context.getR1Pointer();
int options = context.getR2Int();
Pointer rusage = context.getR3Pointer();
System.out.println("wait4 pid=" + pid + ", wstatus=" + wstatus + ", options=0x" + Integer.toHexString(options) + ", rusage=" + rusage);
return true;
}
return super.handleUnknownSyscall(emulator, NR);
}
}
现在就需要去替换掉原来的ARM64SyscallHandler
将emulator = AndroidEmulatorBuilder.for64Bit().setProcessName(“com.xiaojianbang.app”).build();改成了上面的样子
其实for64Bit就是下图的函数,只不过展开了
还会调用build
再跟进AndroidARM64Emulator,就可以发现引用调用号的地方,所以重写了UnixSyscallHandler
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 767778848@qq.com