这篇文章上次修改于 1168 天前,可能其部分内容已经发生变化,如有疑问可询问作者。
原文 -> https://blog.csdn.net/qq_38851536/article/details/117419041
前言
这是SO逆向入门实战教程的第二篇,总共会有十三篇,十三个实战。有以下几个注意点:
- 主打入门级的实战,适合有一定基础但缺少实战的朋友(了解JNI,也上过一些Native层逆向的课,但感觉实战匮乏,想要壮壮胆,入入门)。
- 侧重新工具、新思路、新方法的使用,算法分析的常见路子是Frida Hook + IDA ,在本系列中,会淡化Frida 的作用,采用Unidbg Hook + IDA 的路线。
- 主打入门,但并不限于入门,你会在样本里看到有浅有深的魔改加密算法、以及OLLVM、SO对抗等内容。
- 细,非常的细,奶妈级教学。
- 共十三篇,1-2天更新一篇。每篇的资料放在文末的百度网盘中。
准备
这是我们的分析目标
参数一是Context上下文,参数二是传入的明文,参数三是固定的值,疑似Key或者盐。
返回值是8位的Sign,且输入不变的情况下,输出也固定不变。
Unidbg模拟执行
IDA中打开libutility.so,先搜索一下会不会是静态绑定。
难得遇到静态绑定的Native函数,先参数重命名,在笔者的IDA 7.5中,JNIEnv不需要导入jni.h,设置一下type就可以识别JNI函数。
if ( sub_1C60(a1, context) )
{
if ( (*a1)->PushLocalFrame(a1, 16) >= 0 )
{
v6 = (*a1)->GetStringUTFChars(a1, inputKey, 0);
v18 = (char *)(*a1)->GetStringUTFChars(a1, inputBytes, 0);
v7 = j_strlen(v18);
v8 = v7 + j_strlen(v6) + 1;
v9 = j_malloc(v8);
j_memset(v9, 0, v8);
j_strcpy((char *)v9, v18);
j_strcat((char *)v9, v6);
v10 = (_BYTE *)MDStringOld(v9);
v11 = (char *)j_malloc(9u);
*v11 = v10[1];
v11[1] = v10[5];
v11[2] = v10[2];
v11[3] = v10[10];
v11[4] = v10[17];
v11[5] = v10[9];
v11[6] = v10[25];
v12 = v10[27];
v11[8] = 0;
v11[7] = v12;
v21 = (*a1)->FindClass(a1, "java/lang/String");
v22 = (*a1)->GetMethodID(a1, v21, "<init>", "([BLjava/lang/String;)V");
v13 = j_strlen(v11);
v19 = (*a1)->NewByteArray(a1, v13);
v14 = j_strlen(v11);
(*a1)->SetByteArrayRegion(a1, v19, 0, v14, v11);
v15 = (*a1)->NewStringUTF(a1, "utf-8");
v16 = (*a1)->NewObject(a1, v21, v22, v19, v15);
j_free(v11);
j_free(v9);
(*a1)->ReleaseStringUTFChars(a1, (jstring)inputBytes, v18);
inputBytes = (int)(*a1)->PopLocalFrame(a1, v16);
}
else
{
inputBytes = 0;
}
}
return inputBytes;
如果sub_1C60函数False,函数直接返回0,显然这是一条错误的逻辑,而传入的参数又是context,这很容易让人想到是一个签名校验函数。先不往下看了,上Unidbg。
同样先搭一下基础的架子,这个样本连JNI OnLoad都没有。
package com.lession2;
// 导入通用且标准的类库
import com.github.unidbg.linux.android.dvm.AbstractJni;
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.linux.android.dvm.array.ByteArray;
import com.github.unidbg.memory.Memory;
import com.lession1.oasis;
import java.io.File;
public class sina extends AbstractJni{
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
sina() {
// 创建模拟器实例,进程名建议依照实际进程名填写,可以规避针对进程名的校验
emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.sina.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\\lession2\\sinaInternational.apk"));
//
// vm = emulator.createDalvikVM(null);
// 加载目标SO
DalvikModule dm = vm.loadLibrary(new File("unidbg-android\\src\\test\\java\\com\\lession2\\libutility.so"), true); // 加载so到虚拟内存
//获取本SO模块的句柄,后续需要用它
module = dm.getModule();
vm.setJni(this); // 设置JNI
vm.setVerbose(true); // 打印日志
// 样本连JNI OnLoad都没有
// dm.callJNI_OnLoad(emulator); // 调用JNI OnLoad
};
public static void main(String[] args) {
sina test = new sina();
}
}
接下来添加一个calculateS函数,依然是地址方式调用,ARM32有Thumb和ARM两种指令模式,此处是thumb模式,所以地址要在start基础上+1。
注意看代码,相较于第一讲,这里的入参有一些新情况
- context如何构造
- 字符串类型如何构造
除了基本类型,比如int,long等,其他的对象类型一律要手动 addLocalObject。
public String calculateS(){
List<Object> list = new ArrayList<>(10);
list.add(vm.getJNIEnv()); // 第一个参数是env
list.add(0); // 第二个参数,实例方法是jobject,静态方法是jclazz,直接填0,一般用不到。
DvmObject<?> context = vm.resolveClass("android/content/Context").newObject(null);// context
list.add(vm.addLocalObject(context));
list.add(vm.addLocalObject(new StringObject(vm, "12345")));
list.add(vm.addLocalObject(new StringObject(vm, "r0ysue")));
// 因为代码是thumb模式,别忘了+1
Number number = module.callFunction(emulator, 0x1E7C + 1, list.toArray())[0];
String result = vm.getObject(number.intValue()).getValue().toString();
return result;
};
完整代码如下
package com.lession2;
// 导入通用且标准的类库
import com.github.unidbg.linux.android.dvm.AbstractJni;
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.linux.android.dvm.array.ByteArray;
import com.github.unidbg.memory.Memory;
import com.lession1.oasis;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
public class sina extends AbstractJni{
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
sina() {
// 创建模拟器实例,进程名建议依照实际进程名填写,可以规避针对进程名的校验
emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.sina.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\\lession2\\sinaInternational.apk"));
//
// vm = emulator.createDalvikVM(null);
// 加载目标SO
DalvikModule dm = vm.loadLibrary(new File("unidbg-android\\src\\test\\java\\com\\lession2\\libutility.so"), true); // 加载so到虚拟内存
//获取本SO模块的句柄,后续需要用它
module = dm.getModule();
vm.setJni(this); // 设置JNI
vm.setVerbose(true); // 打印日志
// 样本连JNI OnLoad都没有
// dm.callJNI_OnLoad(emulator); // 调用JNI OnLoad
};
public String calculateS(){
List<Object> list = new ArrayList<>(10);
list.add(vm.getJNIEnv()); // 第一个参数是env
list.add(0); // 第二个参数,实例方法是jobject,静态方法是jclazz,直接填0,一般用不到。
DvmObject<?> context = vm.resolveClass("android/content/Context").newObject(null);// context
list.add(vm.addLocalObject(context));
list.add(vm.addLocalObject(new StringObject(vm, "12345")));
list.add(vm.addLocalObject(new StringObject(vm, "r0ysue")));
Number number = module.callFunction(emulator, 0x1E7C + 1, list.toArray())[0];
String result = vm.getObject(number.intValue()).getValue().toString();
return result;
};
public static void main(String[] args) {
sina test = new sina();
System.out.println(test.calculateS());
}
}
顺嘴一提如何判断是Thumb还是Arm模式,最粗暴的方式就是试错法,比如此处不加1,指令肯定就跑偏,会报错非法指令
这个办法粗鲁且有效,第二个办法是从知识角度出发,ARM模式指令总是4字节长度,Thumb指令长度多数为2字节,少部分指令是4字节。
IDA顶部选项框:Options-General
查看汇编指令的机器码
我们发现此处指令大多为两个字节长度,那就是Thumb。如果你还不放心,找准一行汇编,Alt+G快捷键。
Thumb模式是1,ARM模式是0。除此之外,如果偶尔IDA反汇编出了问题,可以考虑它识别错了模式,需要Alt+G手动修改,调整模式。
言归正传,运行我们的代码。
真恼人,竟然报错了,而且没有较为明确的提示
看一下Warn一行显示的报错所处地址
IDA快捷键G跳转到0x2c8d,看这个架势a1是JNIEnv指针
把a1转成JNIEnv
按X查看一下交叉引用,再往上看看,可以发现就是sub_1C60函数。从先前的分析可以看出,这个函数会返回一个值,如果为真,就继续执行,为假,就返回0。再结合此地里面找的这些类,诸如PackageManager之流,很难不让人联想到签名校验函数。
可以直接patch掉对这个函数的调用,说人话就是把这儿的函数跳转改成不跳转了呗。
正常执行这个函数的话,如果校验没问题返回真,比如1,校验失败返回0。
根据ARM调用约定,入参前四个分别通过R0-R3调用,返回值通过R0返回,所以这儿可以通过"mov r0,1"实现我们的目标——不执行这个函数,并给出正确的返回值。除此之外还有一个幸运的地方在于,这个函数并没有产生一些之后需要使用的值或者中间变量,这让我们不需要管别的寄存器。
此处的机器码是FF F7 EB FE, 查看一下"mov r0,1"的机器码,这里我们使用ARMConvert看一下,除此之外,使用别的工具查看汇编代码也是可以的。
即把 FF F7 EB FE 替换成 4FF00100 即可
这个事儿我们过去用Keypatch干,用Frida 干,用010Editor干,现在用Unidbg干罢了,新瓶装旧酒!
Unidbg提供了两种方法打Patch,简单的需求可以调用Unicorn对虚拟内存进行修改,如下
public void patchVerify(){
int patchCode = 0x4FF00100; //
emulator.getMemory().pointer(module.base + 0x1E86).setInt(0,patchCode);
}
需要注意的是,这儿地址可别+1了,Thumb的+1只在运行和Hook时需要考虑,打Patch可别想。
看一下现在的完整代码,以及运行结果。
package com.lession2;
// 导入通用且标准的类库
import com.github.unidbg.linux.android.dvm.AbstractJni;
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.linux.android.dvm.array.ByteArray;
import com.github.unidbg.memory.Memory;
import com.lession1.oasis;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
public class sina extends AbstractJni{
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
sina() {
// 创建模拟器实例,进程名建议依照实际进程名填写,可以规避针对进程名的校验
emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.sina.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\\lession2\\sinaInternational.apk"));
//
// vm = emulator.createDalvikVM(null);
// 加载目标SO
DalvikModule dm = vm.loadLibrary(new File("unidbg-android\\src\\test\\java\\com\\lession2\\libutility.so"), true); // 加载so到虚拟内存
//获取本SO模块的句柄,后续需要用它
module = dm.getModule();
vm.setJni(this); // 设置JNI
vm.setVerbose(true); // 打印日志
// 样本连JNI OnLoad都没有
// dm.callJNI_OnLoad(emulator); // 调用JNI OnLoad
};
public String calculateS(){
List<Object> list = new ArrayList<>(10);
list.add(vm.getJNIEnv()); // 第一个参数是env
list.add(0); // 第二个参数,实例方法是jobject,静态方法是jclazz,直接填0,一般用不到。
DvmObject<?> context = vm.resolveClass("android/content/Context").newObject(null);// context
list.add(vm.addLocalObject(context));
list.add(vm.addLocalObject(new StringObject(vm, "12345")));
list.add(vm.addLocalObject(new StringObject(vm, "r0ysue")));
Number number = module.callFunction(emulator, 0x1E7C + 1, list.toArray())[0];
String result = vm.getObject(number.intValue()).getValue().toString();
return result;
};
public void patchVerify(){
int patchCode = 0x4FF00100; //
emulator.getMemory().pointer(module.base + 0x1E86).setInt(0,patchCode);
}
public static void main(String[] args) {
sina test = new sina();
test.patchVerify();
System.out.println(test.calculateS());
}
}
直接出结果了。
我们的Patch效果非常可,帮助我们绕过了签名校验的烦人逻辑。但有些情况下,我们可能要动态打Patch,或者我们并不想上什么网站,看MOV R0,1的机器码是什么,这时候可以使用Unidbg给我们封装的Patch方法。
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);
}
};
逻辑也非常清晰,先确认有没有找对地方,地址上是不是 FF F7 EB FE,再用Unicorn的好兄弟Keystone 把patch代码"mov r0,1"转成机器码,填进去,校验一下长度是否相等,收工。
算法分析
在第一篇中,我们使用Findhash对算法做了分析,但是纯粹用Unidbg做算法分析一定是件激动人心的事,让我们来试一下吧。
代码的逻辑非常简单,将text和key拼接起来,然后放到MDStringOld函数中,出来的结果,从中分别抽出第1位(从0开始),第5位,等8位,就是结果了。
所以这个时候我们的关注点就是MDStringOld函数,首要的就是获取它的参数和返回值。
- 它的参数可以验证我们对MDStringOld函数前面的分析有没有出错
- 它的返回值可以验证我们对MDStringOld函数后面和结果的分析有没有出错
这个函数的地址是0x1BD0+1
如果是Frida动态分析,我们会通过如下方式Hook
function hookMDStringOld() {
var baseAddr = Module.findBaseAddress("libutility.so")
var MDStringOld = baseAddr.add(0x1BD0).add(0x1)
Interceptor.attach(MDStringOld, {
onEnter: function (args) {
console.log("input:\n", hexdump(this.arg0))
},
onLeave: function (retval) {
console.log("result:\n", hexdump(retval))
}
})
}
那么在Unidbg中,我们该怎么做呢
Unidbg内嵌了多种Hook工具,目前主要是四种
- Dobby
- HookZz
- xHook
- Whale
但我们没必要四种都学
xHook 是爱奇艺开源的基于PLT HOOK的Hook框架,它无法Hook不在符号表里的函数,也不支持inline hook,这在我们的逆向分析中是无法忍受的,所以在这里不去理会它。
Whale 在Unidbg的测试用例中只有对符号表函数的Hook,没看到Inline Hook 或者 非导出函数的Hook,所以也不去考虑。
HookZz是Dobby的前身,两者都可以Hook 非导出表中的函数,即IDA中显示为sub_xxx的函数,也都可以进行inline hook,所以二选一就行了。我喜欢HookZz这个名字,所以就HookZz了。
使用HookZz hook MDStringOld函数,MDStringOld是导出函数,可以传入符号名,解析地址,但管他什么findsymbol,findExport呢,我就认准地址,地址,yyds。
看一下完整代码
package com.lession2;
// 导入通用且标准的类库
import com.github.unidbg.Emulator;
import com.github.unidbg.arm.context.Arm32RegisterContext;
import com.github.unidbg.hook.hookzz.*;
import com.github.unidbg.linux.android.dvm.AbstractJni;
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.linux.android.dvm.array.ByteArray;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.pointer.UnidbgPointer;
import com.github.unidbg.utils.Inspector;
import com.lession1.oasis;
import com.sun.jna.Pointer;
import keystone.Keystone;
import keystone.KeystoneArchitecture;
import keystone.KeystoneEncoded;
import keystone.KeystoneMode;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class sina extends AbstractJni{
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
sina() {
// 创建模拟器实例,进程名建议依照实际进程名填写,可以规避针对进程名的校验
emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.sina.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\\lession2\\sinaInternational.apk"));
//
// vm = emulator.createDalvikVM(null);
// 加载目标SO
DalvikModule dm = vm.loadLibrary(new File("unidbg-android\\src\\test\\java\\com\\lession2\\libutility.so"), true); // 加载so到虚拟内存
//获取本SO模块的句柄,后续需要用它
module = dm.getModule();
vm.setJni(this); // 设置JNI
vm.setVerbose(true); // 打印日志
// 样本连JNI OnLoad都没有
// dm.callJNI_OnLoad(emulator); // 调用JNI OnLoad
};
public String calculateS(){
List<Object> list = new ArrayList<>(10);
list.add(vm.getJNIEnv()); // 第一个参数是env
list.add(0); // 第二个参数,实例方法是jobject,静态方法是jclazz,直接填0,一般用不到。
DvmObject<?> context = vm.resolveClass("android/content/Context").newObject(null);// context
list.add(vm.addLocalObject(context));
list.add(vm.addLocalObject(new StringObject(vm, "12345")));
list.add(vm.addLocalObject(new StringObject(vm, "r0ysue")));
Number number = module.callFunction(emulator, 0x1E7C + 1, list.toArray())[0];
String result = vm.getObject(number.intValue()).getValue().toString();
return result;
};
public void patchVerify(){
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 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));
}
});
}
public static void main(String[] args) {
sina test = new sina();
// test.patchVerify();
test.patchVerify1();
test.HookMDStringold();
System.out.println(test.calculateS());
}
}
运行结果
可以发现,入参就是text+key
验证返回值:439a333788b0cecfce1389d4b83ba1cb
result = 439a333788b0cecfce1389d4b83ba1cb
result[1] = 3
result[5] = 3
result[2] = 9
result[10] = b
验证发现我们关于结果来源的猜想也完全正确。
那么接下来的焦点就是MDStringOld函数了,因为结果是32位,我们首先想到MD5函数,验证一下。
结果完全一致。
NICE,大功告成。
尾声
这个样本十分简单,但让我们更多的理解了Unidbg的功能,下一讲中,让我们解锁更难的样本,探索Unidbg更多功能吧!
只有一条评论 (QwQ)
OωO