在逆向开发中,一般都需要对目标App进行代码注入。主流的代码注入工具是Frida,这个工具能稳定高效实现java代码hook和native代码hook,不过缺点是需要使用Root设备,而且用js开发,入门门槛较高。最近发现一种非Root环境下对Debug App进行代码注入的方案,原理是利用Java调试框架,通过调试器与目标虚拟机之间通讯,实现对虚拟机进程的修改。
Java SE从1.2.2版本以后推出了JPDA框架(Java Platform Debugger Architecture,Java平台调试体系结构)。JPDA定义了一套独立且完整的调试体系,它由三个相对独立的模块组成,分别为:
JDI:Java Debug Interface,Java调试接口(调试者)。调试者定义了用户可以使用的调试接口,用户可以通过这些接口对被调试虚拟机发送调试命令,同时显示调试结果。
Reference: https://docs.oracle.com/javase/7/docs/technotes/guides/jpda/architecture.html
其中,JDWP协议是用于调试器与目标虚拟机之间进行调试交互的通信协议。
JDWP 大致分为两个阶段:握手和应答。握手是在传输层连接建立完成后,做的第一件事:
调试器发送 14 bytes 的字符串“JDWP-Handshake”到目标虚拟机,虚拟机回复“JDWP-Handshake”,从而完成握手。
握手完成后,调试器就可以向虚拟机发送命令了。JDWP 是通过命令(command)和回复(reply)进行通信,这与 HTTP 有些相似。JDWP 本身是无状态的,因此对 command 出现的顺序并不受限制。
JDWP 有两种基本的包(packet)类型:命令包(command packet)和回复包(reply packet)。
调试器和目标虚拟机都有可能发送 command packet。调试器通过发送 command packet 获取虚拟机的信息以及控制程序的执行。虚拟机通过发送 command packet 通知调试器某些事件的发生,如到达断点或是产生异常。
Reply packet 是用来回复 command packet 该命令是否执行成功,如果成功 reply packet 还有可能包含 command packet 请求的数据,比如当前的线程信息或者变量的值。从虚拟机发送的事件消息是不需要回复的。
数据包部分JDWP协议按照功能大致分为18组命令,包含了虚拟机、引用类型、对象、线程、方法、堆栈、事件等不同类型的操作命令。
ART虚拟机对JDWP协议的支持基本是完整的,具体信息可以参考ART-JDWP中所支持的消息。
JDWP协议内容比较多,要自行实现协议内容工作量还是比较大。庆幸的是,国外已有大神将JDWP协议大部分用python实现好了,我们只需要直接使用即可,非常方便。
python实现的jdwp协议源码地址:jdwp-shellifier
既然利用JDWP可以让调试器跟虚拟机进行交互,我们可以通过调用基于JDWP协议的相关接口,向虚拟机进程中注入代码。假如只是注入c/c++代码的话,实现起来很轻松,我们在App进程启动时加上一个断点,在断点处执行加载so的代码即可, 流程如下:
这样native代码就注入到了目标App中。
通过jdwp协议封装的接口可以实现java代码的注入,通过这种方式注入少量Java代码还比较轻松,大量java代码都用jdwp来实现,难度将会非常大。
我们可以将java代码编译成的是dex文件,然后用c/c++实现dex文件的加载以及dex方法的执行,便可实现java代码的注入。
在插件化开发中,加载dex文件大致有两种方案,一种是多ClassLoader方案,一种是单ClassLoader方案。多ClassLoader方案就是根据插件dex路径,每个插件构造自己的DexClassLoader
,然后用这个classLoader加载插件中的类。单ClassLoader就是将插件的ClassLoader里的Element合并到App的ClassLoader中,然后使用App的ClassLoader来加载插件里的类。
这里我们选择单ClassLoader的方案,具体步骤如下:
以上流程需要用c/c++来实现。
为了在注入的代码中更方便地修改被注入App Java代码,我们希望注入的代码能够给App代码加钩子。因此,在注入代码中接入了稳定性较好的一个Android Art Hook库: SandHook。
接入这个Hook库的方法有两种:
方案二优势更明显,因此这里采用了方案二来实现。
最终,在利用JDWP协议注入的so文件中,需要实现以下功能:
整体流程大致如此,不过其中还有不少细节需要处理,比如,SandHook初始化时需要传App Context,但是我们这个注入流程是在LoadedApk.makeApplication之前,此时App的Context并没有创建出来,因此,需要通过反射主动构造出一个Context对象:
还有,为了能够加载Xposed插件中的so库,在加载插件Apk之前,需要将Apk中的so文件拷贝到data/data目录下,并将so路径传给DexClassLoader
构造方法的最后一个参数。为了更高效地拷贝so,这里反射调用了Framework里的内部类NativeLibraryHelper
。App安装时的so拷贝就是用NativeLibraryHelper
实现的,具体拷贝操作在native层完成,效率更高。
另外,Android9及以上的系统限制了App对隐藏Api的调用。我们可以在注入so的JNI_Onload函数中加入以下代码,便可简单绕过这种限制:
以上流程的完整实现已经上传到github上:jdwp-xposed-injector
使用方法:
最终,我们在Android设备上启动了目标App,并且Xposed插件Apk中的代码被注入到目标App中。
本工具唯一的要求是App必须是Debuggable的,那么如何让一个App变成Debuggable的?大致总结了以下几种方式:
Runtime.getRuntime().load(
"data/data/package_name/libnative_injecter.so"
)
Runtime.getRuntime().load(
"data/data/package_name/libnative_injecter.so"
)
LoadedApk loadedApk
=
ActivityThread.currentActivityThread().mBoundApplication.info;
ContextImpl appContext
=
ContextImpl.createAppContext(activityThread, loadedApk);
LoadedApk loadedApk
=
ActivityThread.currentActivityThread().mBoundApplication.info;
ContextImpl appContext
=
ContextImpl.createAppContext(activityThread, loadedApk);
static void BypassHiddenApi(JNIEnv
*
env) {
jclass vmRumtime_class
=
env
-
>FindClass(
"dalvik/system/VMRuntime"
);
void
*
getRuntime_art_method
=
env
-
>GetStaticMethodID(vmRumtime_class,
"getRuntime"
,
"()Ldalvik/system/VMRuntime;"
);
jobject vmRuntime_instance
=
env
-
>CallStaticObjectMethod(vmRumtime_class, (jmethodID)getRuntime_art_method);
jstring mystring
=
env
-
>NewStringUTF(
"L"
);
jclass
cls
=
env
-
>FindClass(
"java/lang/String"
);
jobjectArray jarray
=
env
-
>NewObjectArray(
1
,
cls
, nullptr);
env
-
>SetObjectArrayElement(jarray,
0
, mystring);
void
*
setHiddenApiExemptions_art_method
=
env
-
>GetMethodID(vmRumtime_class,
"setHiddenApiExemptions"
,
"([Ljava/lang/String;)V"
);
env
-
>CallVoidMethod(vmRuntime_instance, (jmethodID)setHiddenApiExemptions_art_method, jarray);
}
static void BypassHiddenApi(JNIEnv
*
env) {
jclass vmRumtime_class
=
env
-
>FindClass(
"dalvik/system/VMRuntime"
);
void
*
getRuntime_art_method
=
env
-
>GetStaticMethodID(vmRumtime_class,
"getRuntime"
,
"()Ldalvik/system/VMRuntime;"
);
jobject vmRuntime_instance
=
env
-
>CallStaticObjectMethod(vmRumtime_class, (jmethodID)getRuntime_art_method);
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)