这篇是原理类的,后面会有一篇对应的实战。周五了,建议提肛摸鱼下班,有什么事下周再说吧^ ^
1 相关知识
1.1 Native方法
native方法,实际上就是在Java中调用C++的底层方法,实际方法的实现在APP的so文件中,作为二进制存储。如果要举一个贴切的例子,我们可以看C#中调用dll函数的范例(它们的原理一样):
using System;
using System.Runtime.InteropServices;
class Program
{
[ ]
public static extern void HelloWorld();
static void Main()
{
HelloWorld();
}
}
在Java中则大可能是这样:
public class Aes {
public static native String aesDecrypt(String str);
public static native String aesDecrypt1(String str);
public static native String aesDecrypt3(String str);
public static native String aesDecryptKey(String str, String str2);
public static native String aesEncrypt(String str);
public static native String aesEncrypt2(String str);
public static native String aesEncryptKey(String str, String str2);
private static native void native_signature(Context context);
public Aes() {
}
public static void signature(Context context) {
native_signature(context);
}
static {
System.loadLibrary("aes"); // Catch: UnsatisfiedLinkError -> L6
return;
L6:
e = move-exception;
PrintStream printStream = System.out;
printStream.println("loadLibrary ses: " + e.getMessage());
}
}
总而言之,只要函数只有声明,且声明中带有native关键词,我们就可以认为它的函数体在so文件内,也就是它是一个native函数。
2 Native方法溯源
2.1 寻找so文件
我们知道native函数的函数体在so文件内,而对应的so文件名则是lib[xxx].so。这个xxx取决于System.loadLibrary传入的值。例如如下情况:
则对应的so文件为libaes.so。
2.2 寻找静态Native函数体
在静态注册的情况下,native方法名与so文件中的函数名存在一一对应的关系,如Java包com.a中的B类中的函数c,在so文件中的名字则为Java_com_a_B_c。
由于总会带关键词Java,我们可以在IDA函数列表中先搜索单词Java以判断当前方法是否为静态注册。
不开IDA的情况下也可以搜导出表。
如果搜索中没有函数命中,则代表native函数使用动态绑定。
2.3 寻找动态Native函数体
2.3.1 静态溯源法
在未经混淆的情况下我们可以搜索native函数的原名,然后根据出现位置进行查找,也可以直接读JNI_Onload。以下是一个例子:
首先,已知native函数名为aesEncrypt2,则直接搜索text:
有两个结果,一个只读数据段一个数据段,都可以看看。
rodata段没有什么好看的,就是一些杂乱的数据:
data段,可以看到这里挨着函数名定义了函数的跳转位置,为0x3745C+1,即0x3745D(这几个数据位置没有必然性,但是实战时可以推断猜测一下)。
2.3.2 frida挂钩法
我们使用一个frida脚本打印出所有被注册的native函数位置(需要调用此native函数才能打印出位置)。
注入frida后作出能调用native函数的操作(这个前期应该都定位过,这里就不赘述了):
frida -H 192.168.3.181:65000 --no-pause -f com.nursinghome.monitor -l F:TTTTN 8APP_MiniProgramtelefonofridanative.js
如果使用frida继续Hook,我们需要拿到的则是module base(模块基地址)和offset(偏移量)(请参考dll分析)。
也就是:
如果我们要在IDA中定位函数,则只需要关注offset。获取offset后,在IDA的Jump->Jump to address填入偏移量跳转到函数的位置:
可以看到直接跳转到了一个sub的开头:
按下F5反编译:
可以看到此函数对应的参数有三个。由于IDA无法识别函数的类型,所以这三个参数类型都为int,实际上应该是一个JNIEnv*和两个未知类型。
3 使用IDA调试so文件
IDA调试和frida一样,需要在手机端另外安装一个server并转发端口。
由于笔者使用了lamda,就直接使用lamda脚本打开IDA-server,如果自行使用server只要push到手机内存并运行即可。
启动IDA-server的lamda脚本:
#启动IDA
debug = d.stub("Debug")
# 启动 IDA 32 服务端(端口可自定义)64位后面接一个64就行
debug.start_ida64(port=23946)
然后使用adb命令转发端口:
adb forward tcp:23946 tcp:23946
先切到Debugger->select debugger,开始编辑调试选项:
这里选择Remote ARM Linux/Android debugger(远程ARM Linux或安卓调试器)
在这里配置一下对应的IP端口,默认为虚拟机IP 和 23946
然后Cancel,选择Debugger->Attach to process:
出现运行的进程列表,即连接成功。选择对应的运行中APK包名即可开始调试。
由于运行进程数量偏多,我们可以使用frida查找APK运行对应的pid,然后快速选择进程,frida-ps -U一下然后接一个管道findstr即可:
4 使用frida Hook native方法
其实跟hook Java方法的步骤差不多,只不过寻找函数的方式不同,需要获取native函数的指针来引用。
如果函数有导出,可以使用Process.getModuleByName("lib名").enumerateSymbols()枚举所有Symbols然后通过函数导出名,然后通过寻找导出名对应的偏移量做指针。
var ishook_libart = false;
function hook_libart() {
if (ishook_libart === true) {
return;
}
var symbols = Module.enumerateSymbolsSync("libart.so");
var addrGetStringUTFChars = null;
var addrNewStringUTF = null;
var addrFindClass = null;
var addrGetMethodID = null;
var addrGetStaticMethodID = null;
var addrGetFieldID = null;
var addrGetStaticFieldID = null;
var addrRegisterNatives = null;
var addrAllocObject = null;
var addrCallObjectMethod = null;
var addrGetObjectClass = null;
var addrReleaseStringUTFChars = null;
for (var i = 0; i < symbols.length; i++) {
var symbol = symbols[i];
if (symbol.name == "_ZN3art3JNI17GetStringUTFCharsEP7_JNIEnvP8_jstringPh") {
addrGetStringUTFChars = symbol.address;
console.log("GetStringUTFChars is at ", symbol.address, symbol.name);
} else if (symbol.name == "_ZN3art3JNI12NewStringUTFEP7_JNIEnvPKc") {
addrNewStringUTF = symbol.address;
console.log("NewStringUTF is at ", symbol.address, symbol.name);
} else if (symbol.name == "_ZN3art3JNI9FindClassEP7_JNIEnvPKc") {
addrFindClass = symbol.address;
console.log("FindClass is at ", symbol.address, symbol.name);
} else if (symbol.name == "_ZN3art3JNI11GetMethodIDEP7_JNIEnvP7_jclassPKcS6_") {
addrGetMethodID = symbol.address;
console.log("GetMethodID is at ", symbol.address, symbol.name);
} else if (symbol.name == "_ZN3art3JNI17GetStaticMethodIDEP7_JNIEnvP7_jclassPKcS6_") {
addrGetStaticMethodID = symbol.address;
console.log("GetStaticMethodID is at ", symbol.address, symbol.name);
} else if (symbol.name == "_ZN3art3JNI10GetFieldIDEP7_JNIEnvP7_jclassPKcS6_") {
addrGetFieldID = symbol.address;
console.log("GetFieldID is at ", symbol.address, symbol.name);
} else if (symbol.name == "_ZN3art3JNI16GetStaticFieldIDEP7_JNIEnvP7_jclassPKcS6_") {
addrGetStaticFieldID = symbol.address;
console.log("GetStaticFieldID is at ", symbol.address, symbol.name);
} else if (symbol.name == "_ZN3art3JNI15RegisterNativesEP7_JNIEnvP7_jclassPK15JNINativeMethodi") {
addrRegisterNatives = symbol.address;
console.log("RegisterNatives is at ", symbol.address, symbol.name);
} else if (symbol.name.indexOf("_ZN3art3JNI11AllocObjectEP7_JNIEnvP7_jclass") >= 0) {
addrAllocObject = symbol.address;
console.log("AllocObject is at ", symbol.address, symbol.name);
} else if (symbol.name.indexOf("_ZN3art3JNI16CallObjectMethodEP7_JNIEnvP8_jobjectP10_jmethodIDz") >= 0) {
addrCallObjectMethod = symbol.address;
console.log("CallObjectMethod is at ", symbol.address, symbol.name);
} else if (symbol.name.indexOf("_ZN3art3JNI14GetObjectClassEP7_JNIEnvP8_jobject") >= 0) {
addrGetObjectClass = symbol.address;
console.log("GetObjectClass is at ", symbol.address, symbol.name);
} else if (symbol.name.indexOf("_ZN3art3JNI21ReleaseStringUTFCharsEP7_JNIEnvP8_jstringPKc") >= 0) {
addrReleaseStringUTFChars = symbol.address;
console.log("ReleaseStringUTFChars is at ", symbol.address, symbol.name);
}
}
if (addrRegisterNatives != null) {
Interceptor.attach(addrRegisterNatives, {
onEnter: function (args) {
console.log("[RegisterNatives] method_count:", args[3]);
var env = args[0];
var java_class = args[1];
var funcAllocObject = new NativeFunction(addrAllocObject, "pointer", ["pointer", "pointer"]);
var funcGetMethodID = new NativeFunction(addrGetMethodID, "pointer", ["pointer", "pointer", "pointer", "pointer"]);
var funcCallObjectMethod = new NativeFunction(addrCallObjectMethod, "pointer", ["pointer", "pointer", "pointer"]);
var funcGetObjectClass = new NativeFunction(addrGetObjectClass, "pointer", ["pointer", "pointer"]);
var funcGetStringUTFChars = new NativeFunction(addrGetStringUTFChars, "pointer", ["pointer", "pointer", "pointer"]);
var funcReleaseStringUTFChars = new NativeFunction(addrReleaseStringUTFChars, "void", ["pointer", "pointer", "pointer"]);
var clz_obj = funcAllocObject(env, java_class);
var mid_getClass = funcGetMethodID(env, java_class, Memory.allocUtf8String("getClass"), Memory.allocUtf8String("()Ljava/lang/Class;"));
var clz_obj2 = funcCallObjectMethod(env, clz_obj, mid_getClass);
var cls = funcGetObjectClass(env, clz_obj2);
var mid_getName = funcGetMethodID(env, cls, Memory.allocUtf8String("getName"), Memory.allocUtf8String("()Ljava/lang/String;"));
var name_jstring = funcCallObjectMethod(env, clz_obj2, mid_getName);
var name_pchar = funcGetStringUTFChars(env, name_jstring, ptr(0));
var class_name = ptr(name_pchar).readCString();
funcReleaseStringUTFChars(env, name_jstring, name_pchar);
//console.log(class_name);
var methods_ptr = ptr(args[2]);
var method_count = parseInt(args[3]);
for (var i = 0; i < method_count; i++) {
var name_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3));
var sig_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize));
var fnPtr_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize * 2));
var name = Memory.readCString(name_ptr);
var sig = Memory.readCString(sig_ptr);
var find_module = Process.findModuleByAddress(fnPtr_ptr);
console.log("[RegisterNatives] java_class:", class_name, "name:", name, "sig:", sig, "fnPtr:", fnPtr_ptr, "module_name:", find_module.name, "module_base:", find_module.base, "offset:", ptr(fnPtr_ptr).sub(find_module.base));
}
},
onLeave: function (retval) { }
});
}
ishook_libart = true;
}
hook_libart();
使用脚本跑出各个Symbols的偏移量,获取输出里的相对偏移量,然后在脚本中获取模块偏移量(因为模块偏移量会变化)加上相对偏移量,即为函数对应的指针。在frida脚本里则这样写:
var baseAdd=Module.findBaseAddress("模块so文件");
var realAdd=baseAdd.add(ptr(之前获取到的相对偏移量));
指针的使用方式和普通对象相同,就像这样:
Interceptor.attach(指针,{ //这是Hook函数
onEnter:function(args){
},onLeave:function (retval) {
}
});
5 使用unidbg和frida主动调用native方法
unidbg和frida各有千秋,frida不需要补环境,unidbg可以在无apk无安卓模拟器的情况下运行。
5.1 使用unidbg调用native方法
我直接上代码了,下载下来在TTEncrypt下面改也可以(TTEncrypt这个示例会给一个日志记录,很详细),新建一个也可以。
这里的代码是新建的实例:
package com.augusttheodor.abc;
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.StringObject;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.virtualmodule.android.AndroidModule;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
public class abcMain {
public static void main(String[] args) {
AndroidEmulator emulator = AndroidEmulatorBuilder.for32Bit()
.setProcessName("进程名,随意设置或保持你要模拟的APK进程名")
.addBackendFactory(new Unicorn2Factory(true))
.build(); // 创建模拟器实例,要模拟32位或者64位,在这里区分
final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口
memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析
VM vm = emulator.createDalvikVM(new File("此处可删除,或设置为要模拟的APK路径")); // 创建Android虚拟机
new AndroidModule(emulator,vm).register(memory);
DalvikModule dm = vm.loadLibrary(new File("so文件路径"), false); // 加载libttEncrypt.so到unicorn虚拟内存,加载成功以后会默认调用init_array等函数
dm.callJNI_OnLoad(emulator); // 手动执行JNI_OnLoad函数
Module module = dm.getModule();
DvmClass dvc = vm.resolveClass("要加载的方法对应的类名");
List<Object> argss = new ArrayList<>(10); //加载参数,为JNIEnv*,args...
argss.add(vm.getJNIEnv());
argss.add(vm.addLocalObject(new StringObject(vm,"")));
argss.add(vm.addLocalObject(new StringObject(vm,"Aa123456")));
Number result=module.callFunction(emulator,0x3745d,argss.toArray()); //我感觉这个是指针 //返回执行结果的指针
System.out.println(vm.getObject(result.intValue()).getValue().toString()); //这里将指针转回String然后打印
}
}
5.2 使用frida调用native方法
使用frida调用native方法则简便许多,不需要做初始的进程生成之类的。但与此相对的则需要按照参数类型声明一个NativeFunction。以下是frida的脚本范例:
var env = Java.vm.getEnv(); //获取环境
var baseAdd=Module.findBaseAddress("so文件名");
var realAdd=baseAdd.add(ptr(之前获取到的偏移量));
var enc_native=new NativeFunction(realAdd,"pointer"(返回值,一般都是指针),["pointer"(JNIEnv*),参数表...]);
console.log(enc_native(a3,env.newStringUtf(""),env.newStringUtf(""))); 然后直接调用
原文始发于微信公众号(重生之成为赛博女保安):Android渗透测试从零开始⑧:调用Native
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论