DroidAssist(一):当我们hook ServiceManager中服务的前后事
背景
最近在看DroidAssist的代码,刚好在翻看以前同事写的旧帖子的时候,发现了一个通过hook快速进行合规改造的帖子,可以通过DroidAssist从另一个角度解决,我们先来看一下原来的实现,它的步骤如下:
- 需要解决的问题是获取deviceId等敏感信息需要被禁止,老代码要改造。
- 首先通过将AndroidManifests.xml中READ_PHONE_STATE的node设为“remove”
<uses-permission android:name="android.permission.READ_PHONE_STATE" tools:node="remove" />
- 高版本类似于telephonyManager.getDeviceId()就会抛出异常
- 创建你需要的代理binder如ITelephony,将其中的的queryLocalInterface与asInterface后的Object给代理。
- 反射获取到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);
}
}
我们分析一下上面的代码,
- 检查uid与包名是否匹配,不对则抛出异常,但是这个安全检查异常和我们看到的异常一致。
- 获取电话对象。
- 获取子ID
- 权限检查:检查调用该方法的应用程序是否有权限读取设备标识符。
- 获取设备标识符。
回想我们如果要在低版本触发这个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里的说明:
- 如果调用方具有READ_PRIVILEGED_PHONE_STATE权限,则调用方返回true
- 抛出SecurityException:如果调用者不满足任何要求并且Android版本为Q,或pre-Q但是没有READ_PHONE_STATE权限。
- 返回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的后续文章,敬请关注,持续学习,每天进步一点点!