[Android][Security] Android 逆向之 xposed

  • 2020 年 1 月 20 日
  • 笔记

Xposed

网上关于Xposed的介绍很多,但都是点到为止,比如:

在Android系统中,应用程序进程以及系统服务进程SystemServer都是由Zygote进程孵化出来的,而Zygote进程是由Init进程启动的,Zygote进程在启动时会创建一个Dalvik虚拟机实例,每当它孵化一个新的应用程序进程时,都会将这个Dalvik虚拟机实例复制到新的应用程序进程里面去,从而使得每一个应用程序进程都有一个独立的Dalvik虚拟机实例,这也是Xposed选择替换app_process的原因。 Zygote进程在启动的过程中,除了会创建一个Dalvik虚拟机实例之外,还会注册一些Android核心类的JNI方法到Dalvik虚拟机实例中去,以及将Java运行时库加载到进程中来。而一个应用程序进程被Zygote进程孵化出来的时候,不仅会获得Zygote进程中的Dalvik虚拟机实例拷贝,还会与Zygote一起共享Java运行时库,这也就是可以将XposedBridge这个jar包加载到每一个Android应用程序中的原因,

我当然不会满足于这么一点浅薄的介绍,既然用这个框架了,那就得把这个框架搞清楚对不?

一句话原理:

Xposed框架的原理是通过替换/system/bin/app_process程序控制zygote进程,使得app_process在启动过程中会加载XposedBridge.jar这个jar包,从而完成对Zygote进程及其创建的Dalvik虚拟机的劫持。

为什么是app_process

Android系统是基于Linux内核的,而在Linux系统中,所有的进程都是init进程的子孙进程,也就是说,所有的进程都是直接或者间接地由init进程fork出来的。Zygote进程也不例外,它是在系统启动的过程,由init进程创建的。在系统启动脚本system/core/rootdir/init.rc文件中,我们可以看到启动Zygote进程的脚本命令:

service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server      socket zygote stream 666      onrestart write /sys/android_power/request_state wake      onrestart write /sys/power/state on      onrestart restart media      onrestart restart netd

系统启动之后就可以在/dev/socket目录下看到有一个名为zygote的文件,就是zygote占用的socket端口。

所以,zygote是由app_process启动的,替换app_process后,启动的就是Xposed之后的zygote了。

为什么XposedBridge可以生效

Xposed版zygote进程在启动时会创建一个Dalvik虚拟机实例,以及注册一些Android核心类的JNI方法到Dalvik虚拟机实例中去。同时Xposed版zygote把XposedBridge.jar添加到CLASSPATH环境变量,并将Java运行时库加载到进程中。一个应用程序进程被Zygote进程孵化出来的时候,不仅会获得Zygote进程中的Dalvik虚拟机实例拷贝,还会与Zygote一起共享Java运行时库,所以XposedBridge.jar可以被加载到每一个Android应用程序中。

zygote进程加载XposedBridge将所有需要替换的Method通过JNI方法hookMethodNative指向Native方法 xposedCallHandler,xposedCallHandler在转入handleHookedMethod这个Java方法执行用户规定的Hook Func。

Xposed版zygote在启动时还会获得一个JNIEnv实例,该实例描述的是zygote进程的主线程的JNI环境,Xposed版zygote进程通过JNIEnv实例的成员函数CallStaticVoidMethod()调用de.robv.android.xposed.XposedBridge的main函数作为java代码的入口点。

de.robv.android.xposed.XposedBridge.main函数做了以下几件事:

(1) 初始化xposed框架。

(2) 调用initForZygote()方法hook应用进程创建时调用的一些关键函数,比如通过挂钩LoadedApk的构造函数获得应用进程的相关信息并保存至XC_LoadPackage.LoadPackageParam的实例中,该实例在后续hook应用程序中的函数时可用于获取应用程序相关信息。通过挂钩handleBindApplication方法,可以在应用程序启动时调用所有IXposedHookLoadPackage类型的钩子(其实最终调用的是IXposedHookLoadPackage的handleLoadPackage方法)。该类型的钩子用于对应用程序进行挂钩,假如要hook应用程序中的函数,我们编写的xposed插件中的钩子类必须实现IXposedHookLoadPackag接口,重写它的handleLoadPackage方法并在方法体中调用xposed框架提供的挂钩函数(比如findAndHookMethod)hook想要挂钩的应用程序函数。

(3) 调用loadModules()加载所有的xposed插件,将这些插件中不同钩子类型的钩子分别保存起来。有三种类型的钩子,IXposedHookLoadPackage类型的钩子对应用程序挂钩,IXposedHookZygoteInit类型钩子对Zygote的初始化进行挂钩,IXposedHookInitPackageResources类型钩子对资源进行挂钩。

(4) 最后再调用原始的ZygoteInit.main函数,完成zygote的全部初始化工作。

http://4hou.win/wordpress/?p=7516

https://blog.csdn.net/u014385722/article/details/82013306

使用Java反射实现API Hook

通过对 Android 平台的虚拟机注入与 Java 反射的方式,来改变 Android 虚拟机调用函数的方式(ClassLoader),从而达到 Java 函数重定向的目的,这里我们将此类操作称为 Java API Hook。

先从简单的开始,比如尝试Hook按钮的点击事件。

首先先看一下点击事件:

/**   * Interface definition for a callback to be invoked when a view is clicked.   */  public interface OnClickListener {      /**       * Called when a view has been clicked.       *       * @param v The view that was clicked.       */      void onClick(View v);  }

我们对Button绑定点击事件:

mBtnHijack = findViewById(R.id.btn_hijack);  mBtnHijack.setOnClickListener(v -> {    Toast.makeText(MainActivity.this, "Click button", Toast.LENGTH_LONG).show();  });

所以下一步是看setOnClickListener方法是怎么保存OnClickListener接口的:

public void setOnClickListener(@Nullable OnClickListener l) {    if (!isClickable()) {      setClickable(true);    }    getListenerInfo().mOnClickListener = l;  }

看到OnClickListener被保存到ListenerInfo的成员变量中:

ListenerInfo getListenerInfo() {    if (mListenerInfo != null) {      return mListenerInfo;    }    mListenerInfo = new ListenerInfo();    return mListenerInfo;  }    static class ListenerInfo {  	...    public OnClickListener mOnClickListener;      protected OnLongClickListener mOnLongClickListener;      protected OnContextClickListener mOnContextClickListener;    ...  }

而ListenerInfo是View的一个内部类。

既然知道OnClickListener的保存位置,那么我们要Hook点击事件,就是创建一个自己的点击事件,然后替换掉原来的事件即可。

先创建一个实现自己功能的点击事件

class HookedOnClickListener implements View.OnClickListener {    private View.OnClickListener origin; // 原始的点击事件      HookedOnClickListener(View.OnClickListener origin) {      this.origin = origin;    }      @Override    public void onClick(View v) {      Toast.makeText(MainActivity.this, "hook click", Toast.LENGTH_SHORT).show();      Log.i("WOW", "Before click, do what you want to to.");      if (origin != null) {        origin.onClick(v); // 执行原始的点击逻辑      }      Log.i("WOW", "After click, do what you want to to.");    }  }

然后就是使用反射,用我们的OnClickListener替换原来注册的点击回调:

private void hookOnClickListener(View view) {    try {      // 得到 View 的 ListenerInfo 对象      Method getListenerInfo = View.class.getDeclaredMethod("getListenerInfo");      // 强制访问      getListenerInfo.setAccessible(true);      // 执行getListenerInfo拿到对象      Object listenerInfo = getListenerInfo.invoke(view);      // 得到 原始的 ListenerInfo 类      Class<?> listenerInfoClz = Class.forName("android.view.View$ListenerInfo");      // 从 ListenerInfo找到onClickListener属性      Field mOnClickListener = listenerInfoClz.getDeclaredField("mOnClickListener");      mOnClickListener.setAccessible(true);      // 用前面的listenerInfo对象获取原始的listener      View.OnClickListener originOnClickListener = (View.OnClickListener) mOnClickListener.get(listenerInfo);      // 用自定义的 OnClickListener 替换原始的 OnClickListener      View.OnClickListener hookedOnClickListener = new HookedOnClickListener(originOnClickListener);      mOnClickListener.set(listenerInfo, hookedOnClickListener);    } catch (Exception e) {      Log.w("hook clickListener failed!", e);    }  }

把这段代码放到按钮设置OnClickListener之后:

mBtnHijack.setOnClickListener(v -> {  	Toast.makeText(MainActivity.this, "Click button", Toast.LENGTH_LONG).show();  });  hookOnClickListener(mBtnHijack);

这样就完成了对按钮点击事件的Hook。

但是这只能编码Hook自己的应用,这样做的意义是什么呢?

当应用内接入了众多的 SDK,SDK 内部会使用系统服务 NotificationManager 发送通知,这就导致通知难以管理和控制。现在我们就用 Hook 技术拦截部分通知,限制应用内的通知发送操作。

发送通知是由NotificationManager的notify方法实现,通过查看源码,定位到:

public void notifyAsUser(String tag, int id, Notification notification, UserHandle user)      {          INotificationManager service = getService();          ...          try {              service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id, copy, user.getIdentifier());          } catch (RemoteException e) {              throw e.rethrowFromSystemServer();          }      }    private static INotificationManager sService;    /** @hide */  static public INotificationManager getService()  {    if (sService != null) {      return sService;    }    IBinder b = ServiceManager.getService("notification");    sService = INotificationManager.Stub.asInterface(b);    return sService;  }

INotificationManager 是跨进程通信的 Binder 类,sService 是 NMS(NotificationManagerService) 在客户端的代理,发送通知要委托给 sService,由它传递给 NMS。我们发现 sService 是个静态成员变量,而且只会初始化一次。只要把 sService 替换成自定义的不就行了么,确实如此。

private void hookNotificationManager(Context context) {    try {      NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);      // 得到系统的 sService      Method getService = NotificationManager.class.getDeclaredMethod("getService");      getService.setAccessible(true);      final Object sService = getService.invoke(notificationManager);        Class iNotiMngClz = Class.forName("android.app.INotificationManager");      // 动态代理 INotificationManager      Object proxyNotiMng = Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{iNotiMngClz}, new InvocationHandler() {          @Override        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable           		  {          log.debug("invoke(). method:{}", method);          if (args != null && args.length > 0) {            for (Object arg : args) {              log.debug("type:{}, arg:{}", arg != null ? arg.getClass() : null, arg);            }          }          // 操作交由 sService 处理,不拦截通知          // return method.invoke(sService, args);          // 拦截通知,什么也不做          return null;          // 或者是根据通知的 Tag 和 ID 进行筛选        }      });      // 替换 sService      Field sServiceField = NotificationManager.class.getDeclaredField("sService");      sServiceField.setAccessible(true);      sServiceField.set(notificationManager, proxyNotiMng);    } catch (Exception e) {      log.warn("Hook NotificationManager failed!", e);    }  }

Hook 的时机还是尽量要早,我们在 attachBaseContext 里面操作。

@Override  protected void attachBaseContext(Context newBase) {      super.attachBaseContext(newBase);      hookNotificationManager(newBase);  }

这样我们就完成了对通知的拦截,可见 Hook 技术真的是非常强大,好多插件化的原理都是建立在 Hook 之上的。

总结一下:

  1. Hook 的选择点:静态变量和单例,因为一旦创建对象,它们不容易变化,非常容易定位。
  2. Hook 过程:
    • 寻找 Hook 点,原则是静态变量或者单例对象,尽量 Hook public 的对象和方法。
    • 选择合适的代理方式,如果是接口可以用动态代理。
    • 偷梁换柱——用代理对象替换原始对象。
  3. Android 的 API 版本比较多,方法和类可能不一样,所以要做好 API 的兼容工作。

Xposed Hook微信运动

首先在AndroidManifest.xml Application下添加xposed模块

<!--xposed描述-->  <meta-data      android:name="xposeddescription"      android:value="这是一个Xposed例程" />    <!--xposed最低版本53-->  <meta-data      android:name="xposedminversion"      android:value="53" />    <!--这是一个xposed模块-->  <meta-data      android:name="xposedmodule"      android:value="true" />

Gradle添加依赖

compileOnly 'de.robv.android.xposed:api:82'  compileOnly 'de.robv.android.xposed:api:82:sources'

然后再assets目录添加一个xposed_init文件供Xposed框架访问,内容为包名:

com.softard.xposedemo.HookTest

然后创建我们的HookTest

public class HookTest implements IXposedHookLoadPackage {    // 实现Hook篡改程序      @SuppressLint("PrivateApi")      @Override      public void handleLoadPackage(final XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {  			if (lpparam.packageName.equals("com.tencent.mm")) { // 搞一搞微信              XposedBridge.log("hoooook wechat");              Class<?> clazz1 = Class.forName(                      "android.hardware.SystemSensorManager$SensorEventQueue", true, lpparam.classLoader);                XposedBridge.hookAllMethods(clazz1, "dispatchSensorEvent", new XC_MethodHook() {                  @Override                  protected void beforeHookedMethod(MethodHookParam param) throws Throwable {                      int times = XSharedPreferencesUtil.getPref().getInt("step", 500);                        XposedBridge.log("~~~~~~~Multi times: " + times);                      XposedBridge.log("Wechat2222 Sensor param " + ((float[]) param.args[1])[0]);                      ((float[]) param.args[1])[0] = ((float[]) param.args[1])[0] * times;                      XposedBridge.log("final Sensor param " + ((float[]) param.args[1])[0]);                      super.beforeHookedMethod(param);                  }                    @Override                  protected void afterHookedMethod(MethodHookParam param) throws Throwable {                      super.afterHookedMethod(param);                  }              });            }*/      }  }

To be continued