转自:http://blog.csdn.net/l173864930/article/details/38455951
写在前面
这个系列本来是在公司的一个分享,内容比较多,所以就把这个PPT重新组织整理成博客,希望对大家学习有所帮助。我会先以一个”短信拦截“作为例子,抛出问题,并提出了一种基于”注入“的技术方案达到提高拦截优先级,接着再重点讲解注入的技术细节。最后,我会跟大家分享一个我业余时间开发注入框架——AIM(Android IPC Manager)。当然了,这个框架跟目前的XPosed、CydiaStructe的侧重点不太一样。
短信拦截
场景
如果某款安全支付类应用和一个未知木马安装在同一台手机,木马会截获所有的验证码短信。
问题
有什么办法可以保证安全支付类的应用可以比木马更优先截获到验证码短信呢?
解决方案
其实这类优先级抢占问题,做短信功能开发的朋友,都应该比较清楚,因为老板都关注的就是这个了,呵呵。一般的可行方案如下:
- 提高BroadcastReceiver的优先级;
- 采用动态的方式注册BroadcastReceiver;
- 注册时间要越早越好;
基于上述三点,我们的可以做如下优化:
- 监听Android的开机广播事件BOOT_COMPLETED,提早注册时间;
- 启动后,开启一个Service,并在里面以动态的注册BroadcastReceiver;
- 注册的优先级调整至最高——Integer.MAX_VALUE;
关键代码如下所示:
[java] view
plaincopy
- IntentFilter filter = new IntentFilter("android.provider.Telephony.SMS_RECEIVED");
- filter.setPriority(Integer.MAX_VALUE);
- registerReceiver(new SmsReceiver(), filter);
问题来了
通过上面这个方案,是可以解决部分问题了,但这种方式完全没有技术含量,对处于技术第一线的黑客们根本不是问题,他们不但也采用了相同的技术,而且做得更好,在”注册时间越早越好“这一点上,他们添加几个额外的触发点,分别如下:
- WIFI_STATE_CHANED
- CONNECTIVITY_CHANGE
- ACCESS_NETWORK_STATE
- ACCESS_WIFI_STATE
这几个系统事件,都比BOOT_COMPLETED触发得要早,所以木马当然能优先截获到短信了。
基于这种对抗下,我们是否跟木马就只有打成平手呢?是否有更优的解决方案?答复是肯定的。
促进科技发展最有效的方法就是战争,在这种技术对抗中,同样也是最容易产生新的思想方案的。
基本思路
既然广播发送之后,我们有可能处于后次序。那是否有办法,可以在广播发送之前,做一下顺序调整呢?我们知道,Android的广播发送,都是由ActivityManagerService(AMS)做路由转发的,所以AMS里肯定保存了各个BroadcastReceiver的信息以及次序,我们如果有办法可以进入到AMS里头,再通过一些反射的技巧,是否就可以在每次发送广播之前,调整广播的发送顺序呢?
这个就是我要跟大家分享的技术方案——通过注入实现短信的绝对优先拦截。技术的基本思路是这样的,我们先注入到系统进程——system_process,然后截获AMS的broadcastIntent方法,加插我们的调整逻辑。
《二》里我会着重介绍注入的技术原理,然后通过一系列的DEMO讲解这个方案所涉及到的技术点。
继续
在《一》里,我把基本思路描述了一遍,接下为我们先从注入开始入手。
注入
分类
我们平时所说的代码注入,主要静态和动态两种方式
- 静态注入,针对是可执行文件,比如平时我们修改ELF,DEX文件等等,相关的辅助工具也很多,比如IDA、JEB、ApkTool等等;
- 动态注入,针对是进程,比如修改进程的寄存器、内存值等等;
动态跟静态最大的区别是,动态不需要改动源文件,但需要高权限(通常是root权限),而且所需的技术含量更高。
本质
动态注入技术,本质上就是一种调度技术。想想平时我们调试一个进程时,可以做哪些功能? 一般有下列几项:
- 查看变量值
- 修改变量值
- 跟踪进程跳转
- 查看进程调用堆栈
- 等等
动态注入相比于普通的调试,最大的区别就是动态注入是一个”自动化调试并达到加载自定义动态链接库“的过程。所谓自动化,其实就是通过代码实现,在Linux上通过Ptrace就可以完成上面所有功能,当然Ptrace功能是比较原始的,平时调试中的功能还需要很多高层逻辑封装才可以实现。
在阅读下面章节之前,强烈建议阅读一下man文档,见这里。
目的
一般而言,我们要对一个进程进行注入,主要有以下几方面目的:
- 增强目标进程的功能;
- 修复目标进程缺陷;
- 劫持目标进程函数;
- 窃取目标进程数据;
- 篡改目标进程数据;
过程
如上图所示,进程A注入到进程B后,通过修改寄存器和内存,让进程B加载自定义的动态库a,当a被加载后,a会尝试加载其他模块,比如加载dex文件等等,具体的注入过程如下:
- ATTATCH,指定目标进程,开始调试;
- GETREGS,获取目标进程的寄存器,保存现场;
- SETREGS,修改PC等相关寄存器,使其指向mmap;
- POPETEXT,把so path写入mmap申请的地址空间;
- SETRESG,修改PC等相关寄存器,使其指向dlopen;
- SETREGS,恢复现场;
- DETACH,解除调试,使其恢复;
上述是一个简化的过程,整个注入的代码,我已经上传到github,地址https://github.com/boyliang/Poison
当so被dlopen加载到目标进程后,我们需要让so中的逻辑被执行,比较复杂的做法是同样使用ptrace修改寄存器的办法,让目标进程调用dlsym找到我们函数的地址。而比较简单的做法有两种,如下
- 使用gcc的预编译指令__attribute__ ((__constructor__)),作用是让so被加载后,函数被自动执行;
[cpp] view
plaincopy
- __attribute__ ((__constructor__))
- void Main() {
- LOGI(">>>>>>>>>>>>>I am in, I am a bad boy 1!!!!<<<<<<<<<<<<<<");
- void* handle = dlopen("libinso.so", RTLD_NOW);
- void (*setA_func)(int) = (void (*)(int))dlsym(handle, "setA");
- if (setA_func) {
- setA_func(999);
- }
- }
- 使用c++全局对象初始化,其构造函数会被自动执行;
[cpp] view
plaincopy
- void Main();
- static void* _main(void*){
- Main();
- return NULL;
- }
- class EntryClass {
- public:
- EntryClass() {
- pthread_t tid;
- pthread_create(&tid, NULL, _main, NULL);
- pthread_detach(tid);
- }
- } boy;
示例一
下面示例一个通过ptrace注入的示例,涉及到两部分代码,一部分是目标进程代码记作host,另一部分是被我们注入的so代码记作libmyso.so
Host代码
包含三个源文件,分别是demo1.c,inso.h, inso.c
[cpp] view
plaincopy
- /*
- * inso.h
- *
- * Created on: 2014年6月24日
- * Author: boyliang
- */
- __attribute__ ((visibility ("default"))) void setA(int i);
- __attribute__ ((visibility ("default"))) int getA();
[cpp] view
plaincopy
- /*
- * inso.c
- *
- * Created on: 2014年6月24日
- * Author: boyliang
- */
- #include <stdio.h>
- #include "inso.h"
- static int gA = 1;
- void setA(int i){
- gA = i;
- }
- int getA(){
- return gA;
- }
[cpp] view
plaincopy
- /*
- * demo1.c
- *
- * Created on: 2014年6月24日
- * Author: boyliang
- */
- #include <stdio.h>
- #include <unistd.h>
- #include "inso.h"
- #include "log.h"
- int main(){
- LOGI("DEMO1 start.");
- while(1){
- LOGI("%d", getA());
- setA(getA() + 1);
- sleep(2);
- }
- return 0;
- }
libmyso.so代码
[cpp] view
plaincopy
- /*
- * myso.c
- *
- * Created on: 2014年6月24日
- * Author: boyliang
- */
- #include <stdio.h>
- #include <stddef.h>
- #include <dlfcn.h>
- #include <pthread.h>
- #include <stddef.h>
- #include "log.h"
- __attribute__ ((__constructor__))
- void Main() {
- LOGI(">>>>>>>>>>>>>I am in, I am a bad boy 1!!!!<<<<<<<<<<<<<<");
- void* handle = dlopen("libinso.so", RTLD_NOW);
- void (*setA_func)(int) = (void (*)(int))dlsym(handle, "setA");
- if (setA_func) {
- setA_func(999);
- }
- }
调用
注入程序,我将其命名为poison,使用方法是poison <so_path> <target_pit>。下面是示例的输出显示:
[plain] view
plaincopy
- I/TTT ( 594): DEMO1 start.
- I/TTT ( 594): 1
- I/TTT ( 594): 2
- I/TTT ( 594): 3
- I/TTT ( 594): 4
- I/TTT ( 594): 5
- I/TTT ( 594): 6
- I/TTT ( 594): 7
- I/TTT ( 594): >>>>>>>>>>>>>I am in, I am a bad boy 1!!!!<<<<<<<<<<<<<<
- I/TTT ( 594): 999
- I/TTT ( 594): 1000
- I/TTT ( 594): 1001
当执行./poison /data/local/tmp/libmyso.so 594后,输出中马上出现了特定字符串,并且打印的数据一下子变成了999,证明我们注入成功了。
示例代码
上述示例所涉及到代码,我都放发布到github上了,大家如果想研究代码,可以到https://github.com/boyliang/injection_by_ptrace
在《三》,我会再介绍一种Android上特有的注入技术,敬请期待。
进击的Android注入术《三》
在《二》详细介绍了通过ptrace实现注入的技术方案,在这个章节里,我再介绍一种Android上特有的注入技术,我命其名为——Component Injection。顾名思义,这种方式是跟Android的组件相关的,详细见下面叙述。
Component Injection
原理
在android的开发者文档里,对android:process的描述是这样的:
android:process
The name of a process where all components of the application should run. Each component can override this default by setting its own process
attribute.
By default, Android creates a process for an application when the first of its components needs to run. All components then run in that process. The name of the default process matches the package name set by the <manifest>
element.
By setting this attribute to a process name that's shared with another application, you can arrange for components of both applications to run in the same process — but only if the two applications also share a user ID and be
signed with the same certificate.
If the name assigned to this attribute begins with a colon (':'), a new process, private to the application, is created when it's needed. If the process name begins with a lowercase character, a global process of that name is created. A global process can be
shared with other applications, reducing resource usage.
从描述上可以发现,当两个应用,它们签名同样且具备相同的shareduserID,它们之间只有一个组件的android:process是相同的,那么这两个组件之间的互动可以发生在同一个进程里。这里所说的同一个进程,其实就是进程注入的效果的了。
示例二
示例二同样包含两部分代码,分别是com.demo.host和com.demo.inject,它们的代码都非常简单,如下所示:
com.demo.host
先看看host的manifest.xml的配置
[html] view
plaincopy
- <manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.demo.host"
- android:sharedUserId="com.demo"
- android:versionCode="1"
- android:versionName="1.0" >
- <application
- android:name=".DemoApplication"
- android:allowBackup="true"
- android:icon="@drawable/ic_launcher"
- android:label="@string/app_name"
- android:process="com.demo"
- android:theme="@style/AppTheme" >
- <activity android:name=".MainActivity" >
- <intent-filter>
- <action android:name="android.intent.action.MAIN" />
- <category android:name="android.intent.category.LAUNCHER" />
- </intent-filter>
- </activity>
- </application>
- <uses-sdk
- android:minSdkVersion="8"
- android:targetSdkVersion="9" />
- </manifest>
关键代码
[java] view
plaincopy
- package com.demo.host;
- import android.app.Activity;
- import android.content.ContentResolver;
- import android.net.Uri;
- import android.os.Bundle;
- import android.util.Log;
- /**
- *
- * @author boyliang
- *
- */
- public final class MainActivity extends Activity {
- private static int sA = 1;
- public static void setA(int a) {
- sA = a;
- }
- public static int getA() {
- return sA;
- }
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- ContentResolver resolver = getContentResolver();
- Uri uri = Uri.parse("content://demo_contentprovider");
- resolver.query(uri, null, null, null, null);
- new Thread() {
- public void run() {
- while (true) {
- Log.i("TTT", "" + getA());
- setA(getA() + 1);
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- };
- }.start();
- }
- }
host一启动,就马上调用ContentResolver的query,这个正是Inject里的ContentProvider组件。
com.demo.inject
manifest.xml
[html] view
plaincopy
- <manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.demo.inject"
- android:sharedUserId="com.demo"
- android:versionCode="1"
- android:versionName="1.0" >
- <application
- android:allowBackup="true"
- android:icon="@drawable/ic_launcher"
- android:label="@string/app_name"
- android:process="com.demo"
- android:theme="@style/AppTheme" >
- <provider
- android:name=".DemoContentProvider"
- android:authorities="demo_contentprovider"
- android:exported="false" />
- </application>
- <uses-sdk
- android:minSdkVersion="8"
- android:targetSdkVersion="9" />
- </manifest>
关键代码
[java] view
plaincopy
- <span style="white-space:pre"> </span>@Override
- public Cursor query(Uri arg0, String[] arg1, String arg2, String[] arg3, String arg4) {
- final Timer timer = new Timer("demo");
- timer.schedule(new TimerTask() {
- @Override
- public void run() {
- try {
- Log.i("TTT", ">>>>>>>>>>>>>I am in, I am a bad boy!!!!<<<<<<<<<<<<<<\n");
- //Class<?> MainActivity_class = Class.forName("com.demo.host.MainActivity");
- Context context = ContexHunter.getContext();
- ClassLoader classloader = context.getClass().getClassLoader();
- Class<?> MainActivity_class = classloader.loadClass("com.demo.host.MainActivity");
- Method setA_method = MainActivity_class.getDeclaredMethod("setA", int.class);
- setA_method.invoke(null, 998);
- } catch (Exception e) {
- e.printStackTrace();
- }
- timer.cancel();
- }
- }, 5000);
- return null;
- }
inject中,当query被调用后,会等待5s,然后通过反射调用host的MainActivity.setA方法,修改打印的数值。
绕过ClassLoader双亲委托
细心的朋友会发现,inject的代码中,获取MainActivity的Class,并不是直接通过Class.forName("com.demo.host.MainActivity")获取到,而是先获取到全局Context(即Application对象),然后再调用其ClassLoader来间接获取得的,为什么要这样呢?我我们知道,Java中每个class都是通过双亲委托机制加载的,这方面的内容可以参考http://blog.csdn.net/xyang81/article/details/7292380,下面我画出示意图:
当我们尝试在DemoContentProvider通过Class.forNmae寻找MainActivity时,必然会抛ClassNotFoundException。唯一可行的方案是找到host的PathClassLoader,然后通过这个ClassLoader寻找MainActivity。我们需要寻找的变量需要满足如下条件:
- 这个变量必须由host产生的;
- 这个变量必须是全局的,而且其引用会保存在BootClassLoader(也就是Android SDK中的某个引用);
- 可以通过反射机制读取到;
很自然的,想到了host的Application对象。通过阅读源码,发现可以通过下面的方式读取到Application对象:
- 如果是System_Process,可以通过如下方式获取
[java] view
plaincopy
- Context context = ActivityThread.mSystemContext
- 如果是非System_Process(即普通的Android进程),可以通过如下方式获取
[java] view
plaincopy
- Context context = ((ApplicationThread)RuntimeInit.getApplicationObject()).app_obj.this$0
输出
理解了上述的原理之后,我们再看看示例的输出:
[plain] view
plaincopy
- I/TTT ( 633): com.demo.inject starts.
- I/TTT ( 633): com.demo.host starts
- I/TTT ( 633): 1
- I/TTT ( 633): 2
- I/TTT ( 633): 3
- I/TTT ( 633): 4
- I/TTT ( 633): 5
- I/TTT ( 633): >>>>>>>>>>>>>I am in, I am a bad boy!!!!<<<<<<<<<<<<<<
- I/TTT ( 633): 998
- I/TTT ( 633): 999
- I/TTT ( 633): 1000
- I/TTT ( 633): 1001
- I/TTT ( 633): 1002
- I/TTT ( 633): 1003
从前二行就可以看出,这两个组件都是运行在同一个进程的。从第5秒开始,打印的数据开始发生变化,证明我们的注入逻辑生效了。
文中的示例代码,大家可以到https://github.com/boyliang/Component_Injection下载
最后
ComponentInjection的好处是不需要ROOT权限,但其使用限制也非常多。但如果跟MaskterKey漏洞结合起来用,那效果还是相当惊艳的。我们知道,Zygote进程会接收来自system_process的命令,其中比较关键的信息有uid, gid, gids, classpath, runtime-init等等,这些信息是决定了Zygote子进程的加载容器以及所从属的uid。
通过MasterKey漏洞我们可以伪造系统的Setting包,Setting与system_process的配置正好符合我所说的ComponentInjection条件,因此利用这种方式,可以注入到system_process进程,进而控制传递给Zygote的参数。其中classpath和runtime-init是加载容器的配置,classpath是指向一个dex文件的路径,runtime-init是其main函数所在的类名,通过指定每个App的加载容器,就可以很巧妙的控制了所有普通用户的进程的环境。
LBE 曾经就是利用这种技术实现主动防御的,更详细的介绍可访问http://safe.baidu.com/2013-10/lbe-root.html,不过这个文章分析得并不到位,最关键的环节即ComponentInjection并没有提及,结合的我分享,算是做一个完美的补充吧。
这一章节里,介绍了一种Android特有的注入技术,通过一些小技巧绕过了Java的双父委托机制。而且找到了可以轻松找到Application对象的方法,这个对象在Android开发中可以是至关重要的。在接下来的《四》里,我会详细介绍如何利用JNI获取JNIEnv指针,再通过JNI找到DexCloassLoader加载DEX文件。
在前《一》、《二》、《三》里已经把注入的技术介绍完了,这章开始说注入之后需要做的事情。如果对注入技术已经比较熟悉了,那么可以直接看本章,否则建议先把前三章阅读一遍会比较好。
注入之后
完成了注入,那只是万里长征的第一步。
众所周知,Android的应用进程,都是由Zygote孵化的子进程,每个进程都运行在独立的JVM中。通过ptrace的注入方式,我们得到了在目标进程执行代码的机会,但距离修改JVM的内容,还差那么一点点。我们重新看一下《二》中被注入SO的关键代码:
[cpp] view
plaincopy
- void Main();
- static void* _main(void*){
- Main();
- return NULL;
- }
- class EntryClass {
- public:
- EntryClass() {
- pthread_t tid;
- pthread_create(&tid, NULL, _main, NULL);
- pthread_detach(tid);
- }
- } boy;
当so被注入后,我们的逻辑代码实际上是跑在一个Linux线程上,这样做的目的是为了不对主线程造成干扰。我们的目标是打通Java层,很自然的联想到JNI,通过JNI我们就是可以跟Java层互动了。但这里缺少了一个非常重要的元素——JNIEnv,没有这个对象,JNI就无从说起了。
示例三
我们知道,在JVM进程中,JavaVM是全局唯一的,而JNIEnv则是按线程分配。另外,Dalvik的线程跟Linux线程是一一对应的,因此我们可以把自身所在的线程Attatch到JavaVM,JavaVM就会为我们分配JNIEnv对象了。通过阅读Dalvik源码,从AndroidRuntime中我们可以得到JavaVm的地址,再通过JavaVm所提供的AttachCurrentThead和DetachCurrentThread两个函数,即可完成JNIEnv的获取,示例代码如下:
[java] view
plaincopy
- JNIEnv *jni_env = NULL;
- JavaVM *jvm = AndroidRuntime::getJavaVM();
- jvm-AttachCurrentThread(&jni_env, NULL);
- //TODO 使用JNIEnv
- jvm->DetachCurrentThread();
至此,我们就拿到了至关重要的JNIEnv对象了。接下来,我们通过DexClassLoader加载我们的dex文件,关键代码如下所示:
先找到SystemClassLoader
[cpp] view
plaincopy
- //ClassLoader.getSystemClassLoader()
- static jobject getSystemClassLoader(){
- jclass class_loader_claxx = jni_env->FindClass("java/lang/ClassLoader");
- snprintf(sig_buffer, 512, "()%s", JCLASS_LOADER);
- jmethodID getSystemClassLoader_method = jni_env->GetStaticMethodID(class_loader_claxx, "getSystemClassLoader", sig_buffer);
- return jni_env->CallStaticObjectMethod(class_loader_claxx, getSystemClassLoader_method);
- }
然后通过SystemClassLoader,生成DexClassLoader对象
[cpp] view
plaincopy
- snprintf(sig_buffer, 512, "(%s%s%s%s)V", JSTRING, JSTRING, JSTRING, JCLASS_LOADER);
- jmethodID dexloader_init_method = jni_env->GetMethodID(dexloader_claxx, "<init>", sig_buffer);
- snprintf(sig_buffer, 512, "(%s)%s", JSTRING, JCLASS);
- jmethodID loadClass_method = jni_env->GetMethodID(dexloader_claxx, "loadClass", sig_buffer);
- jobject class_loader = getSystemClassLoader();
- check_value(class_loader);
- jobject dex_loader_obj = jni_env->NewObject(dexloader_claxx, dexloader_init_method, apk_path, dex_out_path, NULL, class_loader);
最后再通过dex_loader_obj加载dex,找到自定义方法的入口,并调用
[cpp] view
plaincopy
- jstring class_name = jni_env->NewStringUTF("com.demo.inject2.EntryClass");
- jclass entry_class = static_cast<jclass>(jni_env->CallObjectMethod(dex_loader_obj, loadClass_method, class_name));
- jmethodID invoke_method = jni_env->GetStaticMethodID(entry_class, "invoke", "(I)[Ljava/lang/Object;");
- check_value(invoke_method);
- jobjectArray objectarray = (jobjectArray) jni_env->CallStaticObjectMethod(entry_class, invoke_method, 0);
至此我们的dex逻辑开始执行了。我让com.demo.inject2.EntryClass.invoke作为的入口函数,从invoke里用上《三》示例中的com.demo.inject的代码,对com.demo.host打印的数据再进行修改(同一个进程被连续注入两次,应该是比较痛苦的)。下面看看inject2中invoke的代码:
[java] view
plaincopy
- package com.demo.inject2;
- import java.lang.reflect.Method;
- import android.content.Context;
- import android.util.Log;
- /**
- *
- * @author boyliang
- *
- */
- public final class EntryClass {
- public static Object[] invoke(int i) {
- try {
- Log.i("TTT", ">>>>>>>>>>>>>I am in, I am a bad boy 2!!!!<<<<<<<<<<<<<<");
- Context context = ContexHunter.getContext();
- Class<?> MainActivity_class = context.getClassLoader().loadClass("com.demo.host.MainActivity");
- Method setA_method = MainActivity_class.getDeclaredMethod("setA", int.class);
- setA_method.invoke(null, 1);
- } catch (Exception e) {
- e.printStackTrace();
- }
- return null;
- }
- }
代码跟《三》的示例非常相似,只是入口点不一样罢了。注意,这里同样有双亲委派的限制。
输出
am start com.demo.host/.MainActivity
./poison /data/local/tmp/libimportdex.so 738
看看示例三的输出
[plain] view
plaincopy
- com.demo.inject starts.
- I/TTT ( 738): com.demo.host starts
- I/TTT ( 738): 1
- I/TTT ( 738): 2
- I/TTT ( 738): 3
- I/TTT ( 738): 4
- I/TTT ( 738): 5
- I/TTT ( 738): >>>>>>>>>>>>>I am in, I am a bad boy!!!!<<<<<<<<<<<<<<
- I/TTT ( 738): 998
- I/TTT ( 738): 999
- I/TTT ( 738): 1000
- I/TTT ( 738): 1001
- I/TTT ( 738): 1002
- I/TTT ( 738): 1003
- I/TTT ( 738): >>>>>>>>>>>>>I am in, I am a bad boy 2!!!!<<<<<<<<<<<<<<
- I/TTT ( 738): 1
- I/TTT ( 738): 2
- I/TTT ( 738): 3
- I/TTT ( 738): 4
- I/TTT ( 738): 5
- I/TTT ( 738): 6
- I/TTT ( 738): 7
从两次的字符串输出,证明这次的注入修改已经成功了。
示例中的所有代码,都已经上传到https://github.com/boyliang/Java_Injection
最后
到目前为止,我们已经实现如下功能:
- 注入目标进程
- 获取JNIEnv地址;
- 另目标进程加载Dex,并执行指定的方法;
距离我们的目标,还差一步——截获broadcastIntent方法,在《五》里我会再介绍一种叫BinderProxy的技术,通过这种技术,我们可以截获任意的BinderService的方法。
在Android,几乎所有的IPC通讯都是通过Binder,可以说Binder在Android中占据着非常重要的地位。IPC通讯一般涉及client和server两部分,在Android上,所有Binder的serivce部分统称为NativeService(跟平时所说的Service组件不一样),一个NativeService可以跟多个client通讯,如果想更详细地了解这方面的内容可以到老罗的博客睢睢。
在日常开发过程中, 我们经常会使用到的ActivityManager、PackageManager就是一个client的调用,只是本身封装得比较好,让你感觉不到。而Service部分的逻辑,主要集中在system_process和com.android.phone这两个进程里头。
broadcastIntent是ActivityManagerService(AMS)的一个方法,AMS的宿主进程是system_process,毫无疑问我们需要先注入到system_process进程,至于接下来怎么做呢,正是本章的内容。
BinderProxy
原理
所有NativeService都继承到IBinder接口,BinderProxy原理很简单,就是先到找到要代理的NativeService引用,再通过自己编写的ProxyBinder对象代理NativeService,从而达到截获IPC通讯的目的。下面我们以AMS为例,做一个说明:
AMS跟binder进行通讯,是通过JNI实现的。AMS继承Binder(IBinder的子类,封装了IPC通讯公共部分的逻辑),Binder里保存着一个类型为int的mObject的字段,这个字段正是其C++对象JavaBBinder对象的地址,这个JavaBBinder才是AMS最终跟内核通讯的对象。代码如下:
[cpp] view
plaincopy
- public class Binder implements IBinder {
- //...
- /* mObject is used by native code, do not remove or rename */
- private int mObject; //这个对象保存的就是JavaBBinder的指针
- private IInterface mOwner;
- private String mDescriptor;
- //...
- }
同样的,在JavaBBinder中,也保存着一个类型jobject的mObject,指向上层Java对象。看看JavaBBinder的代码:
[cpp] view
plaincopy
- class JavaBBinder : public BBinder
- {
- //...
- jobject object() const
- {
- return mObject;
- }
- //...
- private:
- JavaVM* const mVM;
- jobject const mObject; //这个保存的是AMS的引用
- };
- }
Java和C++就是通过这两个字段相互连结在一起的。
其中JavaBBinder中的mObject是整个IPC关键的一节,所有的client请求,都是先到达JavaBBinder,然后JavaBBinder再通过JNI调用mObject的execTransact的方法,最终把请求发送到AMS。
因此,我们只要想办法找到AMS的对象的JavaBBinder,再把mObject替换为代理对象(记作ProxyBinder,一个Java对象的引用),即可实现BinderService代理,下面是示意图:
在实现这个代理,我们需要获取AMS和及对应用的JavaBBinder两个对象。
获取AMS引用
要获取AMS引用,通过ServiceManager即可,不过这类是隐藏类,通过反射才可以调用。通过ServiceManager.getService("activity")即可以拿到AMS。
获取JavaBBinder
通过前面的介绍,拿到AMS之后,就可以获取其mObject字段,这个对象正好就是JavaBBinder的地址。另外,也有一种比较简单的方式,那就是通过defaultServiceManager的getService方法获取到。
替换mObject对象
JavaBBinder的mObject对象并不能直接替换,因为mObject是const的,我写了一个DummyJavaBBinder的类,可以很容易地处理好这个问题,DummyJavaBBinder的实现如下:
[cpp] view
plaincopy
- class DummyJavaBBinder : public BBinder{
- public:
- virtual status_t onTransact(uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags = 0) {
- return NO_ERROR;
- }
- jobject object() const {
- return mObject;
- }
- JavaVM* javaVM() const {
- return mVM;
- }
- void changeObj(jobject newobj){
- const jobject* p_old_obj = &mObject;
- jobject* p_old_obj_noconst = const_cast<jobject *>(p_old_obj);
- *p_old_obj_noconst = newobj;
- }
- private:
- JavaVM* const mVM;
- jobject const mObject;
- };
这个类的作用主要添加了changeObj方法,主要功能是把mObject去掉const限制,并修改为的newobj。
示例四
示例四包含三部分代码,分别是com.demo.sms,com.demo.smstrojan,以及DemonInject3。
com.demo.sms和com.demo.smstrojan的逻辑是一样的,都是拦截短信,并打印短信内容,代码片断如下:
[java] view
plaincopy
- public final class SmsReceiver extends BroadcastReceiver {
- @Override
- public void onReceive(Context context, Intent intent) {
- Bundle bundle = intent.getExtras();
- if (bundle != null) {
- this.abortBroadcast();
- Object[] pdus = (Object[]) bundle.get("pdus");
- SmsMessage[] messages = new SmsMessage[pdus.length];
- for (int i = 0; i < pdus.length; i++) {
- messages[i] = SmsMessage.createFromPdu((byte[]) pdus[i]);
- }
- for (SmsMessage message : messages) {
- String msg = message.getMessageBody();
- String to = message.getOriginatingAddress();
- Log.i("TTT", context.getPackageName() + " To:" + to + " Msg:" + msg);
- }
- }
- }
- }
DemoInject3相对复杂,包含dex和proxybinder(被注入的so)两部分。dex的逻辑是生成代理的proxybinder,并通过invoke返回给lib,lib再通过DummyJavaBBinder修改其mObject为proxybinder,关键代码如下
dex代码
[java] view
plaincopy
- package com.demo.inject3;
- import android.net.Uri;
- import android.os.Binder;
- import android.os.IBinder;
- import android.os.Parcel;
- import android.os.RemoteException;
- import android.util.Log;
- /**
- *
- * @author boyliang
- *
- */
- public final class EntryClass {
- private static final class ProxyActivityManagerServcie extends Binder {
- private static final String CLASS_NAME = "android.app.IActivityManager";
- private static final String DESCRIPTOR = "android.app.IActivityManager";
- private static final int s_broadcastIntent_code;
- private SmsReceiverResorter mResorter;
- static {
- if (ReflecterHelper.setClass(CLASS_NAME)) {
- s_broadcastIntent_code = ReflecterHelper.getStaticIntValue("BROADCAST_INTENT_TRANSACTION", -1);
- } else {
- s_broadcastIntent_code = -1;
- }
- }
- private IBinder mBinder;
- public ProxyActivityManagerServcie(IBinder binder) {
- mBinder = binder;
- mResorter = new SmsReceiverResorter(binder);
- }
- @Override
- protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
- if (code == s_broadcastIntent_code) {
- mResorter.updatePriority("com.demo.sms");
- }
- return mBinder.transact(code, data, reply, flags);
- }
- }
- public static Object[] invoke(int i) {
- IBinder activity_proxy = null;
- try {
- activity_proxy = new ProxyActivityManagerServcie(ServiceManager.getService("activity"));
- Log.i("TTT", ">>>>>>>>>>>>>I am in, I am a bad boy 3!!!!<<<<<<<<<<<<<<");
- } catch (Exception e) {
- e.printStackTrace();
- }
- return new Object[] { "activity", activity_proxy };
- }
- }
看到onTransact中code的过滤处理,当code==s_broadcastIntent_code时,证明有client调用了sendBroadcast方法了,然后马上调用SmsReceiverRestorter中的updatePriority方法。
最后invoke返回的是一个Object数组,分别是"activity"字符串和activity_proxy对象,再看看proxybinder.cpp的中调用invoke方法的处理:
[cpp] view
plaincopy
- <span style="white-space:pre"> </span>jmethodID invoke_method = jni_env->GetStaticMethodID(entry_class, "invoke", "(I)[Ljava/lang/Object;");
- check_value(invoke_method);
- jobjectArray objectarray = (jobjectArray) jni_env->CallStaticObjectMethod(entry_class, invoke_method, 0);
- check_value(objectarray);
- jsize size = jni_env->GetArrayLength(objectarray);
- sp<IServiceManager> servicemanager = defaultServiceManager();
- for (jsize i = 0; i < size; i += 2) {
- jstring name = static_cast<jstring>(jni_env->GetObjectArrayElement(objectarray, i));
- jobject obj = jni_env->GetObjectArrayElement(objectarray, i + 1);
- const char* c_name = jni_env->GetStringUTFChars(name, NULL);
- DummyJavaBBinder* binder = (DummyJavaBBinder*) servicemanager->getService(String16(c_name)).get();
- binder->changObj(jni_env->NewGlobalRef(obj));
- }
lproxybinder.cpp中根据invoke返回的数组进行处理。
至此,整个BinderProxy技术的技术已经介绍完毕了,接下来看看SmsReceiverRestorter的代码,这个类主要是负责修改广播的发送顺序。跟广播发送顺序有关的变量位置ActivityManagerService.mReceiverResolver.mActionToFilter,其定义如下为private final HashMap<String, ArrayList<IntentFilter>> mActionToFilter。其中key是action,value是各个broadcast中的intentfilter描述,这个value本身是一个List,其顺序即为广播的发送顺序,调整这个顺序即可,见代码;
[java] view
plaincopy
- final class SmsReceiverResorter {
- private static final String[] sActions = { "android.provider.Telephony.SMS_RECEIVED", "android.provider.Telephony.SMS_RECEIVED2", "android.provider.Telephony.GSM_SMS_RECEIVED" };
- private final String TAG = "SmsReceiverResorter";
- private HashMap<String, ArrayList<? extends IntentFilter>> mActionToFilter;
- private Field mPackageNameField;
- @SuppressWarnings("unchecked")
- public SmsReceiverResorter(IBinder am) {
- Class<?> claxx = am.getClass();
- try {
- Field field = claxx.getDeclaredField("mReceiverResolver");
- field.setAccessible(true);
- Object mReceiverResolver = field.get(am);
- claxx = mReceiverResolver.getClass();
- field = claxx.getSuperclass().getDeclaredField("mActionToFilter");
- field.setAccessible(true);
- mActionToFilter = (HashMap<String, ArrayList<? extends IntentFilter>>) field.get(mReceiverResolver);
- } catch (Exception e) {
- Log.e(TAG, e.toString());
- }
- }
- /**
- * 修改优先级
- */
- public void updatePriority(String target_pkg) {
- if (mActionToFilter != null) {
- for (String action : sActions) {
- @SuppressWarnings("unchecked")
- ArrayList<IntentFilter> filters = (ArrayList<IntentFilter>) mActionToFilter.get(action);
- if (filters != null) {
- Log.i("TTT", "send sms broadcast");
- IntentFilter filter = null;
- for (IntentFilter f : filters) {
- String pkg = getPackageName(f);
- if (target_pkg.equals(pkg)) {
- filter = f;
- break;
- }
- }
- // 调整顺序
- if (filter != null && filters.remove(filter) ) {
- filters.add(0, filter);
- filter = null;
- Log.i("TTT", target_pkg + " is the first now");
- }
- }
- }
- }
- }
- private String getPackageName(IntentFilter filter) {
- if (mPackageNameField == null && filter != null) {
- Class<?> claxx = filter.getClass();
- try {
- mPackageNameField = claxx.getDeclaredField("packageName");
- mPackageNameField.setAccessible(true);
- } catch (Exception e) {
- Log.e(TAG, e.toString());
- }
- }
- String result = null;
- if (filter != null) {
- try {
- result = (String) mPackageNameField.get(filter);
- } catch (Exception e) {
- Log.e(TAG, e.toString());
- }
- }
- return result;
- }
- }
最后
这次的示例代码有点多,我已经上传至https://github.com/boyliang/Hijack_AMS_broadIntent。
通过上面的方法,无论com.demo.sms是怎样落后于sms.demo.smstrojan注册广播,都可以最先拦截到短信。