DroidAssist(一):当我们hook ServiceManager中服务的前后事

202 阅读9分钟

DroidAssist(一):当我们hook ServiceManager中服务的前后事

背景

最近在看DroidAssist的代码,刚好在翻看以前同事写的旧帖子的时候,发现了一个通过hook快速进行合规改造的帖子,可以通过DroidAssist从另一个角度解决,我们先来看一下原来的实现,它的步骤如下:

  1. 需要解决的问题是获取deviceId等敏感信息需要被禁止,老代码要改造。
  2. 首先通过将AndroidManifests.xml中READ_PHONE_STATE的node设为“remove”
<uses-permission android:name="android.permission.READ_PHONE_STATE" tools:node="remove" />
  1. 高版本类似于telephonyManager.getDeviceId()就会抛出异常
  2. 创建你需要的代理binder如ITelephony,将其中的的queryLocalInterface与asInterface后的Object给代理。
  3. 反射获取到ServiceManager将其中的sCache里所注册的服务binder替换为你的代理binder

具体的代码就不给出来了,可以参考这篇文章: www.zybuluo.com/TryLoveCatc…

复现及查因

复现

想要理解大佬的思路最好的方法就是复现啦,我们尝试去复刻问题场景,准备一个高版本手机,将telephony权限禁掉之后获取deviceId:

TelephonyManager telephonyManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
@SuppressLint("MissingPermission")
String deviceId = telephonyManager.getDeviceId();
Log.d(TAG, "deviceID: " + deviceId);

查看日志发现crash信息如下

java.lang.RuntimeException: Unable to start activity ComponentInfo{com.didichuxing.tools.droidassist/com.didichuxing.tools.test.MainActivity}: java.lang.SecurityException: getDeviceId: The uid 10180 does not meet the requirements to access device identifiers.
    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3920)
    at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:4073)
    at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:101)
    at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
    at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2426)
    at android.os.Handler.dispatchMessage(Handler.java:106)
    at android.os.Looper.loopOnce(Looper.java:211)
    at android.os.Looper.loop(Looper.java:300)
    at android.app.ActivityThread.main(ActivityThread.java:8503)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:561)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:954)
Caused by: java.lang.SecurityException: getDeviceId: The uid 10180 does not meet the requirements to access device identifiers.
    at android.os.Parcel.createExceptionOrNull(Parcel.java:3011)
    at android.os.Parcel.createException(Parcel.java:2995)
    at android.os.Parcel.readException(Parcel.java:2978)
    at android.os.Parcel.readException(Parcel.java:2920)
    at com.android.internal.telephony.ITelephony$Stub$Proxy.getDeviceIdWithFeature(ITelephony.java:10713)
    at android.telephony.TelephonyManager.getDeviceId(TelephonyManager.java:2103)
    at com.didichuxing.tools.test.MainActivity.onCreate(MainActivity.java:45)
    at android.app.Activity.performCreate(Activity.java:8589)
    at android.app.Activity.performCreate(Activity.java:8553)
    at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1445)
    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3901)
    at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:4073) 
    at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:101) 
    at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135) 
    at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95) 
    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2426) 
    at android.os.Handler.dispatchMessage(Handler.java:106) 
    at android.os.Looper.loopOnce(Looper.java:211) 
    at android.os.Looper.loop(Looper.java:300) 
    at android.app.ActivityThread.main(ActivityThread.java:8503) 
    at java.lang.reflect.Method.invoke(Native Method) 
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:561) 
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:954

查因

我们去查看TelephonyManager.getDeviceId()方法,我的AndroidSDK版本是28,不同版本可能有些差异,但是不影响大体逻辑。

public String getDeviceId() {
    try {
        ITelephony telephony = getITelephony();
        if (telephony == null)
            return null;
        return telephony.getDeviceId(mContext.getOpPackageName());
    } catch (RemoteException ex) {
        return null;
    } catch (NullPointerException ex) {
        return null;
    }
}
private ITelephony getITelephony() {
    return ITelephony.Stub.asInterface(ServiceManager.getService(Context.TELEPHONY_SERVICE));
}

这里的ITelephony是通过binder获取的接口,所以这个服务很可能是跨进程进行的,而这个binder又是通过ServiceManager获取到的。
这里我们可以分成两方面查看,第一个是异常到底是哪里抛出来的,第二是ServiceManager是如何管理这些服务或者说是我们这个Itelephony对应的binder的。

异常抛出点

我们现在来查看一下ITelephony对应的实际工作代码。
可以查到ITelephony是由PhoneInterfaceManager去实现的(便捷的话可以直接问ai),来看一下PhoneInterfaceManager对应的实现:

public String getDeviceIdWithFeature(String callingPackage, String callingFeatureId) {
        try {
            mAppOps.checkPackage(Binder.getCallingUid(), callingPackage);
        } catch (SecurityException se) {
            EventLog.writeEvent(0x534e4554, "186530889", Binder.getCallingUid());
            throw new SecurityException("Package " + callingPackage + " does not belong to "
                    + Binder.getCallingUid());
        }
        final Phone phone = PhoneFactory.getPhone(0);
        if (phone == null) {
            return null;
        }
        int subId = phone.getSubId();
        if (!TelephonyPermissions.checkCallingOrSelfReadDeviceIdentifiers(mApp, subId,
                callingPackage, callingFeatureId, "getDeviceId")) {
            return null;
        }

        final long identity = Binder.clearCallingIdentity();
        try {
            return phone.getDeviceId();
        } finally {
            Binder.restoreCallingIdentity(identity);
        }
    }

我们分析一下上面的代码,

  1. 检查uid与包名是否匹配,不对则抛出异常,但是这个安全检查异常和我们看到的异常一致。
  2. 获取电话对象。
  3. 获取子ID
  4. 权限检查:检查调用该方法的应用程序是否有权限读取设备标识符。
  5. 获取设备标识符。

回想我们如果要在低版本触发这个crash就是要移除权限,那么第四点就很有可能,接着往下看

    /**
     * Check whether the caller (or self, if not processing an IPC) can read device identifiers.
     *
     * <p>This method behaves in one of the following ways:
     * <ul>
     *   <li>return true: if the caller has the READ_PRIVILEGED_PHONE_STATE permission, the calling
     *       package passes a DevicePolicyManager Device Owner / Profile Owner device identifier
     *       access check, or the calling package has carrier privileges on any active
     *       subscription, or the calling package has the {@link
     *       Manifest.permission#USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER} appop permission.
     *   <li>throw SecurityException: if the caller does not meet any of the requirements and is
     *       targeting Q or is targeting pre-Q and does not have the READ_PHONE_STATE permission
     *       or carrier privileges of any active subscription.
     *   <li>return false: if the caller is targeting pre-Q and does have the READ_PHONE_STATE
     *       permission or carrier privileges. In this case the caller would expect to have access
     *       to the device identifiers so false is returned instead of throwing a SecurityException
     *       to indicate the calling function should return placeholder data.
     * </ul>
     */
    public static boolean checkCallingOrSelfReadDeviceIdentifiers(Context context, int subId,
            String callingPackage, @Nullable String callingFeatureId, String message) {
        if (checkCallingOrSelfUseIccAuthWithDeviceIdentifier(context, callingPackage,
                callingFeatureId, message)) {
            return true;
        }
        return checkPrivilegedReadPermissionOrCarrierPrivilegePermission(
                context, subId, callingPackage, callingFeatureId, message, true, true);
    }

首先我们看一下javadoc里的说明:

  1. 如果调用方具有READ_PRIVILEGED_PHONE_STATE权限,则调用方返回true
  2. 抛出SecurityException:如果调用者不满足任何要求并且Android版本为Q,或pre-Q但是没有READ_PHONE_STATE权限。
  3. 返回false:如果调用者的目标是pre-Q并且有READ_PHONE_STATE

我们可以了解到,Q(29)版本之前使用READ_PHONE_STATE会返回false,Q之后不管是否添加权限(除了READ_PRIVILEGED_PHONE_STATE)都会抛出异常。
再看一下具体的方法实现,总共两个方法,第一个是检查调用者是否具有ICC授权,显然我们都不清楚这是个什么授权,第二个方法是要检查我们是否具有特权权限,也就是READ_PHONE_STATE,那么我们来看一下这个权限校验方法。

/**
     * Checks whether the app with the given pid/uid can read device identifiers.
     *
     * <p>This method behaves in one of the following ways:
     * <ul>
     *   <li>return true: if the caller has the READ_PRIVILEGED_PHONE_STATE permission, the calling
     *       package passes a DevicePolicyManager Device Owner / Profile Owner device identifier
     *       access check; or the calling package has carrier privileges on the specified
     *       subscription; or allowCarrierPrivilegeOnAnySub is true and has carrier privilege on
     *       any active subscription.
     *   <li>throw SecurityException: if the caller does not meet any of the requirements and is
     *       targeting Q or is targeting pre-Q and does not have the READ_PHONE_STATE permission.
     *   <li>return false: if the caller is targeting pre-Q and does have the READ_PHONE_STATE
     *       permission. In this case the caller would expect to have access to the device
     *       identifiers so false is returned instead of throwing a SecurityException to indicate
     *       the calling function should return placeholder data.
     * </ul>
     */
    private static boolean checkPrivilegedReadPermissionOrCarrierPrivilegePermission(
            Context context, int subId, String callingPackage, @Nullable String callingFeatureId,
            String message, boolean allowCarrierPrivilegeOnAnySub, boolean reportFailure) {
        int uid = Binder.getCallingUid();
        int pid = Binder.getCallingPid();

        // If the calling package has carrier privileges for specified sub, then allow access.
        if (checkCarrierPrivilegeForSubId(context, subId)) return true;

        // If the calling package has carrier privileges for any subscription
        // and allowCarrierPrivilegeOnAnySub is set true, then allow access.
        if (allowCarrierPrivilegeOnAnySub && checkCarrierPrivilegeForAnySubId(context, uid)) {
            return true;
        }

        LegacyPermissionManager permissionManager = (LegacyPermissionManager)
                context.getSystemService(Context.LEGACY_PERMISSION_SERVICE);
        try {
            if (permissionManager.checkDeviceIdentifierAccess(callingPackage, message,
                    callingFeatureId,
                    pid, uid) == PackageManager.PERMISSION_GRANTED) {
                return true;
            }
        } catch (SecurityException se) {
            throwSecurityExceptionAsUidDoesNotHaveAccess(message, uid);
        }

        if (reportFailure) {
            return reportAccessDeniedToReadIdentifiers(context, subId, pid, uid, callingPackage,
                    message);
        } else {
            return false;
        }
    }

我们当然一眼就看到了权限校验方法,但是它被catch住了,我们看一下catch后的处理:

private static void throwSecurityExceptionAsUidDoesNotHaveAccess(String message, int uid) {
        throw new SecurityException(message + ": The uid " + uid
                + " does not meet the requirements to access device identifiers.");
    }

哇,我们找到了异常信息的抛出地方,这部分结束。

ServiceManager

结合资料可以知道,ServiceManager在binder进程通信中起一个获取/保存各种服务(比如AMS,WMS,PMS等)控制中心的作用,并且它是binder进程通信的服务端
Android系统在启动的时候会优先去启动ServiceManager,其他服务启动后会向ServiceManager中注册自己的服务,而ServiceManager运行在自己独立的进程中。
我们可以很容易的找到保存服务接口的地方:

    private static Map<String, IBinder> sCache = new ArrayMap<String, IBinder>();

再看一下我们获取服务的方法

public static IBinder getService(String name) {
        try {
            IBinder service = sCache.get(name);
            if (service != null) {
                return service;
            } else {
                return Binder.allowBlocking(rawGetService(name));
            }
        } catch (RemoteException e) {
            Log.e(TAG, "error in getService", e);
        }
        return null;
    }

可以看到首先查询sCache是否保存有该服务binder,如果没有则去查找该服务,获取服务的binder请求最终会到service_manager.c中的svcmgr_handler方法,但是我们不需要了解它的c实现。
一些c层的分析可以看一下这篇文章:juejin.cn/post/706252…

至此,我们可以梳理一下整个流程:
客户端向ServiceManager发送获取“phone”服务的请求->ServiceManager返回“phone”服务在系统启动时所注册的binder->客户端发送getDeviceId请求->PhoneInterfaceManager检查权限->权限校验失败,抛出异常->客户端crash。

ServiceManager hook

关于ServiceManager的hook服务这里不再赘述,开篇给出了文章链接,说明的很清楚了,这里提一下binder的hook。

binder hook

DEMO

我们来写一个IPay.aidl

interface IPay {
   boolean pay(in String orderId, in double amount);
   String getPaymentStatus(in String orderId);
}

在编译之后,看一下生成的IPay.java

/*
 * This file is auto-generated.  DO NOT MODIFY.
 */
package com.example.myapplication;
// Declare any non-default types here with import statements
// IPay.aidl

public interface IPay extends android.os.IInterface
{
  /** Default implementation for IPay. */
  public static class Default implements com.example.myapplication.IPay
  {
    @Override public boolean pay(java.lang.String orderId, double amount) throws android.os.RemoteException
    {
      return false;
    }
    @Override public java.lang.String getPaymentStatus(java.lang.String orderId) throws android.os.RemoteException
    {
      return null;
    }
    @Override
    public android.os.IBinder asBinder() {
      return null;
    }
  }
  /** Local-side IPC implementation stub class. */
  public static abstract class Stub extends android.os.Binder implements com.example.myapplication.IPay
  {
    private static final java.lang.String DESCRIPTOR = "com.example.myapplication.IPay";
    /** Construct the stub at attach it to the interface. */
    public Stub()
    {
      this.attachInterface(this, DESCRIPTOR);
    }
    /**
     * Cast an IBinder object into an com.example.myapplication.IPay interface,
     * generating a proxy if needed.
     */
    public static com.example.myapplication.IPay asInterface(android.os.IBinder obj)
    {
      if ((obj==null)) {
        return null;
      }
      android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
      if (((iin!=null)&&(iin instanceof com.example.myapplication.IPay))) {
        return ((com.example.myapplication.IPay)iin);
      }
      return new com.example.myapplication.IPay.Stub.Proxy(obj);
    }
    @Override public android.os.IBinder asBinder()
    {
      return this;
    }
    @Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
    {
      java.lang.String descriptor = DESCRIPTOR;
      switch (code)
      {
        case INTERFACE_TRANSACTION:
        {
          reply.writeString(descriptor);
          return true;
        }
        case TRANSACTION_pay:
        {
          data.enforceInterface(descriptor);
          java.lang.String _arg0;
          _arg0 = data.readString();
          double _arg1;
          _arg1 = data.readDouble();
          boolean _result = this.pay(_arg0, _arg1);
          reply.writeNoException();
          reply.writeInt(((_result)?(1):(0)));
          return true;
        }
        case TRANSACTION_getPaymentStatus:
        {
          data.enforceInterface(descriptor);
          java.lang.String _arg0;
          _arg0 = data.readString();
          java.lang.String _result = this.getPaymentStatus(_arg0);
          reply.writeNoException();
          reply.writeString(_result);
          return true;
        }
        default:
        {
          return super.onTransact(code, data, reply, flags);
        }
      }
    }
    private static class Proxy implements com.example.myapplication.IPay
    {
      private android.os.IBinder mRemote;
      Proxy(android.os.IBinder remote)
      {
        mRemote = remote;
      }
      @Override public android.os.IBinder asBinder()
      {
        return mRemote;
      }
      public java.lang.String getInterfaceDescriptor()
      {
        return DESCRIPTOR;
      }
      @Override public boolean pay(java.lang.String orderId, double amount) throws android.os.RemoteException
      {
        android.os.Parcel _data = android.os.Parcel.obtain();
        android.os.Parcel _reply = android.os.Parcel.obtain();
        boolean _result;
        try {
          _data.writeInterfaceToken(DESCRIPTOR);
          _data.writeString(orderId);
          _data.writeDouble(amount);
          boolean _status = mRemote.transact(Stub.TRANSACTION_pay, _data, _reply, 0);
          if (!_status && getDefaultImpl() != null) {
            return getDefaultImpl().pay(orderId, amount);
          }
          _reply.readException();
          _result = (0!=_reply.readInt());
        }
        finally {
          _reply.recycle();
          _data.recycle();
        }
        return _result;
      }
      @Override public java.lang.String getPaymentStatus(java.lang.String orderId) throws android.os.RemoteException
      {
        android.os.Parcel _data = android.os.Parcel.obtain();
        android.os.Parcel _reply = android.os.Parcel.obtain();
        java.lang.String _result;
        try {
          _data.writeInterfaceToken(DESCRIPTOR);
          _data.writeString(orderId);
          boolean _status = mRemote.transact(Stub.TRANSACTION_getPaymentStatus, _data, _reply, 0);
          if (!_status && getDefaultImpl() != null) {
            return getDefaultImpl().getPaymentStatus(orderId);
          }
          _reply.readException();
          _result = _reply.readString();
        }
        finally {
          _reply.recycle();
          _data.recycle();
        }
        return _result;
      }
      public static com.example.myapplication.IPay sDefaultImpl;
    }
    static final int TRANSACTION_pay = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
    static final int TRANSACTION_getPaymentStatus = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
    public static boolean setDefaultImpl(com.example.myapplication.IPay impl) {
      // Only one user of this interface can use this function
      // at a time. This is a heuristic to detect if two different
      // users in the same process use this function.
      if (Stub.Proxy.sDefaultImpl != null) {
        throw new IllegalStateException("setDefaultImpl() called twice");
      }
      if (impl != null) {
        Stub.Proxy.sDefaultImpl = impl;
        return true;
      }
      return false;
    }
    public static com.example.myapplication.IPay getDefaultImpl() {
      return Stub.Proxy.sDefaultImpl;
    }
  }
  public boolean pay(java.lang.String orderId, double amount) throws android.os.RemoteException;
  public java.lang.String getPaymentStatus(java.lang.String orderId) throws android.os.RemoteException;
}

我们的使用
服务方:

private final IPay.Stub mBinder = new IPay.Stub() {
    @Override
    public boolean pay(String orderId, double amount) {
        Log.d(TAG, "pay: " + orderId);
        // 实际支付逻辑(如调用支付 SDK)
        return true;
    }

    @Override
    public String getPaymentStatus(String orderId) {
        return "SUCCESS";
    }
};

客户端:

IPay? pay = IPay.Stub.asInterface(service)
hook 位置

由上面的代码我们可以看到客户端在拿到binder之后是通过asInterface方法拿到接口的进行通信的,来看一下这个方法的实现:

public static com.example.myapplication.IPay asInterface(android.os.IBinder obj)
{
  if ((obj==null)) {
    return null;
  }
  android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
  if (((iin!=null)&&(iin instanceof com.example.myapplication.IPay))) {
    return ((com.example.myapplication.IPay)iin);
  }
  return new com.example.myapplication.IPay.Stub.Proxy(obj);
}

可以看到首先通过queryLocalInterface方法去获取本地的实例,否则在去初始化,这个特性也就是我们代理的点,通过对queryLocalInterface和getDeviceId的两层代理实现自己的功能。

ServiceManager 反射

事情到这里就结束了吗?当然不是,我们还有个重要的问题没有解决,那就是按照我们的理解ServiceManager不是运行在自己的进程中,怎么会可以通过反射获取到呢?

事情的真相就是在每个app初始化的时候zygote都会初始化一份ServiceManager,其中的服务都是对独立进程ServiceManager的引入,在之前我们分析的代码中如果sCache没有的话就会去请求该服务,其实这里指的是如果app进程中的ServiceManager的sCache中没有的话就去请求独立进程的ServiceManager。

// ApplicationThread.java

// Setup the service cache in the ServiceManager
ServiceManager.initServiceCache(services);

DroidAssist

好了,终于到了我们的重点:DroidAssist,利用这款工具我们该怎么去解决这个问题呢,这里给出一个示例:

<TryCatchMethodCall>
    <Source>
        java.lang.String android.telephony.TelephonyManager.getDeviceId()
    </Source>
    <Exception>
        java.lang.SecurityException
    </Exception>
    <Target>
        android.util.Log.d("test", "catch call permission exception", $e);
    </Target>
</TryCatchMethodCall>

由于很多功能都是在运行阶段加载进来的,这里只能catch住调用方了。

结语

这篇文章算是对DroidAssist入坑的一个契机和使用场景,后续会继续发DroidAssist的后续文章,敬请关注,持续学习,每天进步一点点!

image.png