一种基于JDWP动态注入代码的方案

前言

在逆向开发中,一般都需要对目标App进行代码注入。主流的代码注入工具是Frida,这个工具能稳定高效实现java代码hook和native代码hook,不过缺点是需要使用Root设备,而且用js开发,入门门槛较高。最近发现一种非Root环境下对Debug App进行代码注入的方案,原理是利用Java调试框架,通过调试器与目标虚拟机之间通讯,实现对虚拟机进程的修改。

JPDA框架和JDWP协议

Java SE从1.2.2版本以后推出了JPDA框架(Java Platform Debugger Architecture,Java平台调试体系结构)。JPDA定义了一套独立且完整的调试体系,它由三个相对独立的模块组成,分别为:

  • JVM TI:Java虚拟机工具接口(被调试者)。被调试者运行在我们想要调试的虚拟机上,它可以通过JVM TI这个标准接口监控当前虚拟机的信息。
  • JDWP:Java Debug Wire Protocol,Java调试协议(通道)。在调试者和被调试者之间,通过JDWP传输层传输消息。
  • 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协议内容比较多,要自行实现协议内容工作量还是比较大。庆幸的是,国外已有大神将JDWP协议大部分用python实现好了,我们只需要直接使用即可,非常方便。

python实现的jdwp协议源码地址:jdwp-shellifier

基于JDWP的代码注入方案

Native代码注入

既然利用JDWP可以让调试器跟虚拟机进行交互,我们可以通过调用基于JDWP协议的相关接口,向虚拟机进程中注入代码。假如只是注入c/c++代码的话,实现起来很轻松,我们在App进程启动时加上一个断点,在断点处执行加载so的代码即可, 流程如下:

  1. 利用JDWP的命令,在App进程启流程的某个方法中添加断点,使得App启动前能执行到我们注入的指令。为了使注入的代码尽早执行,这里选择android.app.LoadedApk.makeApplication方法处加断点;
  2. 在断点处通过jdwp协议执行下面的Java代码:
Runtime.getRuntime().load("data/data/package_name/libnative_injecter.so")
  1. 注入的so加载完成后,继续App的启动流程;

这样native代码就注入到了目标App中。

Java代码注入

通过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的方案,具体步骤如下:

  1. 根据插件dex或者Apk路径,反射调用DexPathList.javamakePathElements静态方法,构造出来一个用于类加载的Element数组;
  2. 获取App的Classloader,这是一个BaseDexClassLoader对象,反射获取成员变量pathList,其类型时DexPathList,再反射获取成员变量dexElements,得到一个Element数组;
  3. 将第一步获取到的Element数组合并到第二步获取到的dexElements对象对应的Element数组中;
  4. 最后用App的classLoader加载插件Apk中的类,并执行插件入口方法;

以上流程需要用c/c++来实现。

Xposed模块加载器的注入

为了在注入的代码中更方便地修改被注入App Java代码,我们希望注入的代码能够给App代码加钩子。因此,在注入代码中接入了稳定性较好的一个Android Art Hook库: SandHook

接入这个Hook库的方法有两种:

  1. 方法一:在每个需要动态注入的插件工程中接入SandHook,然后在插件工程中使用Xposed Api来Hook Java代码;
  2. 方法二:将Xposed Api的SandHook所有Java代码(dex文件)和Native代码(so文件)注入到目标App中,并增加加载Xposed插件的相关逻辑。注入的插件工程就可以按照一个Xposed Module工程模式来开发,这样能显著降低插件工程的接入成本。

方案二优势更明显,因此这里采用了方案二来实现。

最终,在利用JDWP协议注入的so文件中,需要实现以下功能:

  1. 将加载Xposed插件的功能编译出dex文件,用dex构造出来新的Element合并到App的ClassLoader中,同时so路径也要合并到App的ClassLoader nativeBianryPath中;
  2. 调用dex文件中的加载指定Xposed模块的方法,完成外置插件的动态注入;

整体流程大致如此,不过其中还有不少细节需要处理,比如,SandHook初始化时需要传App Context,但是我们这个注入流程是在LoadedApk.makeApplication之前,此时App的Context并没有创建出来,因此,需要通过反射主动构造出一个Context对象:

LoadedApk loadedApk = ActivityThread.currentActivityThread().mBoundApplication.info;
ContextImpl appContext = ContextImpl.createAppContext(activityThread, loadedApk);

还有,为了能够加载Xposed插件中的so库,在加载插件Apk之前,需要将Apk中的so文件拷贝到data/data目录下,并将so路径传给DexClassLoader构造方法的最后一个参数。为了更高效地拷贝so,这里反射调用了Framework里的内部类NativeLibraryHelper。App安装时的so拷贝就是用NativeLibraryHelper实现的,具体拷贝操作在native层完成,效率更高。

另外,Android9及以上的系统限制了App对隐藏Api的调用。我们可以在注入so的JNI_Onload函数中加入以下代码,便可简单绕过这种限制:

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);
}

使用方法

以上流程的完整实现已经上传到github上:jdwp-xposed-injector

使用方法:

  1. git clone https://github.com/WindySha/jdwp-xposed-injector.git,下载工具文件;
  2. 下载插件模板仓库:XposedModuleSample,将工程导入到Android Studio中,打开插件工程;
  3. 在插件AS工程中加入自己需要注入的业务代码并编译出插件Apk。注入的代码可以是:Hook某些java方法,Hook某些c/c++函数,添加魔改ART虚拟机的逻辑等。
  4. 连接android设备,在命令行中执行以下命令:
// 第一个参数是需要注入的Debug App的包名,第二个参数是需要注入的插件Apk路径
$ injector.sh  com.pkg.test  ../XposedModuleSample.apk

// 对于同一个App,第二次执行命令时,可以加上fast_mode参数,避免了重复复制dex和so文件到data/data目录下,提升启动速度
$ injector.sh  com.pkg.test  ../XposedModuleSample.apk  fast_mode

最终,我们在Android设备上启动了目标App,并且Xposed插件Apk中的代码被注入到目标App中。

Debuggable App

本工具唯一的要求是App必须是Debuggable的,那么如何让一个App变成Debuggable的?大致总结了以下几种方式:

  1. 对于项目开发中的App,打出Debug包即可;
  2. 利用ApkTool对Release包进行反编译,得到AndroidManifest.xml文本文件,直接修改这个文本文件,在application标签下添加android:debuggable="true",然后将再用Apktool将修改后的包打包成Apk并签名即可;
  3. 直接解压Apk文件,利用ManifestEditor或者其他Axml二进制修改器直接修改解压出来的AndroidManifest.xml二进制文件,然后再压缩成Apk文件并重新签名。
  4. 对于已Root的设备,可以通过设置系统属性ro.debuggable的值为1,将设备中所有App都设置为Debuggable的,具体方法可以参考这个文章: Android修改ro.debuggable 的四种方法

Some Issues

  1. 在已启动Android studio的情况下,执行注入命令会偶现jdwp传输数据解析失败的问题,一般重新注入一次都可以恢复正常;
  2. 目前仅支持arm,arm64处理器,仅测试了android 5及以上的部分机器,未对多种机型做测试,可能存在部分机型兼容性问题。

Reference

  1. jdwp-shellifier
  2. SandHook
  3. xposed_module_loader
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容