[关闭]
@TryLoveCatch 2022-05-08T14:24:28.000000Z 字数 8943 阅读 564

Android知识体系之Binder Hook

Android知识体系


系统服务的获取过程

我们知道系统的各个远程service对象都是以Binder的形式存在的,而这些Binder有一个管理者,那就是ServiceManager;我们要Hook掉这些service,自然要从这个ServiceManager下手,不然星罗棋布的Binder广泛存在于系统的各个角落,要一个个找出来还真是大海捞针。

回想一下我们使用系统服务的时候是怎么干的,想必这个大家一定再熟悉不过了:通过Context对象的getSystemService方法;比如要使用ActivityManager:

  1. ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);

可是这个貌似跟ServiceManager没有什么关系啊?我们再查看getSystemService方法;(Context的实现在ContextImpl里面):

  1. public Object getSystemService(String name) {
  2. ServiceFetcher fetcher = SYSTEM_SERVICE_MAP.get(name);
  3. return fetcher == null ? null : fetcher.getService(this);
  4. }

很简单,所有的service对象都保存在一张map里面,我们再看这个map是怎么初始化的:

  1. registerService(ACCOUNT_SERVICE, new ServiceFetcher() {
  2. public Object createService(ContextImpl ctx) {
  3. IBinder b = ServiceManager.getService(ACCOUNT_SERVICE);
  4. IAccountManager service = IAccountManager.Stub.asInterface(b);
  5. return new AccountManager(ctx, service);
  6. }});

在ContextImpl的静态初始化块里面,有的Service是像上面这样初始化的;可以看到,确实使用了ServiceManager;当然还有一些service并没有直接使用ServiceManager,而是做了一层包装并返回了这个包装对象,比如我们的ActivityManager,它返回的是ActivityManager这个包装对象:

  1. registerService(ACTIVITY_SERVICE, new ServiceFetcher() {
  2. public Object createService(ContextImpl ctx) {
  3. return new ActivityManager(ctx.getOuterContext(), ctx.mMainThread.getHandler());
  4. }});

但是在ActivityManager这个类内部,也使用了ServiceManager;具体来说,因为ActivityManager里面所有的核心操作都是使用ActivityManagerNative.getDefault()完成的。那么这个语句干了什么呢?

  1. private static final Singleton<IActivityManager> gDefault = new Singleton<IActivityManager>() {
  2. protected IActivityManager create() {
  3. IBinder b = ServiceManager.getService("activity");
  4. IActivityManager am = asInterface(b);
  5. return am;
  6. }
  7. };

小结

因此,通过分析我们得知,系统Service的使用其实就分为两步:

  1. // 1、获取原始的IBinder对象
  2. IBinder b = ServiceManager.getService("service_name");
  3. // 2、转换为Service接口
  4. IXXInterface in = IXXInterface.Stub.asInterface(b);

HOOK的过程

参考这里,整个Hook过程简要总结如下:

  1. 寻找Hook点,原则是静态变量或者单例对象,尽量Hook pulic的对象和方法,非public不保证每个版本都一样,需要适配。
  2. 选择合适的代理方式,如果是接口可以用动态代理;如果是类可以手动写代理也可以使用cglib。
  3. 偷梁换柱——用代理对象替换原始对象

寻找Hook点

我们现在已经搞清楚了系统服务的使用过程,那么就需要找出在这个过程中,在哪个环节是最合适hook的。
之前咱们分析过,在重复一遍:

  1. // 1、获取原始的IBinder对象
  2. IBinder b = ServiceManager.getService("service_name");
  3. // 2、转换为Service接口
  4. IXXInterface in = IXXInterface.Stub.asInterface(b);

由于系统服务的使用者都是对第二步获取到的IXXInterface进行操作,因此如果我们要hook掉某个系统服务,只需要把第二步的asInterface方法返回的对象修改为为我们Hook过的对象就可以了。

asInterface过程

这里我们以系统剪切版服务(android.content.IClipboard)为例,来分析asInterface方法,然后想办法把这个方法的返回值修改为我们Hook过的系统服务对象。

  1. public static android.content.IClipboard asInterface(android.os.IBinder obj) {
  2. if ((obj == null)) {
  3. return null;
  4. }
  5. android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR); // Hook点
  6. if (((iin != null) && (iin instanceof android.content.IClipboard))) {
  7. return ((android.content.IClipboard) iin);
  8. }
  9. return new android.content.IClipboard.Stub.Proxy(obj);
  10. }

如果不太明白的,可以参考之前的文章

简单来说,先查看本进程是否存在这个Binder对象,如果有那么直接就是本进程调用了;
如果不存在那么创建一个代理对象,让代理对象委托驱动完成跨进程调用。

察这个方法,前面的那个if语句判空返回肯定动不了手脚;最后一句调用构造函数然后直接返回我们也是无从下手,要修改asInterface方法的返回值,我们唯一能做的就是从这一句下手:

  1. // Hook点
  2. android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);

我们可以尝试修改这个obj对象的queryLocalInterface方法的返回值,并保证这个返回值符合接下来的if条件检测,那么就达到了修改asInterface方法返回值的目的。

而这个obj对象刚好是我们第一步返回的IBinder对象,接下来我们尝试对这个IBinder对象的queryLocalInterface方法进行hook。

getService过程

上文分析得知,我们想要修改IBinder对象的queryLocalInterface方法;获取IBinder对象的过程如下:

  1. IBinder b = ServiceManager.getService("service_name");

因此,我们希望能修改这个getService方法的返回值,让这个方法返回一个我们伪造过的IBinder对象;这样,我们可以在自己伪造的IBinder对象的queryLocalInterface方法作处理,进而使得asInterface方法返回在queryLocalInterface方法里面处理过的值,最终实现hook系统服务的目的。

在跟踪这个getService方法之前我们思考一下,由于系统服务是一系列的远程Service,它们的本体,也就是Binder本地对象一般都存在于某个单独的进程,在这个进程之外的其他进程存在的都是这些Binder本地对象的代理。因此在我们的进程里面,存在的也只是这个Binder代理对象,我们也只能对这些Binder代理对象下手。如果不理解的话,可以参考之前的文章

接下来就可以看这个getService的代码了:

  1. public static IBinder getService(String name) {
  2. try {
  3. IBinder service = sCache.get(name);
  4. if (service != null) {
  5. return service;
  6. } else {
  7. return getIServiceManager().getService(name);
  8. }
  9. } catch (RemoteException e) {
  10. Log.e(TAG, "error in getService", e);
  11. }
  12. return null;
  13. }

ServiceManager为了避免每次都进行跨进程通信,把这些Binder代理对象缓存在一张map里面。

我们可以替换这个map里面的内容为Hook过的IBinder对象,由于系统在getService的时候每次都会优先查找缓存,因此返回给使用者的都是被我们修改过的对象,从而达到瞒天过海的目的。

总结一下,要达到修改系统服务的目的,我们需要如下两步:

  1. 首先肯定需要伪造一个系统服务对象,接下来就要想办法让asInterface能够返回我们的这个伪造对象而不是原始的系统服务对象。
  2. 通过上文分析我们知道,只要让getService返回IBinder对象的queryLocalInterface方法直接返回我们伪造过的系统服务对象就能达到目的。所以,我们需要伪造一个IBinder对象,主要是修改它的queryLocalInterface方法,让它返回我们伪造的系统服务对象;然后把这个伪造对象放置在ServiceManager的缓存map里面即可。

Hook系统剪切版服务

伪造剪切版服务对象

首先我们用代理的方式伪造一个剪切版服务对象,具体代码如下,我们用动态代理的方式Hook掉了hasPrimaryClip(),getPrimaryClip()这两个方法:

  1. public class BinderHookHandler implements InvocationHandler {
  2. private static final String TAG = "BinderHookHandler";
  3. // 原始的Service对象 (IInterface)
  4. Object base;
  5. public BinderHookHandler(IBinder base, Class<?> stubClass) {
  6. try {
  7. Method asInterfaceMethod = stubClass.getDeclaredMethod("asInterface", IBinder.class);
  8. // IClipboard.Stub.asInterface(base);
  9. this.base = asInterfaceMethod.invoke(null, base);
  10. } catch (Exception e) {
  11. throw new RuntimeException("hooked failed!");
  12. }
  13. }
  14. @TargetApi(Build.VERSION_CODES.HONEYCOMB)
  15. @Override
  16. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  17. // 把剪切版的内容替换为 "you are hooked"
  18. if ("getPrimaryClip".equals(method.getName())) {
  19. Log.d(TAG, "hook getPrimaryClip");
  20. return ClipData.newPlainText(null, "you are hooked");
  21. }
  22. // 欺骗系统,使之认为剪切版上一直有内容
  23. if ("hasPrimaryClip".equals(method.getName())) {
  24. return true;
  25. }
  26. return method.invoke(base, args);
  27. }
  28. }

注意,我们拿到原始的IBinder对象之后,如果我们希望使用被Hook之前的系统服务,并不能直接使用这个IBinder对象,而是需要使用asInterface方法将它转换为IClipboard接口;
因为getService方法返回的IBinder实际上是一个裸Binder代理对象,它只有与驱动打交道的能力,但是它并不能独立工作,需要人指挥它;asInterface方法返回的IClipboard.Stub.Proxy类的对象通过操纵这个裸BinderProxy对象从而实现了具体的IClipboard接口定义的操作。

伪造IBinder 对象

在上一步中,我们已经伪造好了系统服务对象,现在要做的就是想办法让asInterface方法返回我们伪造的对象了;我们伪造一个IBinder对象:

  1. public class BinderProxyHookHandler implements InvocationHandler {
  2. private static final String TAG = "BinderProxyHookHandler";
  3. // 绝大部分情况下,这是一个BinderProxy对象
  4. // 只有当Service和我们在同一个进程的时候才是Binder本地对象
  5. // 这个基本不可能
  6. IBinder base;
  7. Class<?> stub;
  8. Class<?> iinterface;
  9. public BinderProxyHookHandler(IBinder base) {
  10. this.base = base;
  11. try {
  12. this.stub = Class.forName("android.content.IClipboard$Stub");
  13. this.iinterface = Class.forName("android.content.IClipboard");
  14. } catch (ClassNotFoundException e) {
  15. e.printStackTrace();
  16. }
  17. }
  18. @Override
  19. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  20. if ("queryLocalInterface".equals(method.getName())) {
  21. Log.d(TAG, "hook queryLocalInterface");
  22. // 这里直接返回真正被Hook掉的Service接口
  23. // 这里的 queryLocalInterface 就不是原本的意思了
  24. // 我们肯定不会真的返回一个本地接口, 因为我们接管了 asInterface方法的作用
  25. // 因此必须是一个完整的 asInterface 过的 IInterface对象, 既要处理本地对象,也要处理代理对象
  26. // 这只是一个Hook点而已, 它原始的含义已经被我们重定义了; 因为我们会永远确保这个方法不返回null
  27. // 让 IClipboard.Stub.asInterface 永远走到if语句的else分支里面
  28. return Proxy.newProxyInstance(proxy.getClass().getClassLoader(),
  29. // asInterface 的时候会检测是否是特定类型的接口然后进行强制转换
  30. // 因此这里的动态代理生成的类型信息的类型必须是正确的
  31. new Class[] { IBinder.class, IInterface.class, this.iinterface },
  32. new BinderHookHandler(base, stub));
  33. }
  34. Log.d(TAG, "method:" + method.getName());
  35. return method.invoke(base, args);
  36. }
  37. }

我们使用动态代理的方式伪造了一个跟原始IBinder一模一样的对象,然后在这个伪造的IBinder对象的queryLocalInterface方法里面返回了我们第一步创建的伪造过的系统服务对象;

替换ServiceManager的IBinder对象

现在就是万事具备,只欠东风了;我们使用反射的方式修改ServiceManager类里面缓存的Binder对象,使得getService方法返回我们伪造的IBinder对象,进而asInterface方法使用伪造IBinder对象的queryLocalInterface方法返回了我们伪造的系统服务对象。代码较简单,如下:

  1. final String CLIPBOARD_SERVICE = "clipboard";
  2. // 下面这一段的意思实际就是: ServiceManager.getService("clipboard");
  3. // 只不过 ServiceManager这个类是@hide的
  4. Class<?> serviceManager = Class.forName("android.os.ServiceManager");
  5. Method getService = serviceManager.getDeclaredMethod("getService", String.class);
  6. // ServiceManager里面管理的原始的Clipboard Binder对象
  7. // 一般来说这是一个Binder代理对象
  8. IBinder rawBinder = (IBinder) getService.invoke(null, CLIPBOARD_SERVICE);
  9. // Hook 掉这个Binder代理对象的 queryLocalInterface 方法
  10. // 然后在 queryLocalInterface 返回一个IInterface对象, hook掉我们感兴趣的方法即可.
  11. IBinder hookedBinder = (IBinder) Proxy.newProxyInstance(serviceManager.getClassLoader(),
  12. new Class<?>[] { IBinder.class },
  13. new BinderProxyHookHandler(rawBinder));
  14. // 把这个hook过的Binder代理对象放进ServiceManager的cache里面
  15. // 以后查询的时候 会优先查询缓存里面的Binder, 这样就会使用被我们修改过的Binder了
  16. Field cacheField = serviceManager.getDeclaredField("sCache");
  17. cacheField.setAccessible(true);
  18. Map<String, IBinder> cache = (Map) cacheField.get(null);
  19. cache.put(CLIPBOARD_SERVICE, hookedBinder);

接下来,在app里面使用剪切版,比如长按进行粘贴之后,剪切版的内容永远都是you are hooked了;这样,我们Hook系统服务的目的宣告完成!详细的代码参见 github

LocationManager的hook可以参考这个例子,跟上面的基本上大同小异。

总结

  1. // 1、获取原始的IBinder对象
  2. IBinder b = ServiceManager.getService("service_name");
  3. // 2、转换为Service接口
  4. IXXInterface in = IXXInterface.Stub.asInterface(b);

参考

Android插件化原理解析——概要
Android插件化原理解析——Hook机制之Binder Hook

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注