背景阐述

Android是一种基于Linux的自由及开放源代码的操作系统,由Google公司和开放手机联盟领导及开发。由于其开放的特质,吸引了一大批硬件厂商和软件开发者。第三方的统计数据显示,2016年Android占有的市场份额高达76.4%,远远超过其他智能手机厂商。

大量的Android os装机量,在丰富安卓系统使用场景的同时,也催生出了许多安全问题。xposed框架提供了不需要修改系统源码就能灵活定制系统功能的能力,极大的方便了安全研究人员的工作。且xposed利用了JNI机制修改Java framework的功能实现,没有涉及arm本地指令的适配工作,所以很少出现兼容性问题。本文是为了带领大家了解下此框架的能力以及实现方式,最终打造一款属于自己的“神器”。

本文总计5个章节,以xposed的使用需求作为切入点,由浅入深地介绍了模块的编写实战、模块编写进阶篇和常用模块编写以及异常情况&后续展望。本文来源于平时的实践,用作大家互相交流与学习。

xposed 使用需求

我们在选择使用xposed功能模块的时候,可能基于以下需求之一:

[1]监控app行为:查看关键api 的调用日志,用于特定目标的行为分析。

[2]定制系统功能:改变原先函数的处理逻辑,自定义api行为。

[3]沙箱功能定制:主要关注反环境检测(上述两点关注app本身),如:恶意样本分析,模拟器需要尽可能的“真实”以便触发样本行为。

当然,实际的需求并不会仅仅局限于此,可能会更多。这里列出的需求点也只是个人的总结,如有遗漏,敬请告知。毕竟需求驱动学习,文章的出发点也是希望聚集有着共同需求点的小伙伴,大家有个讨论地方,共同学习和进步。好,其它话不多说,接下来进入xposed模块的编写实战。

xposed 模块编写实战

xposed 模块的能力包括以下几个方面:

[1] 对普通函数或者构造函数有作用(针对具体实现类,不包括接口,抽象类的实现函数也可以hook)。

[2] 对目标函数进行 before、after 代码插桩,多用于操作(查看或修改)api的入参以及返回值。

[3] 目标函数替换,多用于功能变更、版本升级。

接下来,我们列举下 xposed 模块编写可能遇到的实际场景(假设阅读本文之前,读者拥有基本的模块编写经验)。在这个demo中,我将尽可能全面的再现需要hook操作的场景。比如:函数体、构造函数、匿名类、匿名内部类以及类的值域。

1.png

上述demo中存在

(1)静态field变量sMoney 

(2)隐藏函数hidden_fun(触发的条件相对苛刻)

(3)内部类inner_class

(4)匿名内部类Animal animal = new Animal(){}

(5)构造函数demo()

我们逐个回顾下相应问题的解决方法:

(1)静态field变量sMoney的值的修改和获取,可以直接使用xposed提供的XposedHelpers类相关功能函数。具体操作可以类比以下示例代码片段:

/*
 * Hook field
 * class: com.example.inner_class_demo.demo
 *  field: sMoney
 */ Class clazz = XposedHelpers.findClass("com.example.inner_class_demo.demo",lpparam.classLoader);
XposedHelpers.setStaticObjectField(clazz,"sMoney",110);
Field sMoney = clazz.getDeclaredField("sMoney");
sMoney.setAccessible(true);
System.out.println(sMoney.get(null)); 

(2)主动调用隐藏函数hidden_fun(这一类函数是指触发条件比较苛刻的函数,但是我们又需要了解它的输入、输出的大致关系),需要通过clazz来新建实例,最后将此实例与函数名组装成XposedHelpers.callMethod() 的实参需求形式。具体操作可以类比以下示例代码片段:

/*
 * Call hidden function
 * class : com.example.inner_class_demo.demo 
 *  function: hidden_fun()
 */

Class clazz = XposedHelpers.findClass("com.example.inner_class_demo.demo",lpparam.classLoader);
XposedHelpers.callMethod(constructor.newInstance(),"hidden_fun"); 

以上代码仅适用于存在无参构造函数的类,如果目标类没有无参构造函数,那就麻烦一点了,需要根据构造函数参数类型,反射寻找构造函数,接着才能类似上述操作。具体操作可以类比以下示例代码片段:

假设此时的构造函数仅有以下函数,即public demo(){}不存在的情形: public demo(String str){...} /*
 * Call hidden function
 * class   : com.example.inner_class_demo.demo 
 *  function: hidden_fun()
 */ Class clazz = XposedHelpers.findClass("com.example.inner_class_demo.demo",lpparam.classLoader);
Constructor constructor = clazz.getConstructor(String.class);
XposedHelpers.callMethod(constructor.newInstance("..."),"hidden_fun"); 

(3)内部类inner_class作为Android编程过程常见的一种编程方式,这里为了demo的全面,也将其列出。其实内部类整个处理过程与普通类极其相似,具体操作可以类比以下示例代码片段:

/*
 * Hook the function of inner class
 * class   : com.example.inner_class_demo.demo$inner_class
 *  function: secret(String , boolean)
 */ XposedHelpers.findAndHookMethod("com.example.inner_class_demo.demo$inner_class", lpparam.classLoader, "secret", String.class, boolean.class, new XC_MethodHook() { protected void beforeHookedMethod(MethodHookParam param) throws Throwable { for (int i = 0; i < param.args.length; i++) {
          XposedBridge.log(" argument is:" + param.args[i]);
        } int field_result = (int) XposedHelpers.getObjectField(param.thisObject,"pMoney");

        XposedBridge.log(String.valueOf(field_result));
      }
    }); 

需要注意的是,这里打印目标函数参数列表的时候,用了XposedBridge.log()。这样的输出方式对日志的长度有限制,即长度不超过1024。特殊场合(比如:文件读取时,需要查看文件的内容)需要注意处理下,不然会出现截断的现象。

(4)匿名内部类Animal animal = new Animal(){}的处理

同内部类,一般是class_name$1之类,具体可以反编译目标程序查看下。常见的反编译工具,比如:apktool、jeb、baksmali 均可以方便达到目的。

(5)构造函数demo()的处理,可以使用xposed提供的XposedHelpers类,具体操作可以类比以下示例代码片段:

/*
 * Hook the constructor of class
 * class   : om.example.inner_class_demo.demo
 *  function: demo()
 */ Class clazz = XposedHelpers.findClass("com.example.inner_class_demo.demo",lpparam.classLoader);
XposedHelpers.findAndHookConstructor(clazz, new XC_MethodHook() {
  ...
}); 

需要注意的是,由于构造函数不同于普通函数,函数名不需要提供(因为与类名相同,xposed框架处理函数名问题)。

模块编写进阶篇

在实际的模块编写时候,我们都或多或少地遇到一些问题。接下来我将列出一些我在实践过程中遇到的一些问题和解决思路,期待帮助有同样困惑的小伙伴。如果你有问题,这里很不幸的又没有列出,那你可以拿出来大家一起讨论下。

一:同时监控多个构造函数、多个重载函数

通过上一小节模块的编写,我们现在已经可以顺利hook某一特定目标函数了。如果遇到某一类函数需要“批量”hook操作的时候,比如:需要同时监控多个构造函数、多个重载函数,我们此时不可能去挨个hook每个具体目标,那么应该怎么操作呢?我们可以这样来实现:

/*
 * Hook all constructors of class
 * class   : om.example.inner_class_demo.demo
 *  function: demo()、demo(String)
 */ hookAllConstructors(clazz, new XC_MethodHook() {
  ...
}); /*
 * Call all methods of class
 * class   : om.example.inner_class_demo.demo
 *  function: method()、method(String)
 */ hookAllMethods(clazz, new XC_MethodHook() {
  ...
}); 

我们可以总结一个规律:hook重载函数时候,只需要忽略参数的具体类型即可。这种方式其实可以达到两种效果:1. 高效的处理函数重载问题 2.目标函数参数类型太复杂,自定义的类型太多。忽略参数类型,可以简化我们的hook工作。

二:目标app功能丰富,用到multidex加载技术,我们又该怎么办?

由于dalvik环境下xposed对multidex的支持没有很好的通用解决方案,寻找目标函数会发生ClassNotFoundError,所以处理multidex需要一些技巧(Tips): 此问题因为classloader出错引起的,所以要寻找attachBaseContext 的classloader,而非lpparam.classLoader(此思路来自非虫前辈)。

下图即为xposed作者对不支持multidex的解释,详细的内容可以去github上查看对应的issue。

图片5.png

常用模块编写

在这一章节,我将列出一些常见的功能模块。希望发散下大家的思路、节约模块开发的时间成本(毕竟重复劳动会消耗些时间、精力,积少成多嘛)。

第一部分

Hook org.apache.http 包中的网络请求,忽略参数然后使用hookAllMethods就可以同时拦截HttpPost、HttpGet、HttpUriRequest类型的网络请求参数。

/*
 * Hook net access
 * abstract class: org.apache.http.impl.client.AbstractHttpClient
 *  function      : execute(HttpHost target, HttpRequest request,HttpContext context)
 *                :execute(HttpUriRequest request, HttpContext context)
 */ hookAllMethods("org.apache.http.impl.client.AbstractHttpClient", lpparam.classLoader, "execute", new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
   print_args(param);
  }
}); 

需要注意点有二:

1. 这里就是0×02章节里面第[1]点提到的,xposed针对抽象类中的具体实现函数的hook。

2. 这里参数特殊,如果直接强转成String类型然后输出,将会得到无意义的输出,形如:org.apache.http.client.methods.HttpPost@41d45200。所以输出之前可以判断下,具体操作可以类比以下示例代码片段:

Object arg = param.args[i]; String argValue = "null"; if(arg instanceof HttpPost){
  URI uri = ((HttpPost)arg).getURI();
  argValue = String.format("uri=%s ", uri.toString());
}else if(arg instanceof HttpGet){
  URI uri = ((HttpGet)arg).getURI();
  argValue = uri.toString();
}else if(arg instanceof HttpUriRequest){
  URI uri = ((HttpUriRequest)arg).getURI();
  argValue = uri.toString();
}else argValue = arg.toString(); 

这里需要注意的是,HttpPost 之类的包在高版本的sdk中已经不存在了,顺利通过编译需要进行以下操作:1. Android studio中修改编译文件,添加

android { useLibrary 'org.apache.http.legacy' } 

3. ADT中可以按照以下教程,The import org.apache.http.HttpResponce cannot resolved error solution

第二部分

网络重定向,修改网络请求地址,“模拟“”中间人攻击效果,具体操作可以类比以下示例代码片段:

/*
 * Redirect net access
 * class: java.net.URL
 *
 */ XposedHelpers.findAndHookConstructor("java.net.URL", lpparam.classLoader, String.class, new XC_MethodHook() {
  @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
    String url = (String) param.args[0];
    param.args[0] = "http://www.baidu.com/";
    XposedBridge.log("new URL to " + param.args[0]);
  }
}); 

[3] IO异常监控,这里的IO异常包括所有网络IO异常和本地异常,具体操作可以类比以下示例代码片段:

/*
 * Monitor IO Exception
 * class: java.io.IOException
 *
 */ XposedBridge.hookAllConstructors(IOException.class, new XC_MethodHook() {
  @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable {
    XposedBridge.log((Throwable) param.thisObject);
  }
}); 

异常情况和后续展望

常用的hook操作总结如下,Object…代表着变参,实际编程过程中,需要保证提供的参数列表中最后一个参数一定是hook操作的回调,前面是函数参数class类型。

/*
 *  Hook any method (or constructor) with the specified callback
 *
 * @param targetclass The method in which
 * @param hookMethod The method to be hooked.
 * @param callback The callback to be executed when the hooked method is called.
 * @return An object that can be used to remove the hook.
 */ XposedHelpers#findAndHookMethod(String, ClassLoader, String, Object...) XposedHelpers#findAndHookMethod(Class, String, Object...) XposedBridge#hookAllMethods XposedHelpers#findAndHookConstructor(String, ClassLoader, Object...) XposedHelpers#findAndHookConstructor(Class, Object...) XposedBridge#hookAllConstructors 

模块编程过程中,如果不希望直接提供变参列表,可以提供Object数组,这样可以保证上述接口的“稳定”。具体操作可以类比以下示例代码片段:

Object[] args_obj = new Object[2] ;

XC_MethodHook callback_fun = new XC_MethodHook() {
    @Override
    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
      XposedBridge.log("...");

    }

    @Override
    protected void afterHookedMethod(MethodHookParam param) throws Throwable {
      XposedBridge.log("...);

    }
};

args_obj[0] = String.class;
args_obj[1] = callback_fun;

findAndHookMethod(class_name, lpparam.classLoader, function_name,args_obj); 

以上demo为debug版本,没有混淆。在实践过程中,可能遇到混淆甚至加固的产品。更有甚者,目标app即使没有混淆、加固,但是我们还是不能很快定位目标函数,难道此时只能大海捞针般静态寻找target?这时候需要通过一些辅助工具帮助我们定位api位置。

此文章可能考虑续篇,内容根据上述异常情况或者我能想到的新的出发点与思路。比如:反模拟器检测、反调试和加密库操作监控,每一点都是一个小的工程,需要考虑周全些才能有实用价值,大家一起努力。

最后,感谢非虫对文章难点提供的解决思路,以及最终文章质量的把关。

本文原创作者:L5,