app逆向之安卓native层安全逆向分析(二):unidbg+ida使用+过签名校验
前言
继续跟着龙哥的unidbg学习:SO逆向入门实战教程二:calculateS_so 逆向_白龙~的博客-CSDN博客
还是那句,我会借鉴龙哥的文章,以一个初学者的角度,加上自己的理解,把内容丰富一下,尽量做到不在龙哥的基础上画蛇添足,哈哈。感谢观看的朋友
分析
首先抓包分析:
其中,里面的s就是今天的需要逆向的加密参数了。
调试
老样子,打开jadx,发现没壳,可以的,直接看吧,拿着这几个参数一顿搜,直接搜【p】
感觉有两个地方很可疑,进去一看:
跟下调用栈,很快就找到这里:
ok,用objection hook下,发现确实调用了这里
再仔细看看这里,明显这里很奇怪了
ok,终于到这里了,这里就跟龙哥给的位置一致了
先不急着用unidbg,先调试下,hook下这个方法,哎哟,我擦,这直接就对上了
看看这三个参数 ,一个是context,上下文,又叫寄存器,第二个是账号密码加起来,第三个就是一个特殊的值,大概率是加的盐,刺激。
unidbg调试
1.ida分析
先用ida打开看看:发现是静态注册的,可以的
选中a1
然后按键盘【y】 ,把第一个参数的类型改成JNIEnv *,这样就可以更好的反编译c代码:
点ok,瞬间这段代码的可读性就更强了
大概看了一眼,反正前面有个if判断,然后就进入主逻辑,然后返回
选到这个函数
然后按【tab】 键:这个0x1E7C就是这个函数的地址了。记一下,后面会用到
2.搭架子
开始搭建unidbg的架子,新建一个文件,然后把apk和目标so文件放进去
先把架子搭起来,这里我们直接复制前面oasis的:
package com.weibo;
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 com.github.unidbg.pointer.UnidbgPointer;
import com.github.unidbg.utils.Inspector;
import com.sun.jna.Pointer;
import keystone.Keystone;
import keystone.KeystoneArchitecture;
import keystone.KeystoneEncoded;
import keystone.KeystoneMode;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class international extends AbstractJni {
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
international() {
// 创建模拟器实例,进程名建议依照实际进程名填写,可以规避针对进程名的校验
emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.weibo.international").build();
// 获取模拟器的内存操作接口
final Memory memory = emulator.getMemory();
// 设置系统类库解析
memory.setLibraryResolver(new AndroidResolver(23));
// 创建Android虚拟机,传入APK,Unidbg可以替我们做部分签名校验的工作
vm = emulator.createDalvikVM(new File("unidbg-android\\src\\test\\java\\com\\weibo\\sinaInternational.apk"));
// 加载目标SO
DalvikModule dm = vm.loadLibrary(new File("unidbg-android\\src\\test\\java\\com\\weibo\\libutility.so"), true); // 加载so到虚拟内存
//获取本SO模块的句柄,后续需要用它
module = dm.getModule();
vm.setJni(this); // 设置JNI
vm.setVerbose(true); // 打印日志
// dm.callJNI_OnLoad(emulator); // 调用JNI OnLoad
}
public static void main(String[] args) {
international test = new international();
System.out.println(test);
}
}
运行下,没啥问题
2.地址调用
方法先写好,参数还是用list,然后用vm.addLocalObject包装一下添加进去,第一个参数是context,直接给个空就行,剩下的参数直接复制hook到的放进去
好的,现在运行一下
3.找异常执行原因——签名校验,以及绕过
可以的,跟龙哥的博客一样,报错了,然后找找日志,看看上面的一个
复制这个地址,ida里,按键盘【g】输入地址跳转过去
跳转到这里:
改下JNIEnv
好的,根据龙哥说的,有packagemanagger之类的,大概率是签名检测
选中这个方法,按【x】找交叉引用
很有缘分的又看到了这个sub_1c60
进去看:
再回到这里,应该就是这个判断了
按下tab键,记住地址,就是这个0xFFF7EBFE了
要能返回是true才给过,ok,直接把这里hook下,也就是直接这么改下,
在java层里,我们直接hook这个方法修改返回值为1就行,但是在这里,是在so里面,怎么搞呢?根据龙哥说的,用arm指令修改就行
用上面的地址,写个过验证的:
龙哥还给了另一种patch的方法:
public void patchverfify(){
int patchCode = 0x4FF00100;
emulator.getMemory().pointer(module.base+0x1E86).setInt(0,patchCode);
}
public 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);
}
}
然后执行下,ok,这就出来了,舒服:
但是好像跟hook到的返回不一样:
不急,再来看看,擦,复制的时候,激动了,把逗号复制进去了
ok,这下对上了,跟hook的结果一致
代码
package com.weibo;
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 com.github.unidbg.pointer.UnidbgPointer;
import com.github.unidbg.utils.Inspector;
import com.sun.jna.Pointer;
import keystone.Keystone;
import keystone.KeystoneArchitecture;
import keystone.KeystoneEncoded;
import keystone.KeystoneMode;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class international extends AbstractJni {
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
international() {
// 创建模拟器实例,进程名建议依照实际进程名填写,可以规避针对进程名的校验
emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.weibo.international").build();
// 获取模拟器的内存操作接口
final Memory memory = emulator.getMemory();
// 设置系统类库解析
memory.setLibraryResolver(new AndroidResolver(23));
// 创建Android虚拟机,传入APK,Unidbg可以替我们做部分签名校验的工作
vm = emulator.createDalvikVM(new File("unidbg-android\\src\\test\\java\\com\\weibo\\sinaInternational.apk"));
// 加载目标SO
DalvikModule dm = vm.loadLibrary(new File("unidbg-android\\src\\test\\java\\com\\weibo\\libutility.so"), true); // 加载so到虚拟内存
//获取本SO模块的句柄,后续需要用它
module = dm.getModule();
vm.setJni(this); // 设置JNI
vm.setVerbose(true); // 打印日志
// dm.callJNI_OnLoad(emulator); // 调用JNI OnLoad
}
public static void main(String[] args) {
international test = new international();
test.patchverfify();
System.out.println(test.calculateS());
}
public void patchverfify(){
int patchCode = 0x4FF00100;
emulator.getMemory().pointer(module.base+0x1E86).setInt(0,patchCode);
}
public 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);
}
}
public String calculateS() {
List<Object> list = new ArrayList<>(10);
list.add(vm.getJNIEnv()); // arg1,env
list.add(0); // arg2,jobject
DvmObject<?> context = vm.resolveClass("android/content/Context").newObject(null);
list.add(vm.addGlobalObject(context));
list.add(vm.addLocalObject(new StringObject(vm, "135691695686123456789")));
list.add(vm.addLocalObject(new StringObject(vm, "CypCHG2kSlRkdvr2RG1QF8b2lCWXl7k7")));
Number number = module.callFunction(emulator, 0x1E7C + 1, list.toArray());
String result = vm.getObject(number.intValue()).getValue().toString();
return result;
}
}
4.符号调用
前面都是地址调用,而龙哥自己也说过,他喜欢用地址调用。不过这里,总要学习下,怎么符号调用
怎么找符号,首选选中这个方法:
然后按tab键,进入汇编页面:
然后再按空格,然后这个export就是要用的符号表了,注意了,这是只是刚好都一样,很多时候,这三个,不一样的,找准export的才行
开始调用,就换了下名字,其他都没变,对比两个不同的调用方式,结果一样,没毛病
代码:
package com.weibo;
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 com.github.unidbg.pointer.UnidbgPointer;
import com.github.unidbg.utils.Inspector;
import com.sun.jna.Pointer;
import keystone.Keystone;
import keystone.KeystoneArchitecture;
import keystone.KeystoneEncoded;
import keystone.KeystoneMode;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class international extends AbstractJni {
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
international() {
// 创建模拟器实例,进程名建议依照实际进程名填写,可以规避针对进程名的校验
emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.weibo.international").build();
// 获取模拟器的内存操作接口
final Memory memory = emulator.getMemory();
// 设置系统类库解析
memory.setLibraryResolver(new AndroidResolver(23));
// 创建Android虚拟机,传入APK,Unidbg可以替我们做部分签名校验的工作
vm = emulator.createDalvikVM(new File("unidbg-android\\src\\test\\java\\com\\weibo\\sinaInternational.apk"));
// 加载目标SO
DalvikModule dm = vm.loadLibrary(new File("unidbg-android\\src\\test\\java\\com\\weibo\\libutility.so"), true); // 加载so到虚拟内存
//获取本SO模块的句柄,后续需要用它
module = dm.getModule();
vm.setJni(this); // 设置JNI
vm.setVerbose(true); // 打印日志
// dm.callJNI_OnLoad(emulator); // 调用JNI OnLoad
}
public static void main(String[] args) {
international test = new international();
test.patchverfify();
System.out.println("offset===>:" + test.calculateS());
System.out.println("symbol===>:" + test.calculateS1());
}
public String calculateS1() {
List<Object> list = new ArrayList<>(10);
list.add(vm.getJNIEnv()); // arg1,env
list.add(0); // arg2,jobject
DvmObject<?> context = vm.resolveClass("android/content/Context").newObject(null);
list.add(vm.addGlobalObject(context));
list.add(vm.addLocalObject(new StringObject(vm, "135691695686123456789")));
list.add(vm.addLocalObject(new StringObject(vm, "CypCHG2kSlRkdvr2RG1QF8b2lCWXl7k7")));
Number number = module.callFunction(emulator, "Java_com_sina_weibo_security_WeiboSecurityUtils_calculateS", list.toArray());
String result = vm.getObject(number.intValue()).getValue().toString();
return result;
}
public void patchverfify() {
int patchCode = 0x4FF00100;
emulator.getMemory().pointer(module.base + 0x1E86).setInt(0, patchCode);
}
public 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);
}
}
public String calculateS() {
List<Object> list = new ArrayList<>(10);
list.add(vm.getJNIEnv()); // arg1,env
list.add(0); // arg2,jobject
DvmObject<?> context = vm.resolveClass("android/content/Context").newObject(null);
list.add(vm.addGlobalObject(context));
list.add(vm.addLocalObject(new StringObject(vm, "135691695686123456789")));
list.add(vm.addLocalObject(new StringObject(vm, "CypCHG2kSlRkdvr2RG1QF8b2lCWXl7k7")));
Number number = module.callFunction(emulator, 0x1E7C + 1, list.toArray());
String result = vm.getObject(number.intValue()).getValue().toString();
return result;
}
}
部署
有没有想过,搞的这个,如果想部署成一个web服务,以供后续的业务代码调用呢?不然这么个项目,部署在服务器上,然后 每次启动都调用unidbg,说实话不是太现实,所以这里就需要把unidbg打包成jar包:
最实用且简单的方法:
1.
2.
3.
4.
5.
6.
等待结果:
然后根目录就会多一个out目录:
7.这个文件就是最后能直接通过java执行的:
但是有个问题,我们写的apk和so文件,给定的地址是unidgb-android/src里的
要打包的话,就得改下路径,改成相对路径
然后删除out文件夹,重新上面的打包操作,然后把apk和so放到跟unidbg-jar同级的目录:
这样就好了, 终端执行看看,很好,结果也有的,这样就可以把这个out包整个打包 ,然后部署到服务器上就行了。
还有更多的打包方式:
知识点总结
1.ida,按y修改JNIEnv,IDA 7.5之前,JNIEnv需要导入jni.h,7.5之后不需要导入jni.h文件
2.ida,按g 输入地址跳转
3.ida,按x,查看交叉引用
4.ida,optional->generel,把这个改成4,可以查看架构模式,arm架构是固定4位,thumb架构是混合的
5. 有签名校验的需要去patch,patch的时候不能+1,只有运行和hook的时候才+1
6.setJNIload方法只有动态注册方法的时候才执行,静态注册的不用执行
7.keystone,是将汇编语言转成地址的。capstone是将地址转成汇编语言的
8.在线汇编和地址互转的网站:https://armconverter.com/
9.参数的基本类型,比如int,long等,其他的对象类型一律要手动 addLocalObject,其中context对象用:
DvmObject<?> context = vm.resolveClass("android/content/Context").newObject(null);// context
list.add(vm.addLocalObject(context))
10.找准一行汇编,Alt+G快捷键,查看架构类型,值为1则是thumb,为0则是arm,如果ida解析有误,可以手动修改这个值
11.RM调用约定,入参前四个分别通过R0-R3调用,返回值通过R0返回
12.unidbg的项目可以打包成java包执行
13.Unidbg内嵌了多种Hook工具,目前主要是四种
- Dobby
- HookZz
- xHook
- Whale
xHook 是爱奇艺开源的基于PLT HOOK的Hook框架,它无法Hook不在符号表里的函数,也不支持inline hook,这在我们的逆向分析中是无法忍受的,所以在这里不去理会它。
hale 在Unidbg的测试用例中只有对符号表函数的Hook,没看到Inline Hook 或者 非导出函数的Hook,所以也不去考虑。
HookZz是Dobby的前身,两者都可以Hook 非导出表中的函数,即IDA中显示为sub_xxx的函数,也都可以进行inline hook,所以二选一就行了,HookZz针对32位比较稳定,Dobby针对64位比较稳定
比如这里hook MDStringOld方法:
完整代码
package com.weibo; import com.github.unidbg.AndroidEmulator; import com.github.unidbg.Emulator; import com.github.unidbg.Module; import com.github.unidbg.hook.hookzz.*; 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 com.github.unidbg.pointer.UnidbgPointer; import com.github.unidbg.utils.Inspector; import com.sun.jna.Pointer; import keystone.Keystone; import keystone.KeystoneArchitecture; import keystone.KeystoneEncoded; import keystone.KeystoneMode; import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.List; public class international extends AbstractJni { private final AndroidEmulator emulator; private final VM vm; private final Module module; international() { // 创建模拟器实例,进程名建议依照实际进程名填写,可以规避针对进程名的校验 emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.weibo.international").build(); // 获取模拟器的内存操作接口 final Memory memory = emulator.getMemory(); // 设置系统类库解析 memory.setLibraryResolver(new AndroidResolver(23)); // 创建Android虚拟机,传入APK,Unidbg可以替我们做部分签名校验的工作 vm = emulator.createDalvikVM(new File("unidbg-android\\src\\test\\java\\com\\weibo\\sinaInternational.apk")); // 加载目标SO DalvikModule dm = vm.loadLibrary(new File("unidbg-android\\src\\test\\java\\com\\weibo\\libutility.so"), true); // 加载so到虚拟内存 //获取本SO模块的句柄,后续需要用它 module = dm.getModule(); vm.setJni(this); // 设置JNI vm.setVerbose(true); // 打印日志 // dm.callJNI_OnLoad(emulator); // 调用JNI OnLoad } public static void main(String[] args) { international test = new international(); test.patchverfify(); test.HookMDString(); System.out.println("offset===>:" + test.calculateS()); } public String calculateS() { List<Object> list = new ArrayList<>(10); list.add(vm.getJNIEnv()); // arg1,env list.add(0); // arg2,jobject DvmObject<?> context = vm.resolveClass("android/content/Context").newObject(null); list.add(vm.addGlobalObject(context)); list.add(vm.addLocalObject(new StringObject(vm, "135691695686123456789"))); list.add(vm.addLocalObject(new StringObject(vm, "CypCHG2kSlRkdvr2RG1QF8b2lCWXl7k7"))); Number number = module.callFunction(emulator, 0x1E7C + 1, list.toArray()); String result = vm.getObject(number.intValue()).getValue().toString(); return result; } public void HookMDString(){ IHookZz hookZz = HookZz.getInstance(emulator); hookZz.wrap(module.base+0x1BD0+1, new WrapCallback<HookZzArm32RegisterContext>() { @Override public void preCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) { Pointer input = ctx.getPointerArg(0); System.out.println("input:"+input.getString(0)); } @Override public void postCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) { Pointer result = ctx.getPointerArg(0); System.out.println("result:"+result.getString(0)); } }); } public String calculateS1() { List<Object> list = new ArrayList<>(10); list.add(vm.getJNIEnv()); // arg1,env list.add(0); // arg2,jobject DvmObject<?> context = vm.resolveClass("android/content/Context").newObject(null); list.add(vm.addGlobalObject(context)); list.add(vm.addLocalObject(new StringObject(vm, "135691695686123456789"))); list.add(vm.addLocalObject(new StringObject(vm, "CypCHG2kSlRkdvr2RG1QF8b2lCWXl7k7"))); Number number = module.callFunction(emulator, "Java_com_sina_weibo_security_WeiboSecurityUtils_calculateS", list.toArray()); String result = vm.getObject(number.intValue()).getValue().toString(); return result; } public void patchverfify() { int patchCode = 0x4FF00100; emulator.getMemory().pointer(module.base + 0x1E86).setInt(0, patchCode); } public 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); } } }
结语
说实话,目前来说,不难,得后续的大厂项目才会很难