修改源码定制ART
zsk Lv4

ART定制方案比较

  1. 源码修改编译,定制rom
    优点:开发简单,可以直接访问ART中的海量的丰富api
    缺点:较笨重,首先需要一台性能足够强大的pc,其次,每次修改完代码都需要重新编译,刷机,测试。
  2. 基于frida,xposed等hook框架定制
    优点:轻量,快捷,不需要性能强大的pc,也无需经历耗时的编译和刷机测试阶段,往往只需要重启app即可
    缺点:往往无法直接复用ART源码中已有的api,只能后使用有限的api
  3. 将修改源码定制rom和frida等hook框架相结合,比如FART和frida相结合,脱壳功能更强大。
    优点:结合了frida的轻量,快捷以及修改源码的简单。

工具使用:
1. 可以使用AndroidStudio导入aosp源码,对framework层相关的java类进行源码阅读和修改定制
2. 可以使用clion等ide利用aosp编译过程中生成的Cmakelists文件,导入ART部分的源码进行阅读和修改定制。

针对ART进行定制时,对于整个AOSP源码,Java层代码只关心Framework、libcore,因此没有必要全部作为源码导入
以下教程用的机型是pixel xl,源码是android 8.1

第一种:源码修改编译,定制rom

编译生成AndroidStudio项目配置文件并导入源码
  1. 编译生成idegen.jar在AOSP源码目录下输入以下命令,生成idegen.jar
    1
    2
    3
    source build/envsetup.sh
    lunch aosp_marlin-userdebug
    mmm development/tools/idegen/
  2. 生成android.ipr和android.iml
    执行idegen.sh脚本,生成即可
    1
    development/tools/idegen/idegen.sh
    运行完毕上面的命令之后,就在根目录生成了2个文件:android.ipr和android.iml
  3. 排除不必要的模块,提高加载速度
    打开android.iml,找到excludeFolder属性,我们可以看到默认有一些模块是被排除了,不会被导入的。
    但实际上,整个AOSP代码中,我们只关心framework模块和libcore模块,因此可以根据自己的需求去掉其它所有模块,只保留framework和libcore,比如将exclude修改为如下内容:
    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
    <excludeFolder url="file://$MODULE_DIR$/art" />
    <excludeFolder url="file://$MODULE_DIR$/bionic" />
    <excludeFolder url="file://$MODULE_DIR$/bootable" />
    <excludeFolder url="file://$MODULE_DIR$/build" />
    <excludeFolder url="file://$MODULE_DIR$/cts" />
    <excludeFolder url="file://$MODULE_DIR$/dalvik" />
    <excludeFolder url="file://$MODULE_DIR$/developers" />
    <excludeFolder url="file://$MODULE_DIR$/development" />
    <excludeFolder url="file://$MODULE_DIR$/device" />
    <excludeFolder url="file://$MODULE_DIR$/docs" />
    <excludeFolder url="file://$MODULE_DIR$/external" />
    <excludeFolder url="file://$MODULE_DIR$/hardware" />
    <excludeFolder url="file://$MODULE_DIR$/kernel" />
    <excludeFolder url="file://$MODULE_DIR$/libcore" />
    <excludeFolder url="file://$MODULE_DIR$/libnativehelper" />
    <excludeFolder url="file://$MODULE_DIR$/out" />
    <excludeFolder url="file://$MODULE_DIR$/pdk" />
    <excludeFolder url="file://$MODULE_DIR$/platform_testing" />
    <excludeFolder url="file://$MODULE_DIR$/prebuilts" />
    <excludeFolder url="file://$MODULE_DIR$/sdk" />
    <excludeFolder url="file://$MODULE_DIR$/system" />
    <excludeFolder url="file://$MODULE_DIR$/test" />
    <excludeFolder url="file://$MODULE_DIR$/toolchain" />
    <excludeFolder url="file://$MODULE_DIR$/tools" />
    <excludeFolder url="file://$MODULE_DIR$/.repo" />
    这样修改之后,打开速度会快很多。接下来使用AndroidStudio直接打开android.ipr文件。 > File-Open…,找到android.ipr即可。
    导入完成之后,就能看到整个AOSP源码了:橘红色的是在上一步exclude排除掉的模块。
    剩余配置参考以下文章:
    https://juejin.cn/post/6969508324618272782
    https://blog.csdn.net/yanbober/article/details/48846331
Clion导入ART
  1. 编译生成cmakelists文件
    打开以下两个开关,CMakeLists.txt就会根据编译环境自动生成
    1
    2
    3
    4
    source build/envsetup.sh
    lunch aosp_marlin-userdebug
    export SOONG_GEN_CMAKEFILES=1
    export SOONG_GEN_CMAKEFILES_DEBUG=1
    下面开始编译
    1
    make -j8
    生成的相关项目的cmakelists文件存放在out目录,不如我们需要导入ART的源码部分对应的路径为:out/development/ide/clion/art,可以看到下面还有很多模块,我们选择使用arm32或者arm64的runtime模块即可,对应的cmakelists文件在
    out/development/ide/clion/art/runtime/libart-arm-android/CMakeLists.txt
    以及
    out/development/ide/clion/art/runtime/libart-arm64-android/CMakeLists.txt
    然后修改根目录为源码的根目录 Tools > CMake > Change Project Root:
APP运行过程中so加载流程源码分析

接下来对so的加载流程进行分析,加载so有两种方式
System.loadLibrary(“so文件名”);
System.load(“so文件相对路径”);
loadLibrary通过loader.findLibrary(libraryName)函数找到库文件的绝对路径,后续跟load一样调用doLoad(String name, ClassLoader loader),最后去调用art下的nativeLoad native函数
/libcore/ojluni/src/main/java/java/lang/Runtime.java

1
2
3
4
5
6
7
8
9
10
11
private String doLoad(String name, ClassLoader loader) {
String librarySearchPath = null;
if (loader != null && loader instanceof BaseDexClassLoader) {
BaseDexClassLoader dexClassLoader = (BaseDexClassLoader) loader;
librarySearchPath = dexClassLoader.getLdLibraryPath();
}
java.lang.System.logE("Runtime doLoad -> " + name + ", classLoader -> " + loader.toString());
synchronized (this) {
return nativeLoad(name, loader, librarySearchPath);
}
}

也就是说doLoad和nativeLoad是所有加载so文件都会调用的函数,那可以在这个函数下添加log信息,这样就可以在Java下监控所有的so

1
java.lang.System.logE("Runtime doLoad -> " + name + ", classLoader -> " + loader.toString());

通过查找源码知道nativeLoad是在xref: /libcore/ojluni/src/main/native/Runtime.c下

1
2
3
4
5
6
JNIEXPORT jstring JNICALL
Runtime_nativeLoad(JNIEnv* env, jclass ignored, jstring javaFilename,
jobject javaLoader, jstring javaLibrarySearchPath)
{
return JVM_NativeLoad(env, javaFilename, javaLoader, javaLibrarySearchPath);
}

然后通过调用链找到 nativeLoad ->Runtime_nativeLoad(Runtime.c) -> JVM_NativeLoad(Openjdk_Jvm.cc) -> LoadNativeLibrary(java_vm_ext.cc)
最后是LoadNativeLibrary函数,查看函数流程,
在函数刚进来的时候添加日志

1
LOG(ERROR) << "[java_vm_ext.cc]JavaVMExt::LoadNativeLibrary -> " << path.c_str();

在加载本地库的前后添加日志

1
2
3
4
5
6
7
8
9
10
11
//add
LOG(ERROR) << "[java_vm_ext.cc]before call LoadNativeLibrary -> " << path_str;
void* handle = android::OpenNativeLibrary(env,
runtime_->GetTargetSdkVersion(),
path_str,
class_loader,
library_path,
&needs_native_bridge,
error_msg);
//add
LOG(ERROR) << "[java_vm_ext.cc]after call LoadNativeLibrary -> " << path_str;

接下来判断是否有JNI_OnLoad,没有则结束函数,有就加载JNI_OnLoad,在加载JNI_OnLoad前后添加日志

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
38
39
40
41
42
43
44
45
46
bool was_successful = false;
void* sym = library->FindSymbol("JNI_OnLoad", nullptr);
if (sym == nullptr) {
VLOG(jni) << "[No JNI_OnLoad found in \"" << path << "\"]";
was_successful = true;
} else {
// Call JNI_OnLoad. We have to override the current class
// loader, which will always be "null" since the stuff at the
// top of the stack is around Runtime.loadLibrary(). (See
// the comments in the JNI FindClass function.)
ScopedLocalRef<jobject> old_class_loader(env, env->NewLocalRef(self->GetClassLoaderOverride()));
self->SetClassLoaderOverride(class_loader);

VLOG(jni) << "[Calling JNI_OnLoad in \"" << path << "\"]";
typedef int (*JNI_OnLoadFn)(JavaVM*, void*);
JNI_OnLoadFn jni_on_load = reinterpret_cast<JNI_OnLoadFn>(sym);
//add
LOG(ERROR) << "[java_vm_ext.cc]before call " << path_str << ".JNI_Onload";
int version = (*jni_on_load)(this, nullptr);
//add
LOG(ERROR) << "[java_vm_ext.cc]after call " << path_str << ".JNI_Onload" << ", returm: " << version;

if (runtime_->GetTargetSdkVersion() != 0 && runtime_->GetTargetSdkVersion() <= 21) {
// Make sure that sigchain owns SIGSEGV.
EnsureFrontOfChain(SIGSEGV);
}

self->SetClassLoaderOverride(old_class_loader.get());

if (version == JNI_ERR) {
StringAppendF(error_msg, "JNI_ERR returned from JNI_OnLoad in \"%s\"", path.c_str());
} else if (JavaVMExt::IsBadJniVersion(version)) {
StringAppendF(error_msg, "Bad JNI version returned from JNI_OnLoad in \"%s\": %d",
path.c_str(), version);
// It's unwise to call dlclose() here, but we can mark it
// as bad and ensure that future load attempts will fail.
// We don't know how far JNI_OnLoad got, so there could
// be some partially-initialized stuff accessible through
// newly-registered native method calls. We could try to
// unregister them, but that doesn't seem worthwhile.
} else {
was_successful = true;
}
VLOG(jni) << "[Returned " << (was_successful ? "successfully" : "failure")
<< " from JNI_OnLoad in \"" << path << "\"]";
}

到这里也能看出来so中的JNI_Onload的函数名必须要c的形式进行编译,还要导出的原因就是这里它需要去查找符号名
重新编译,刷入手机
运行有加壳的app,查看log

image

第二种:基于frida,xposed等hook框架定制

要实现对加载so监控的话,可以结合流程当中的api进行hook来监控so的加载流程,在hook中选择的api层级越底层越好,如果只通过System.load, loadLibrary进行监控的话,可能会遗漏掉通过runtime进行加载的so。
上面分析到加载so底层的api是libnativeloader.so下的OpenNativeLibrary和libart.so下的LoadNativeLibrary
可以使用frida进行hook,在hook前可以先把libnativeloader.so和libart.so脱出来用ida查看导出表OpenNativeLibrary和LoadNativeLibrary的导出符号
/system/lib/libart.so或者/system/lib64/libart.so
/system/lib/libnativeloader.so或者/system/lib64/libnativeloader.so
OpenNativeLibrary的导出符号:_ZN7android17OpenNativeLibraryEP7_JNIEnviPKcP8_jobjectP8_jstringPbPNSt3__112basic_stringIcNS9_11char_traitsIcEENS9_9allocatorIcEEEE

使用frida进行hook

下面就监控OpenNativeLibrary

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
// libart.so
// bool JavaVMExt::LoadNativeLibrary(JNIEnv* env,
// const std::string& path,
// jobject class_loader,
// jstring library_path,
// std::string* error_msg)


// libnativeloader.so
// void* handle = android::OpenNativeLibrary(env,
// runtime_->GetTargetSdkVersion(),
// path_str,
// class_loader,
// library_path,
// &needs_native_bridge,
// error_msg);

function tracesoload(){
console.log(Process.arch);
var libnativeloaderModule = Process.getModuleByName("libnativeloader.so");
// OpenNativeLibrary
var OpenNativeLibraryAddr = libnativeloaderModule.getExportByName("_ZN7android17OpenNativeLibraryEP7_JNIEnviPKcP8_jobjectP8_jstringPbPNSt3__112basic_stringIcNS9_11char_traitsIcEENS9_9allocatorIcEEEE");
Interceptor.attach(OpenNativeLibraryAddr, {
onEnter: function(args){
this.sopath = ptr(args[2]).readUtf8String();
console.log("go into OpenNativeLibrary -> " + this.sopath);
}, onLeave: function(retval) {
console.log("leave OpenNativeLibrary -> " + this.sopath, ", return: " + retval);
}
})
}

function main(){
tracesoload();
}
setImmediate(main);

输出打印

1
2
3
4
5
6
7
8
9
[AOSP on msm8996::com.iCitySuzhou.suzhou001]-> arm64
go into OpenNativeLibrary -> /data/app/com.iCitySuzhou.suzhou001--s3SCRYsyaFmNLfEq3Pj3g==/lib/arm64/libshell-super.com.iCitySuzhou.suzhou001.so
leave OpenNativeLibrary -> /data/app/com.iCitySuzhou.suzhou001--s3SCRYsyaFmNLfEq3Pj3g==/lib/arm64/libshell-super.com.iCitySuzhou.suzhou001.so
go into OpenNativeLibrary -> /data/app/com.iCitySuzhou.suzhou001--s3SCRYsyaFmNLfEq3Pj3g==/lib/arm64/libjcore230.so
leave OpenNativeLibrary -> /data/app/com.iCitySuzhou.suzhou001--s3SCRYsyaFmNLfEq3Pj3g==/lib/arm64/libjcore230.so
go into OpenNativeLibrary -> /system/app/webview/webview.apk!/lib/arm64-v8a/libwebviewchromium.so
leave OpenNativeLibrary -> /system/app/webview/webview.apk!/lib/arm64-v8a/libwebviewchromium.so
go into OpenNativeLibrary -> /system/lib64/libwebviewchromium_plat_support.so
leave OpenNativeLibrary -> /system/lib64/libwebviewchromium_plat_support.so

加载so后还会对这个so进行查找是否含有 JNI_OnLoad
void* sym = library->FindSymbol(“JNI_OnLoad”, nullptr);

1
2
3
4
5
6
7
void* FindSymbol(const std::string& symbol_name, const char* shorty = nullptr)
REQUIRES(!Locks::mutator_lock_) {
return NeedsNativeBridge()
? FindSymbolWithNativeBridge(symbol_name.c_str(), shorty)
: FindSymbolWithoutNativeBridge(symbol_name.c_str());
}
// NeedsNativeBridge()判断如果设备的架构与应用的架构不同返回true,所以这里是走FindSymbolWithoutNativeBridge()
1
2
3
4
5
void* FindSymbolWithoutNativeBridge(const std::string& symbol_name)
REQUIRES(!Locks::mutator_lock_) {
CHECK(!NeedsNativeBridge());
return dlsym(handle_, symbol_name.c_str());
}
1
void* dlsym(void* handle, const char* _Nonnull symbol);

这里对dlsym进行监控的话,就可以监视接下来对 JNI_OnLoad 的查询的过程
dlsym位于libc.so,也使用freida进行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
function tracesoload(){
console.log(Process.arch);
var libnativeloaderModule = Process.getModuleByName("libnativeloader.so");
// OpenNativeLibrary
var OpenNativeLibraryAddr = libnativeloaderModule.getExportByName("_ZN7android17OpenNativeLibraryEP7_JNIEnviPKcP8_jobjectP8_jstringPbPNSt3__112basic_stringIcNS9_11char_traitsIcEENS9_9allocatorIcEEEE");
Interceptor.attach(OpenNativeLibraryAddr, {
onEnter: function(args){
this.sopath = ptr(args[2]).readUtf8String();
console.log("go into OpenNativeLibrary -> " + this.sopath);
}, onLeave: function(retval) {
console.log("leave OpenNativeLibrary -> " + this.sopath, ", return: " + retval);
}
})
var dlsymAddr = Module.findExportByName("libc.so", "dlsym");
Interceptor.attach(dlsymAddr, {
onEnter: function(args){
this.handle = args[0];
this.symbol = ptr(args[1]).readUtf8String();
}, onLeave: function(retval){
console.log("leave dlsym: handle: " + this.handle + ", symbol: " + this.symbol, ", return: " + retval);
}
})
}
function main(){
tracesoload();
}
setImmediate(main);

已经看到查找JNI_OnLoad的流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
leave OpenNativeLibrary -> /data/app/com.iCitySuzhou.suzhou001--s3SCRYsyaFmNLfEq3Pj3g==/lib/arm64/libshell-super.com.iCitySuzhou.suzhou001.so , return: 0x49668d9309b699ed
leave dlsym: handle: 0x49668d9309b699ed, symbol: JNI_OnLoad , return: 0x730a123a5c
leave dlsym: handle: 0xde2d62226fd8b40d, symbol: InitEsxProfile , return: 0x7314e10620
go into OpenNativeLibrary -> /data/app/com.iCitySuzhou.suzhou001--s3SCRYsyaFmNLfEq3Pj3g==/lib/arm64/libjcore230.so
leave OpenNativeLibrary -> /data/app/com.iCitySuzhou.suzhou001--s3SCRYsyaFmNLfEq3Pj3g==/lib/arm64/libjcore230.so , return: 0x513c62ce705a71c5
leave dlsym: handle: 0x513c62ce705a71c5, symbol: JNI_OnLoad , return: 0x7308e5c5bc
leave OpenNativeLibrary -> /system/lib64/libwebviewchromium_plat_support.so , return: 0xef0d55dae65a7f63
leave dlsym: handle: 0xef0d55dae65a7f63, symbol: JNI_OnLoad , return: 0x731ca1e740
leave dlsym: handle: 0xe5b5bf72a8b510e7, symbol: Java_org_chromium_base_library_1loader_LibraryLoader_nativeLibraryLoaded , return: 0x730ce0dc34
leave dlsym: handle: 0xe5b5bf72a8b510e7, symbol: Java_org_chromium_base_library_1loader_LibraryLoader_nativeGetVersionNumber , return: 0x730ce0dee0
leave dlsym: handle: 0xe5b5bf72a8b510e7, symbol: Java_org_chromium_base_TraceEvent_nativeRegisterEnabledObserver , return: 0x730ce10300
leave dlsym: handle: 0xe5b5bf72a8b510e7, symbol: Java_org_chromium_base_PathService_nativeOverride , return: 0x730ce0ed00
leave dlsym: handle: 0xe5b5bf72a8b510e7, symbol: Java_org_chromium_android_1webview_AwContentsStatics_nativeGetProductVersion , return: 0x730cdca980
leave dlsym: handle: 0xe5b5bf72a8b510e7, symbol: Java_org_chromium_android_1webview_AwGLFunctor_nativeGetAwDrawGLFunction , return: 0x730cdcc4e4
leave dlsym: handle: 0xe5b5bf72a8b510e7, symbol: Java_org_chromium_android_1webview_AwContents_nativeSetAwDrawSWFunctionTable , return: 0x730cdbfcc4
leave dlsym: handle: 0xe5b5bf72a8b510e7, symbol: Java_org_chromium_android_1webview_AwContents_nativeSetAwDrawGLFunctionTable , return: 0x730cdb714c
leave dlsym: handle: 0xe5b5bf72a8b510e7, symbol: Java_org_chromium_android_1webview_AwContentsStatics_nativeSetCheckClearTextPermitted , return: 0x730cdcaad4
leave dlsym: handle: 0xe5b5bf72a8b510e7, symbol: Java_org_chromium_base_CommandLine_nativeHasSwitch , return: 0x730ce08b70
leave dlsym: handle: 0xe5b5bf72a8b510e7, symbol: Java_org_chromium_android_1webview_AwContentsStatics_nativeSetSafeBrowsingEnabledByManifest , return: 0x730cdcaa24
leave dlsym: handle: 0xe5b5bf72a8b510e7, symbol: Java_org_chromium_base_CommandLine_nativeAppendSwitch , return: 0x730ce08cd8
......

第三种:定制rom和frida等hook框架相结合

在art中一些关键地方可以预留api给frida hook, 以下面跟踪jni函数绑定为例。

ART定制跟中jni函数绑定

在上面的日志看到很多symbol的值类似静态注册的JNI函数
jni函数的绑定:每一个java层函数在native层都对应一个ArtMethod对象,jni函数在执行前必须完成对jni函数所在类的加载和初始化、以及该jni函数的ArtMethod对象当中属性和so中具体函数地址的绑定

  1. 静态注册的jni函数
  2. 动态注册的jni函数

静态注册的jni函数

通过源码追踪jni注册流程,首先当一个类中的 JNI 函数被调用的时候,ART 会首先确保该类已经被加载和初始化。这一过程通常通过 class_linker.cc 文件中的 ClassLinker::LoadClass 方法完成。在加载过程中,类的属性和成员函数也会被加载和初始化,这一步由 LoadClassMembers 方法处理。

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// 加载类
void ClassLinker::LoadClass(Thread* self,
const DexFile& dex_file,
const DexFile::ClassDef& dex_class_def,
Handle<mirror::Class> klass) {
const uint8_t* class_data = dex_file.GetClassData(dex_class_def);
if (class_data == nullptr) {
return; // no fields or methods - for example a marker interface
}
LoadClassMembers(self, dex_file, class_data, klass);
}
// 加载类成员和函数
void ClassLinker::LoadClassMembers(Thread* self,
const DexFile& dex_file,
const uint8_t* class_data,
Handle<mirror::Class> klass) {
{
// .....部分代码省略.....
// TODO These should really use the iterators.
for (size_t i = 0; it.HasNextDirectMethod(); i++, it.Next()) {
ArtMethod* method = klass->GetDirectMethodUnchecked(i, image_pointer_size_);
// 加载函数
LoadMethod(dex_file, it, klass, method);
// 将方法与其实现的代码进行链接
LinkCode(this, method, oat_class_ptr, class_def_method_index);
uint32_t it_method_index = it.GetMemberIndex();
if (last_dex_method_index == it_method_index) {
// duplicate case
method->SetMethodIndex(last_class_def_method_index);
} else {
method->SetMethodIndex(class_def_method_index);
last_dex_method_index = it_method_index;
last_class_def_method_index = class_def_method_index;
}
class_def_method_index++;
}
// .........
}
}

// 方法与其实际实现的代码进行链接
static void LinkCode(ClassLinker* class_linker,
ArtMethod* method,
const OatFile::OatClass* oat_class,
uint32_t class_def_method_index) REQUIRES_SHARED(Locks::mutator_lock_) {
......
// 检查一个方法是否具有 native 属性,返回true则该方法是一个JNI函数
if (method->IsNative()) {
// Unregistering restores the dlsym lookup stub.
// 解除方法的本地链接, 该方法在调用时不再直接执行本地代码,而是通过解释器来处理
method->UnregisterNative();

if (enter_interpreter || quick_code == nullptr) {
// We have a native method here without code. Then it should have either the generic JNI
// trampoline as entrypoint (non-static), or the resolution trampoline (static).
// TODO: this doesn't handle all the cases where trampolines may be installed.
const void* entry_point = method->GetEntryPointFromQuickCompiledCode();
DCHECK(class_linker->IsQuickGenericJniStub(entry_point) ||
class_linker->IsQuickResolutionStub(entry_point));
}
}
}
// 解除方法的本地链接
void ArtMethod::UnregisterNative() {
CHECK(IsNative() && !IsFastNative()) << PrettyMethod();
// restore stub to lookup native pointer via dlsym
SetEntryPointFromJni(GetJniDlsymLookupStub());
}
// 用于设置 JNI 方法的入口点
void SetEntryPointFromJni(const void* entrypoint) {
DCHECK(IsNative());
SetEntryPointFromJniPtrSize(entrypoint, kRuntimePointerSize);
}
// 在需要 JNI 方法解析时,可以方便地获取特定的查找函数指针
static inline const void* GetJniDlsymLookupStub() {
return reinterpret_cast<const void*>(art_jni_dlsym_lookup_stub);
}

不管是静态注册还是动态注册的jni函数, 当函数所在的类被加载进到 LinkCode 的时候,会进行函数第一次的地址绑定,当这个函数是静态注册的话,在第一次的时候会去调用dlsym查找符号进行绑定,当第二次调用的时候,由于入口的已经被绑定到真正的地址,就不在执行查找的过程,直接发起调用。
实际上 art_jni_dlsym_lookup_stub 是汇编代码,通过源码查找 art_jni_dlsym_lookup_stub
xref: /art/runtime/arch/arm/jni_entrypoints_arm.S

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
#include "asm_support_arm.S"
.extern artFindNativeMethod
ENTRY art_jni_dlsym_lookup_stub
push {r0, r1, r2, r3, lr} @ spill regs
.cfi_adjust_cfa_offset 20
.cfi_rel_offset r0, 0
.cfi_rel_offset r1, 4
.cfi_rel_offset r2, 8
.cfi_rel_offset r3, 12
.cfi_rel_offset lr, 16
sub sp, #12 @ pad stack pointer to align frame
.cfi_adjust_cfa_offset 12
blx artFindNativeMethod
mov r12, r0 @ save result in r12
add sp, #12 @ restore stack pointer
.cfi_adjust_cfa_offset -12
cbz r0, 1f @ is method code null?
pop {r0, r1, r2, r3, lr} @ restore regs
.cfi_adjust_cfa_offset -20
.cfi_restore r0
.cfi_restore r1
.cfi_restore r2
.cfi_restore r3
.cfi_restore lr
bx r12 @ if non-null, tail call to method's code

无论静态注册还是动态注册,当类加载完就被绑定到art_jni_dlsym_lookup_stub这个桥函数中,会执行这段汇编调用artFindNativeMethod去so查符号地址

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// Used by the JNI dlsym stub to find the native method to invoke if none is registered.
#if defined(__arm__) || defined(__aarch64__)
extern "C" const void* artFindNativeMethod() {
Thread* self = Thread::Current();
#else
extern "C" const void* artFindNativeMethod(Thread* self) {
DCHECK_EQ(self, Thread::Current());
#endif
Locks::mutator_lock_->AssertNotHeld(self); // We come here as Native.
ScopedObjectAccess soa(self);

ArtMethod* method = self->GetCurrentMethod(nullptr);
DCHECK(method != nullptr);

// Lookup symbol address for method, on failure we'll return null with an exception set,
// otherwise we return the address of the method we found.
// FindCodeForNativeMethod 查找函数地址
void* native_code = soa.Vm()->FindCodeForNativeMethod(method);
if (native_code == nullptr) {
self->AssertPendingException();
return nullptr;
}
// Register so that future calls don't come here
return method->RegisterNative(native_code, false);
}

void* JavaVMExt::FindCodeForNativeMethod(ArtMethod* m) {
......
Thread* const self = Thread::Current();
void* native_method = libraries_->FindNativeMethod(self, m, detail);
......
return native_method;
}

void* FindNativeMethodInternal(Thread* self,
void* declaring_class_loader_allocator,
const char* shorty,
const std::string& jni_short_name,
const std::string& jni_long_name)
REQUIRES(!Locks::jni_libraries_lock_)
REQUIRES(!Locks::mutator_lock_) {
MutexLock mu(self, *Locks::jni_libraries_lock_);
// 遍历加载的so去查找函数
for (const auto& lib : libraries_) {
SharedLibrary* const library = lib.second;
// Use the allocator address for class loader equality to avoid unnecessary weak root decode.
if (library->GetClassLoaderAllocator() != declaring_class_loader_allocator) {
// We only search libraries loaded by the appropriate ClassLoader.
continue;
}
// Try the short name then the long name...
const char* arg_shorty = library->NeedsNativeBridge() ? shorty : nullptr;
void* fn = library->FindSymbol(jni_short_name, arg_shorty);
if (fn == nullptr) {
fn = library->FindSymbol(jni_long_name, arg_shorty);
}
if (fn != nullptr) {
VLOG(jni) << "[Found native code for " << jni_long_name
<< " in \"" << library->GetPath() << "\"]";
return fn;
}
}
return nullptr;
}

void* FindSymbol(const std::string& symbol_name, const char* shorty = nullptr)
REQUIRES(!Locks::mutator_lock_) {
return NeedsNativeBridge()
? FindSymbolWithNativeBridge(symbol_name.c_str(), shorty)
: FindSymbolWithoutNativeBridge(symbol_name.c_str());
}
void* FindSymbolWithoutNativeBridge(const std::string& symbol_name)
REQUIRES(!Locks::mutator_lock_) {
CHECK(!NeedsNativeBridge());
return dlsym(handle_, symbol_name.c_str());
}

jni_short_name:是本地方法的短名,它的格式通常是 Java_<类的完全限定名><方法名>,其中类名和方法名中的每个字符(除了字母和数字)都被替换为 。 例如: Java_com_example_MyClass_myMethod
jni_long_name:是本地方法的长名,它的格式通常是 Java
<类的完全限定名>
<方法名>__<参数签名>,其中参数签名部分用 _ 分隔每个参数类型的描述符。 例如: Java_com_example_MyClass_myMethod__ILjava_lang_String_2

通过FindCodeForNativeMethod找到的函数地址后,会调用 RegisterNative 才把它的地址绑定到artmethod里面,method->RegisterNative(native_code, false);
回到上面的汇编代码

1
2
3
blx    artFindNativeMethod
mov r12, r0
bx r12

经过artFindNativeMethod查到绑定后的artmethod地址返回
接下来就可以对上面的一些流程添加log打印,在artFindNativeMethod下可以监控静态注册jni函数绑定的流程

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
#if defined(__arm__) || defined(__aarch64__)
extern "C" const void* artFindNativeMethod() {
Thread* self = Thread::Current();
#else
extern "C" const void* artFindNativeMethod(Thread* self) {
DCHECK_EQ(self, Thread::Current());
#endif
Locks::mutator_lock_->AssertNotHeld(self); // We come here as Native.
ScopedObjectAccess soa(self);

ArtMethod* method = self->GetCurrentMethod(nullptr);
DCHECK(method != nullptr);

// Lookup symbol address for method, on failure we'll return null with an exception set,
// otherwise we return the address of the method we found.
void* native_code = soa.Vm()->FindCodeForNativeMethod(method);
if (native_code == nullptr) {
self->AssertPendingException();
return nullptr;
} else {
//add
std::stringstream oss;
oss << "[artFindNativeMethod]StaticRegisterJniMethod->" << method->PrettyMethod() << ", add: " << native_code;
const char *content = oss.str().c_str();
if (strstr(content, "zsklog") != nullptr) {
LOG(ERROR) << content;
}
//add
}
// Register so that future calls don't come here
return method->RegisterNative(native_code, false);
}

上面添加了标识 strstr(content, “zsklog”) 用来让frida可以控制日志打印开关

动态注册的jni函数

动态注册的jni函数都是通过 RegisterNatives 去注册绑定地址,定位到源码的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static jint RegisterNatives(JNIEnv* env,
jclass java_class,
const JNINativeMethod* methods,
jint method_count) {
......
CHECK_NON_NULL_ARGUMENT_FN_NAME("RegisterNatives", methods, JNI_ERR);
for (jint i = 0; i < method_count; ++i) {
const char* name = methods[i].name;
const char* sig = methods[i].signature;
const void* fnPtr = methods[i].fnPtr;
bool is_fast = false;
......
const void* final_function_ptr = m->RegisterNative(fnPtr, is_fast);
UNUSED(final_function_ptr);
}
return JNI_OK;
}

遍历methods,最后跟静态注册jni函数的流程一样去调用 RegisterNative ,静态注册的log已经添加,那在 RegisterNative 里添加log不就能跟踪到动态注册的函数,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const void* ArtMethod::RegisterNative(const void* native_method, bool is_fast) {
CHECK(IsNative()) << PrettyMethod();
CHECK(!IsFastNative()) << PrettyMethod();
CHECK(native_method != nullptr) << PrettyMethod();
if (is_fast) {
AddAccessFlags(kAccFastNative);
}
void* new_native_method = nullptr;
Runtime::Current()->GetRuntimeCallbacks()->RegisterNativeMethod(this,
native_method,
/*out*/&new_native_method);
//add
std::stringstream oss;
oss << "[ArtMethod::RegisterNative]JniMethod->" << this->PrettyMethod() << ", add: " << native_method;
const char *content = oss.str().c_str();
if (strstr(content, "zsklog") != nullptr) {
LOG(ERROR) << content;
}
//add
SetEntryPointFromJni(new_native_method);
return new_native_method;
}
jni函数的绑定分类总结
  1. 静态函数的jni函数:有两次绑定,第一次是在类加载时,会绑定到ART中的“桥函数”(art_jni_dlsym_lookup_stub) ,第二次绑定是在该jni函数第一次被真正调用时,ART 会通过桥函数查找该函数在本地库中的实际地址,并进行绑定。后续调用将直接使用该地址,避免重复查找。
    LoadClass -> LoadClassMembers -> SetEntryPointFromJni(GetJniDlsymLookupStub() ->art_jni_dlsym_lookup_stub -> artFindNativeMethod -> FindCodeForNativeMethod(method)
  2. 动态注册的jni函数:也是有两次绑定,只不过和静态注册的jni函数的第二次绑定不同之处在于是由开发人员编写的代码主动调用RegeisterNatives进行绑定,而不是由ART“帮助”完成绑定。
    重新编写后刷入,frida代码
    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
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    function traceJniRegiseter(){
    var libc_module = Process.getModuleByName("libc.so");
    var strstr = libc_module.getExportByName("strstr");
    Interceptor.attach(strstr, {
    onEnter: function(args) {
    this.thread_id = Process.getCurrentThreadId();
    this.arg0 = ptr(args[0]).readUtf8String();
    this.arg1 = ptr(args[1]).readUtf8String();
    }, onLeave: function(retval) {
    if (this.arg1 == "zsklog") {
    console.log("[" + this.thread_id + "]" + "go into OpenNativeLibrary -> " + this.arg0);
    retval.replace(0x100);
    }
    }
    })
    }

    function tracesoload(){
    console.log(Process.arch);
    var libnativeloaderModule = Process.getModuleByName("libnativeloader.so");
    // OpenNativeLibrary
    var OpenNativeLibraryAddr = libnativeloaderModule.getExportByName("_ZN7android17OpenNativeLibraryEP7_JNIEnviPKcP8_jobjectP8_jstringPbPNSt3__112basic_stringIcNS9_11char_traitsIcEENS9_9allocatorIcEEEE");
    Interceptor.attach(OpenNativeLibraryAddr, {
    onEnter: function(args){
    this.sopath = ptr(args[2]).readUtf8String();
    this.thread_id = Process.getCurrentThreadId();
    console.log("[" + this.thread_id + "]" + "go into OpenNativeLibrary -> " + this.sopath);
    }, onLeave: function(retval) {
    console.log("[" + this.thread_id + "]" + "leave OpenNativeLibrary -> " + this.sopath, ", return: " + retval);
    }
    })

    var dlsymAddr = Module.findExportByName("libc.so", "dlsym");
    Interceptor.attach(dlsymAddr, {
    onEnter: function(args){
    this.handle = args[0];
    this.symbol = ptr(args[1]).readUtf8String();
    this.thread_id = Process.getCurrentThreadId();
    }, onLeave: function(retval){
    console.log("[" + this.thread_id + "]" + "leave dlsym: handle: " + this.handle + ", symbol: " + this.symbol, ", return: " + retval);
    }
    })
    }

    function main(){
    tracesoload();
    traceJniRegiseter();
    }

    setImmediate(main);
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    arm64
    [8007]leave dlsym: handle: 0x2adcc85529763b0b, symbol: oatdata , return: 0x7d1659d000
    [8007]leave dlsym: handle: 0x2adcc85529763b0b, symbol: oatlastword , return: 0x7d1659e224
    [8007]leave dlsym: handle: 0x2adcc85529763b0b, symbol: oatbss , return: 0x7d1659f000
    [8007]leave dlsym: handle: 0x2adcc85529763b0b, symbol: oatbsslastword , return: 0x7d165a2acc
    [8007]leave dlsym: handle: 0x2adcc85529763b0b, symbol: oatbssmethods , return: 0x0
    [8007]leave dlsym: handle: 0x2adcc85529763b0b, symbol: oatbssroots , return: 0x0
    [8007]go into OpenNativeLibrary -> /data/app/com.iCitySuzhou.suzhou001-IqFG3pwpI6PxX176DYv1tw==/lib/arm64/libshell-super.com.iCitySuzhou.suzhou001.so
    [8007]leave dlsym: handle: 0xa5c5ee03e19145b9, symbol: mmap , return: 0x7db163acd4
    [8007]leave dlsym: handle: 0xa5c5ee03e19145b9, symbol: write , return: 0x7db163b4cc
    [8007]leave dlsym: handle: 0xa5c5ee03e19145b9, symbol: close , return: 0x7db15ed138
    [8007]leave dlsym: handle: 0xa5c5ee03e19145b9, symbol: fdatasync , return: 0x7db163a8b4
    [8007]leave OpenNativeLibrary -> /data/app/com.iCitySuzhou.suzhou001-IqFG3pwpI6PxX176DYv1tw==/lib/arm64/libshell-super.com.iCitySuzhou.suzhou001.so , return: 0x5cd2a12d5c2fc103
    [8007]leave dlsym: handle: 0x5cd2a12d5c2fc103, symbol: JNI_OnLoad , return: 0x7d160c6a5c
    [8007]go into OpenNativeLibrary -> [ArtMethod::RegisterNative]JniMethod->void com.wrapper.proxyapplication.WrapperProxyApplication.Ooo0ooO0oO(), add: 0x7d160c8418
    [8007]go into OpenNativeLibrary -> [ArtMethod::RegisterNative]JniMethod->int com.wrapper.proxyapplication.CustomerClassLoader.ShowLogs(java.lang.String, int), add: 0x7d160c648c
    ......
 评论