[Android][Security] Android 逆向之 xposed


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模块









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


文章作者: Wossoneri
版权声明: 本博客所有文章除特別声明外,均采用 CC BY-NC 4.0 许可协议。转载请注明来源 Wossoneri !
评论
  目录