博客原文:Android Root环境下动态注入Java和Native代码的实践
在Android逆向开发中,我们通常会使用Frida工具在命令行中动态注入JavaScript代码到目标应用,编写JavaScript对Android新手来说可能会有些困难,假如能用Java代码Hook Java层方法,c/c++代码Hook native层函数指令,用起来可能会更顺手。 在Android正向开发中,我们往往需要在Release包上进行性能诊断或复杂问题的分析,然而,这并不是一件容易的事情。原因在于,Release包通常不方便调试,大型App的编译过程需要消耗大量时间,修改框架或第三方SDK代码也相对困难。因此,有时候我们就需要利用逆向工具,比如Xposed或者Frida,通过代码注入的方式来进行问题调试。 那么,如何实现在命令行中将Xposed插件动态地注入到目标应用中呢? 本文将探讨在Android Root设备上将Xposed插件动态注入到目标应用的一种实践。
实现过程主要包括以下几部分:
具体的思路如下:
以上介绍了工具的实现思路,下面详细介绍其实现过程。 整体的流程图如下: 整体功能分为三层:
注入层的目的是将一个二进制可执行文件注入到目标进程中并运行其中的函数; linux平台上,使用ptrace进行进程注入的技术方案已经非常成熟,刚好笔者之前专门研究过,并开源了一个相对稳定可靠的android平台的ptrace注入库:XInjector 这里,直接使用这个仓库编译出来的动态可执行文件,即可轻松实现将动态库注入到指定应用中。
胶水层主要是完成dex和so文件合并到App主ClassLoader中,并根据合并后的ClassLoader调用dex文件中的入口方法。 这一层最大的难点是:如何在native世界撬开Java世界的大门。 核心流程包含:
这一部分三个步骤来讲解。
执行插件加载层初始化的时机选择是本方案面临的主要挑战之一,原因如下:
对此,Frida库在开发过程中可能也会遇到类似的问题。它的解决方案是在注入代码时创建一个子线程来运行hook代码,从而成功地绕过了第三个问题。然而,由于代码是在子线程中执行的,这使得执行时机具有一定的不确定性。这可能会导致注入时机稍微延后,结果会导致一些方法被hook前可能已经被执行。 基于以上分析,运行插件加载层的初始化逻辑选择以下三个时机:
如果ptrace注入到无法使用JNIEnv指针的jni方法中,使用ALooper将初始化逻辑发送到主线程Handler中执行,其缺点是,执行hook逻辑时机较晚,此时Application的attachBaseContext和onCreate已经执行完成,但可以作为一种兜底方案;
如果ptrace注入时机非常早,此时ActivityThread的mLoadedApk成员还未创建出来,无法进入Java世界,因此,需要延迟初始化。这里选择使用 plt hook libcutils中atrace_update_tags
函数,在此函数中执行初始化逻辑,选择这个函数是因为它在启动时只执行一次,并且此时应用的mLoadedApk已经被创建;
非以上两种情况的话,直接在Ptrace注入的流程中执行初始化逻辑,完成dex和so合并到App的主ClassLoader,并调用dex中的入口方法; 伪代码如下:
虽然在主线程中执行代码注入能确保其发生的时机足够早,但在实际应用中,我们发现这个方案会导致注入后的应用启动崩溃或者卡死的概率非常高。这种现象可能是由于虚拟机的限制所导致,虚拟机的某些 JNI 函数内部无法运行 Java 代码。 因此,我们最终还是默认选择使用 Frida 的解决方案,在子线程中初始化和执行插件的入口方法。 这应该是一种更为稳定和可靠的方案。 代码如下:
ptrace成功后,就可以执行我们的native代码,但为了能加载外置插件,我们需要使用Java代码来实现。为了能进入Java世界,需要在native层构造相关Classloader然后调Java层入口方法。 一般来说,使用Classloader加载Java代码有两种策略:
将dex和so合并到App主ClassLoader之后,使用当前线程的JNIEnv指针反射调用dex文件中的入口类和入口方法时,会抛出类找不到的异常,原因暂不可知。 为了规避这个问题,这里使用一种“破釜沉舟”的办法: 直接使用App的ClassLoader调用loadClass方法加载目标类,然后再通过getDeclaredMethod获取目标方法,最后再调用Method.invoke()执行目标方法。 伪代码如下:
插件加载层的最终产物是Android App工程编译出来的dex文件和so文件。 这个Android工程主要包含基于SandHook的Xposed Api库,包含一个入口方法,用于初始化SandHook以及加载Xposed插件Apk。 核心逻辑有:
由于SandHook库的初始化时需要传入context对象,但由于插件加载层被注入的时机很早,此时的应用的Application和App Context对象很可能并未创建出来,因此,需要自行创建一个应用的context对象。 ContextImpl类中有一个方法createAppContext可以用于创建context对象,参数包含两个,一个是当前进程的ActivityThread对象,另一个是LoadedApk对象。前者是个单例,通过反射很容易得到,后者是保存在ActivityThread的mBoundApplication成员变量中,也可以反射获取。 完整的构造context代码如下:
为了实现在xposed插件中自动加载native代码,我们需对插件内的动态so库进行特别处理。 具体的处理方式是:先将插件APK中的so文件提取到指定的目录下,然后在构造插件的DexClassLoader
时,将so文件的目录传入。这样,插件Apk在调用System.loadLibrary()
时就可以成功加载插件的so库了。 在整个流程中,关键环节在于提取插件Apk中的so文件。 最常见的提取方式是首先解压apk压缩包,然后复制解压后的so文件到指定的目录下。 如果你对Android系统源码进行过详细了解,会发现源码中已经集成了提取Apk文件中so的相关功能。这个功能是由NativeLibraryHelper.java
这个工具类提供的。 该工具主要用途是,在安装Apk过程中,如果Apk的manifest文件中配置了android:extractNativeLibs = true
,系统会自动将apk文件中的so文件提取到指定目录下。然后在app启动时,构造App的ClassLoader并将该目录作为参数传入,从而实现native库的自动加载。
提取so文件的源码如下:com/android/internal/content/NativeLibraryHelper.java
这里,只需要先反射构造一个Handle对象,然后再反射调用copyNativeBinaries这个方法即可实现将so文件提取到指定目录下。
加载插件Apk的逻辑比较简单,参考Xposed原生代码的实现即可。 主要分为两个步骤:
至此,完成插件Apk的加载。
下面是最终产物文件目录结构,以及每个文件的用途:
下载zip压缩包 并解压;
打开命令行,cd到解压后的目录下;
执行以下命令,即可实现重启目标app,并将xposed插件注入到目标app进程中:
启动的App是: com.android.settings 注入的xposed插件apk路径是:xposed_module_sample.apk
性能模式:使用参数 -q
默认情况下,会拷贝glue和plugin_loader目录下的dex和so文件到目标app的data/data/目录下,使用-q后便不拷贝这些文件,对同一个app进行第二次注入时使用此参数,可以提升注入性能。
另外,如果对同一app第二次注入相同的xposed插件时,也可以省-f xposed_module_sample.apk
参数,此时会使用上次注入过的插件。 使用-h参数: 可在控制台输出帮助信息:
控制台输出结果:
由于本工具实现比较仓促,目前还存在一些短期内无法修复的问题,主要有:
https://github.com/WindySha/Poros 有能力的可提交PR,欢迎共建。
写这个工具的动机是,笔者的工作是做某大型App的底层优化,经常要改ART虚拟机或者其他系统动态库的一些功能,由于大型App编译安装太耗时,通过这种注入的方式进行测试和开发确实能提升效率,因此就顺手撸了这个工具。 其实很久之前就写好了这个工具,最开始只是自己用,没打算开源出来,后来想想,如其让代码烂在电脑里,不如开源出来,说不定能给其他人带来一些帮助。 世界因开源而更加美好。
static void OnAtraceFuncCalled() {
void
*
current_thread_ptr
=
runtime::CurrentThreadFunc();
JNIEnv
*
env
=
runtime::GetJNIEnvFromThread(current_thread_ptr);
if
(!env) {
LOGE(
"Failed to get JNIEnv, Inject Xposed Module failed."
);
return
;
}
InjectXposedLibraryInternal(env);
}
int
DoInjection(JNIEnv
*
env) {
runtime::InitRuntime();
void
*
current_thread_ptr
=
nullptr;
if
(!env) {
current_thread_ptr
=
runtime::CurrentThreadFunc();
env
=
runtime::GetJNIEnvFromThread(current_thread_ptr);
}
if
(!env) {
LOGE(
"Failed to get JNIEnv !!"
);
return
-
1
;
}
/
/
ptrace到JNI方法Java_java_lang_Object_wait时,会出现由于等锁导致的env
-
>FindClass卡死的问题,这里Handler中加载xposed模块
/
/
ptrace到JNIT方法Java_com_android_internal_os_ClassLoaderFactory_createClassloaderNamespace时,正在构造classLoader此时调用FindClass会卡死
/
/
这里绕过这两个方法,发送任务到主线程Handler执行;
void
*
art_method
=
runtime::GetCurrentMethod(current_thread_ptr, false, false);
if
(art_method !
=
nullptr) {
std::string name
=
runtime::JniShortName(art_method);
if
(strcmp(name.c_str(),
"Java_java_lang_Object_wait"
)
=
=
0
|| strcmp(name.c_str(),
"Java_com_android_internal_os_ClassLoaderFactory_createClassloaderNamespace"
)
=
=
0
) {
/
/
load xposed modules after
in
the main message handler, this
is
later than application's attachBaseContext
and
onCreate method.
InjectXposedLibraryByHandler(env);
return
0
;
}
}
/
/
If the inject time
is
very early, then, the loadedapk info
and
the app classloader
is
not
ready, so we
try
to hook atrace_set_debuggable function to make sure
/
/
the injection
is
early enough
and
the classloader has also been created.
jobject loaded_apk_obj
=
jni::GetLoadedApkObj(env);
LOGD(
"Try to get the app loaded apk info, loadedapk jobject: %p"
, loaded_apk_obj);
if
(loaded_apk_obj
=
=
nullptr) {
/
/
load xposed modules after atrace_set_debuggable
or
atrace_update_tags
is
called.
HookAtraceFunctions(OnAtraceFuncCalled);
}
else
{
/
/
loadedapk
and
classloader
is
ready, so load the xposed modules directly.
InjectXposedLibraryInternal(env);
}
return
0
;
}
static void OnAtraceFuncCalled() {
void
*
current_thread_ptr
=
runtime::CurrentThreadFunc();
JNIEnv
*
env
=
runtime::GetJNIEnvFromThread(current_thread_ptr);
if
(!env) {
LOGE(
"Failed to get JNIEnv, Inject Xposed Module failed."
);
return
;
}
InjectXposedLibraryInternal(env);
}
int
DoInjection(JNIEnv
*
env) {
runtime::InitRuntime();
void
*
current_thread_ptr
=
nullptr;
if
(!env) {
current_thread_ptr
=
runtime::CurrentThreadFunc();
env
=
runtime::GetJNIEnvFromThread(current_thread_ptr);
}
if
(!env) {
LOGE(
"Failed to get JNIEnv !!"
);
return
-
1
;
}
/
/
ptrace到JNI方法Java_java_lang_Object_wait时,会出现由于等锁导致的env
-
>FindClass卡死的问题,这里Handler中加载xposed模块
/
/
ptrace到JNIT方法Java_com_android_internal_os_ClassLoaderFactory_createClassloaderNamespace时,正在构造classLoader此时调用FindClass会卡死
/
/
这里绕过这两个方法,发送任务到主线程Handler执行;
void
*
art_method
=
runtime::GetCurrentMethod(current_thread_ptr, false, false);
if
(art_method !
=
nullptr) {
std::string name
=
runtime::JniShortName(art_method);
if
(strcmp(name.c_str(),
"Java_java_lang_Object_wait"
)
=
=
0
|| strcmp(name.c_str(),
"Java_com_android_internal_os_ClassLoaderFactory_createClassloaderNamespace"
)
=
=
0
) {
/
/
load xposed modules after
in
the main message handler, this
is
later than application's attachBaseContext
and
onCreate method.
InjectXposedLibraryByHandler(env);
return
0
;
}
}
/
/
If the inject time
is
very early, then, the loadedapk info
and
the app classloader
is
not
ready, so we
try
to hook atrace_set_debuggable function to make sure
/
/
the injection
is
early enough
and
the classloader has also been created.
jobject loaded_apk_obj
=
jni::GetLoadedApkObj(env);
LOGD(
"Try to get the app loaded apk info, loadedapk jobject: %p"
, loaded_apk_obj);
if
(loaded_apk_obj
=
=
nullptr) {
/
/
load xposed modules after atrace_set_debuggable
or
atrace_update_tags
is
called.
HookAtraceFunctions(OnAtraceFuncCalled);
}
else
{
/
/
loadedapk
and
classloader
is
ready, so load the xposed modules directly.
InjectXposedLibraryInternal(env);
}
return
0
;
}
static void InjectXposedLibraryAsync(JNIEnv
*
jni_env) {
JavaVM
*
javaVm;
jni_env
-
>GetJavaVM(&javaVm);
std::thread worker([javaVm]() {
JNIEnv
*
env;
javaVm
-
>AttachCurrentThread(&env, nullptr);
int
count
=
0
;
while
(count <
10000
) {
jobject app_loaded_apk_obj
=
jni::GetLoadedApkObj(env);
/
/
wait here until loaded apk
object
is
available
if
(app_loaded_apk_obj
=
=
nullptr) {
usleep(
100
);
}
else
{
break
;
}
count
+
+
;
}
InjectXposedLibraryInternal(env);
if
(env) {
javaVm
-
>DetachCurrentThread();
}
});
worker.detach();
}
static void InjectXposedLibraryAsync(JNIEnv
*
jni_env) {
JavaVM
*
javaVm;
jni_env
-
>GetJavaVM(&javaVm);
std::thread worker([javaVm]() {
JNIEnv
*
env;
javaVm
-
>AttachCurrentThread(&env, nullptr);
int
count
=
0
;
while
(count <
10000
) {
jobject app_loaded_apk_obj
=
jni::GetLoadedApkObj(env);
/
/
wait here until loaded apk
object
is
available
if
(app_loaded_apk_obj
=
=
nullptr) {
usleep(
100
);
}
else
{
break
;
}
count
+
+
;
}
InjectXposedLibraryInternal(env);
if
(env) {
javaVm
-
>DetachCurrentThread();
}
});
worker.detach();
}
void CallStaticMethodByJavaMethodInvoke(JNIEnv
*
env, const char
*
class_name, const char
*
method_name) {
ScopedLocalRef<jobject> app_class_loader(env, jni::GetAppClassLoader(env));
const char
*
classloader_class_name
=
"java/lang/ClassLoader"
;
const char
*
class_class_name
=
"java/lang/Class"
;
const char
*
method_class_name
=
"java/lang/reflect/Method"
;
/
/
Class<?> clazz
=
appClassLoader.loadClass(
"class_name"
);
ScopedLocalRef<jclass> classloader_jclass(env, env
-
>FindClass(classloader_class_name));
auto loadClass_mid
=
env
-
>GetMethodID(classloader_jclass.get(),
"loadClass"
,
"(Ljava/lang/String;)Ljava/lang/Class;"
);
ScopedLocalRef<jstring> class_name_jstr(env, env
-
>NewStringUTF(class_name));
ScopedLocalRef<jobject> clazz_obj(env, env
-
>CallObjectMethod(app_class_loader.get(), loadClass_mid, class_name_jstr.get()));
/
/
get Java Method mid: Class.getDeclaredMethod()
ScopedLocalRef<jclass> class_jclass(env, env
-
>FindClass(class_class_name));
auto getDeclaredMethod_mid
=
env
-
>GetMethodID(class_jclass.get(),
"getDeclaredMethod"
,
"(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;"
);
/
/
Get the Method
object
ScopedLocalRef<jstring> method_name_jstr(env, env
-
>NewStringUTF(method_name));
jvalue args[]
=
{ {.l
=
method_name_jstr.get()},{.l
=
nullptr} };
ScopedLocalRef<jobject> method_obj(env, env
-
>CallObjectMethodA(clazz_obj.get(), getDeclaredMethod_mid, args));
ScopedLocalRef<jclass> method_jclass(env, env
-
>FindClass(method_class_name));
/
/
get Method.invoke jmethodId
auto invoke_mid
=
env
-
>GetMethodID(method_jclass.get(),
"invoke"
,
"(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;"
);
/
/
Call Method.invoke()
jvalue args2[]
=
{ {.l
=
nullptr},{.l
=
nullptr} };
env
-
>CallObjectMethodA(method_obj.get(), invoke_mid, args2);
}
void CallStaticMethodByJavaMethodInvoke(JNIEnv
*
env, const char
*
class_name, const char
*
method_name) {
ScopedLocalRef<jobject> app_class_loader(env, jni::GetAppClassLoader(env));
const char
*
classloader_class_name
=
"java/lang/ClassLoader"
;
const char
*
class_class_name
=
"java/lang/Class"
;
const char
*
method_class_name
=
"java/lang/reflect/Method"
;
/
/
Class<?> clazz
=
appClassLoader.loadClass(
"class_name"
);
ScopedLocalRef<jclass> classloader_jclass(env, env
-
>FindClass(classloader_class_name));
auto loadClass_mid
=
env
-
>GetMethodID(classloader_jclass.get(),
"loadClass"
,
"(Ljava/lang/String;)Ljava/lang/Class;"
);
ScopedLocalRef<jstring> class_name_jstr(env, env
-
>NewStringUTF(class_name));
ScopedLocalRef<jobject> clazz_obj(env, env
-
>CallObjectMethod(app_class_loader.get(), loadClass_mid, class_name_jstr.get()));
/
/
get Java Method mid: Class.getDeclaredMethod()
ScopedLocalRef<jclass> class_jclass(env, env
-
>FindClass(class_class_name));
auto getDeclaredMethod_mid
=
env
-
>GetMethodID(class_jclass.get(),
"getDeclaredMethod"
,
"(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;"
);
/
/
Get the Method
object
ScopedLocalRef<jstring> method_name_jstr(env, env
-
>NewStringUTF(method_name));
jvalue args[]
=
{ {.l
=
method_name_jstr.get()},{.l
=
nullptr} };
ScopedLocalRef<jobject> method_obj(env, env
-
>CallObjectMethodA(clazz_obj.get(), getDeclaredMethod_mid, args));
ScopedLocalRef<jclass> method_jclass(env, env
-
>FindClass(method_class_name));
/
/
get Method.invoke jmethodId
auto invoke_mid
=
env
-
>GetMethodID(method_jclass.get(),
"invoke"
,
"(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;"
);
/
/
Call Method.invoke()
jvalue args2[]
=
{ {.l
=
nullptr},{.l
=
nullptr} };
env
-
>CallObjectMethodA(method_obj.get(), invoke_mid, args2);
}
public static Context createAppContext() {
/
/
ContextImpl appContext
=
ContextImpl.createAppContext(mActivityThread, mLoadedApk);
try
{
Class activityThreadClass
=
Class.forName(
"android.app.ActivityThread"
);
Method currentActivityThreadMethod
=
activityThreadClass.getDeclaredMethod(
"currentActivityThread"
);
currentActivityThreadMethod.setAccessible(true);
Object
activityThreadObj
=
currentActivityThreadMethod.invoke(null);
Field boundApplicationField
=
activityThreadClass.getDeclaredField(
"mBoundApplication"
);
boundApplicationField.setAccessible(true);
Object
mBoundApplication
=
boundApplicationField.get(activityThreadObj);
/
/
AppBindData
Field infoField
=
mBoundApplication.getClass().getDeclaredField(
"info"
);
/
/
info
infoField.setAccessible(true);
Object
loadedApkObj
=
infoField.get(mBoundApplication);
/
/
LoadedApk
Class contextImplClass
=
Class.forName(
"android.app.ContextImpl"
);
Method createAppContextMethod
=
contextImplClass.getDeclaredMethod(
"createAppContext"
, activityThreadClass, loadedApkObj.getClass());
createAppContextMethod.setAccessible(true);
Object
context
=
createAppContextMethod.invoke(null, activityThreadObj, loadedApkObj);
if
(context instanceof Context) {
return
(Context) context;
}
} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException | NoSuchFieldException e) {
e.printStackTrace();
}
return
null;
}
public static Context createAppContext() {
/
/
ContextImpl appContext
=
ContextImpl.createAppContext(mActivityThread, mLoadedApk);
try
{
Class activityThreadClass
=
Class.forName(
"android.app.ActivityThread"
);
Method currentActivityThreadMethod
=
activityThreadClass.getDeclaredMethod(
"currentActivityThread"
);
currentActivityThreadMethod.setAccessible(true);
Object
activityThreadObj
=
currentActivityThreadMethod.invoke(null);
Field boundApplicationField
=
activityThreadClass.getDeclaredField(
"mBoundApplication"
);
boundApplicationField.setAccessible(true);
Object
mBoundApplication
=
boundApplicationField.get(activityThreadObj);
/
/
AppBindData
Field infoField
=
mBoundApplication.getClass().getDeclaredField(
"info"
);
/
/
info
infoField.setAccessible(true);
Object
loadedApkObj
=
infoField.get(mBoundApplication);
/
/
LoadedApk
Class contextImplClass
=
Class.forName(
"android.app.ContextImpl"
);
Method createAppContextMethod
=
contextImplClass.getDeclaredMethod(
"createAppContext"
, activityThreadClass, loadedApkObj.getClass());
createAppContextMethod.setAccessible(true);
Object
context
=
createAppContextMethod.invoke(null, activityThreadObj, loadedApkObj);
if
(context instanceof Context) {
return
(Context) context;
}
} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException | NoSuchFieldException e) {
e.printStackTrace();
}
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!