首页
社区
课程
招聘
[原创]一种基于JDWP动态注入代码的方案
发表于: 2022-2-19 17:14 26828

[原创]一种基于JDWP动态注入代码的方案

2022-2-19 17:14
26828

在逆向开发中,一般都需要对目标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调试接口(调试者)。调试者定义了用户可以使用的调试接口,用户可以通过这些接口对被调试虚拟机发送调试命令,同时显示调试结果。

JPDA整体架构
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期)

收藏
免费 7
支持
分享
最新回复 (5)
雪    币: 4752
活跃值: (2923)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
2
优秀的作品
2022-2-21 09:47
0
雪    币: 457
活跃值: (2793)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
mark
2022-2-23 08:26
0
雪    币: 2907
活跃值: (5495)
能力值: ( LV11,RANK:185 )
在线值:
发帖
回帖
粉丝
4
技术是不错,但是,这个实际运用有点鸡肋啊
2022-2-23 23:23
0
雪    币: 916
活跃值: (3434)
能力值: ( LV8,RANK:120 )
在线值:
发帖
回帖
粉丝
5

道理我都懂,但是 frida 早就写好了 

https://github.com/frida/frida-core/blob/main/src/droidy/injector.vala

https://github.com/frida/frida-core/blob/main/src/droidy/jdwp.vala

最后于 2022-2-24 01:16 被葫芦娃编辑 ,原因:
2022-2-24 01:15
1
雪    币: 116
活跃值: (1012)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
思路确实挺不错 很值得学习 但如果apk都能重打包了 也就不需要这些麻烦的操作了
2022-2-24 11:07
0
游客
登录 | 注册 方可回帖
返回
//