Android Activity Deeplink啟動來源獲取原始碼分析

vivo網際網路技術發表於2021-11-16

一、前言

目前有很多的業務模組提供了Deeplink服務,Deeplink簡單來說就是對外部應用提供入口。

針對不同的跳入型別,app可能會選擇提供不一致的服務,這個時候就需要對外部跳入的應用進行區分。一般來講,我們會使用反射來呼叫Acticity中的mReferrer欄位來獲取跳轉來源的包名。

具體程式碼如下;

/**
 * 通過反射獲取referrer
 * @return
 */
private String reflectGetReferrer() {
    try {
        Field referrerField =
        Activity.class.getDeclaredField("mReferrer");
        referrerField.setAccessible(true);
        return (String) referrerField.get(this);
    } catch (NoSuchFieldException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }
    return "";
}

但是mReferrer有沒有被偽造的可能呢?

一旦mReferrer被偽造,輕則業務邏輯出錯,重則造成經濟損失,針對這種情況,有沒有辦法找到一種較為安全的來源獲取方法呢?

這就需要對mReferrer的來源進行一次分析。下面我們來進行一次mReferrer來源的另類原始碼分析。之所以說另類,是因為這次會大量使用除錯手段來逆向進行原始碼分析。

二、mReferrer從哪裡來

2.1 搜尋mReferrer,來源回溯

使用搜尋功能來搜尋Activity類中的mReferrer;使用 Find Usages 功能來查詢mReferrer欄位。

在Activity的Attach方法中對mReferrer做了賦值。

2.2 使用斷點除錯跟蹤呼叫棧

我們在Attach方法上新增斷點,通過斷點來跟蹤Attach的呼叫;

紅框中就是Attach的呼叫路徑,該呼叫棧在主執行緒中執行;從呼叫棧中看出Attach是ActivityThread.performLaunchActivity呼叫的。

performLaunchActivity呼叫Attach時傳入的是r的referrer引數,r是一個ActivityClientRecord物件。

我們進一步找到ActivityClientRecord中對referrer賦值的地方,就是ActivityClientRecord的建構函式。

在建構函式中新增斷點,檢視呼叫棧;

發現ActivityClientRecord在LaunchActivityItem的execute中被例項化,並且傳入的是LaunchActivityItem的mReferrer。

LaunchActivityItem的mReferrer是在setValues方法中賦值的,我們需要通過除錯來看setValues是被誰呼叫的。當我們使用常規方式斷點檢視setValues的呼叫方時,我們會發現這樣一種情況。

說明LaunchActivityItem在本地程式中,是一個被序列化後反序列化生成的物件。

在Activity中,序列化物件傳輸通常是使用binder來完成的,而binder的服務端是在System程式中。這裡實現了反序列化,那麼在遠端的binder服務中一定有序列化的過程。我們可以在System程式中除錯這個斷點,應該就是序列化的過程。

2.3 斷點除錯

對System程式除錯的方式也比較簡單;

  • step1:下載安裝Android自帶的X86模擬器(注意一定要安裝google api版本,play版本不支援除錯system程式)。

  • step2:在除錯的時候選擇System程式。

通過除錯,我們找到賦值堆疊(注意這裡堆疊顯示的程式已經是Binder程式了)。

我們根據這個堆疊的指示,一步一步的跟進,這裡需要注意一下,我們在檢視除錯堆疊的時候,只需要關注類名和方法名就可以了,不用刻意去關注堆疊中的行號,因為行號不一定準確。如果除錯過程中發現差異太大,可以嘗試更換一個模擬器版本。

這裡跟進到ActivityStackSupervisor的realStartActivityLoacked方法。

在ActivityStackSupervisor中,我們發現這個引數是由r.LaunchedFromPackage的來的,這個r是ActivityRecord,查詢LaunchedFromPackage的賦值的地方,最終找到ActivityRecord的初始化方法。

2.4 物件例項化過程

在初始化方法中新增斷點進行堆疊除錯;

跟著堆疊一步一步的看,到了ActivityStarter的execute方法裡面,這裡可以看到package的來源是mRequest.callingPackage。

通過搜尋Request的callingPackage物件對的Vaule write,mRequest.callingPackage的來源是ActivityStarter的setCallingPackage方法,一定是呼叫了setCallingPackage方法來實現了callingPackage內容的注入。

再看上一步驟中的堆疊,呼叫該方法的是ActivityTaskManagerService的startActivity方法;startActivity在構建時使用setCallingPackage傳入了package。與我們之前的猜測是一致的。

分析到這裡已經接近真相了。

2.5 遠端服務Binder呼叫的分析

我們都知道ActivityTaskManagerService是一個遠端服務,從它工作的程式就可以看出來,是一個binder程式。因為ActivityTaskManagerService extends IActivityTaskManager.Stub,那我們就要去找IActivityTaskManager.Stub被遠端呼叫的地方。

要想找他遠端呼叫的地方,我們就要先找到IActivityTaskManager.Stub是如何被呼叫方拿到的。

全域性搜尋IActivityTaskManager.Stub或者搜尋IActivityTaskManager.Stub.asInterface,這裡為了方便使用了線上的Android原始碼搜尋平臺。

我們在ActivityTaskManager中找到如下程式碼;

@TestApi
@SystemService(Context.ACTIVITY_TASK_SERVICE)
public class ActivityTaskManager {
 
    ActivityTaskManager(Context context, Handler handler) {
    }
 
    /** @hide */
    public static IActivityTaskManager getService() {
        return IActivityTaskManagerSingleton.get();
    }
 
    @UnsupportedAppUsage(trackingBug = 129726065)
    private static final Singleton<IActivityTaskManager> IActivityTaskManagerSingleton =
            new Singleton<IActivityTaskManager>() {
                @Override
                protected IActivityTaskManager create() {
                    final IBinder b = ServiceManager.getService(Context.ACTIVITY_TASK_SERVICE);
                    //這裡生成了遠端呼叫物件
                    return IActivityTaskManager.Stub.asInterface(b);
                }
            };
 
}

也就是說通過ActivityTaskManager.getService()方法可以拿到IActivityTaskManager.Stub的遠端呼叫控制程式碼。

於是ActivityTaskManagerService的startActivity方法呼叫的寫法應該是ActivityTaskManager.getService().startActivity,下一步的計劃是找到這個方法呼叫的地方 。

2.6 萬能的搜尋並不萬能

按照正常的思路,我們會再來使用搜尋功能在這個線上原始碼網站上搜尋一下ActivityTaskManager.getService().startActivity。

搜尋不到?這裡一定要注意,因為startActivity方法裡面有很多引數,很可能程式碼被換行,一旦被換行,搜尋ActivityTaskManager.getService().startActivity就不能搜到了。

搜尋也不是萬能的,我們還是考慮加斷點試試。

那麼斷點應該加在哪裡呢?我們是否可以將斷點加在ActivityTaskManagerService的startActivity上呢?

答案是不行,如果你嘗試去在一個binder程式呼叫(遠端服務呼叫 )的方法上面新增斷點。那麼你只會得到如下呼叫棧。

很顯然呼叫棧直接指向了 binder遠端,這不是我們想要的呼叫棧。我們知道,呼叫startActivity的原始碼一定是ActivityTaskManager.getService().startActivity。

而這行程式碼一定是在App的程式中呼叫的,屬於binder的客戶端呼叫,因此我們試著在getService()上面加一個斷點試試。這裡加了斷點之後也要注意一下,因為這個時候的startActivity應該是攻擊方呼叫的,也就是調起Deeplink的應用呼叫的。

所以。我們需要對Deeplink的發起方進行除錯。我們可以寫一個Demo來進行除錯。

點選按鈕來發起Deeplink,然後進行斷點,這個時候就能找到如下堆疊。

點選下一步斷點(Step Over)剛好就是ActivityTaskManager.getService().startActivity的方法呼叫。

於是我們得到如下呼叫棧;

ContextImpl.startActivty()
Instrumentation.execStartActivity()
ActivityTaskManager.getService()
                    .startActivity(whoThread, who.getBasePackageName(), intent,
                     intent.resolveTypeIfNeeded(who.getContentResolver()),
                     token, target != null ? target.mEmbeddedID : null,
                     requestCode, 0, null, options);

這邊就找到了 可以看到,callingPackage正是使用getBasePackageName方法來實現的。who就是context,也就是我們的Activity。

到這裡就可以確認 mReferrer其實就是使用context的getBasePackageName()來實現的。

三、如何避免包名被偽造

3.1 關注PID和Uid

如何來防止PackageName被偽造呢?

在我們除錯ActivityRecord的時候,我們發現ActivityRecord的屬性中還有PID和Uid;

只要拿到這個Uid,我們就可以根據Uid呼叫packageManager的方法來獲取對應Uid的報名。

3.2 調研Uid是否有偽造的可能性

下面就是要驗證一下Uid是否有被偽造的可能了。除錯查詢Uid的來源,在ActivityRecord的初始化方法中斷點檢視callingUid的來源。

我們發現 這個Uid其實是在ActivityStarter裡面使用Binder.getCallingUid得到的。Binder程式可不是應用層面可以干涉的了,我們可以放心大膽的使用這個Uid,不用擔心被偽造,剩下的就是如何使用Uid獲取PackageName了。

3.3 使用Uid置換PackageName

我們檢索程式碼,發現ActivityTaskManagerService恰好提供了獲取Uid的方法。

所以我們需要拿到ActivityTaskManagerService引用,搜尋IActivityTaskManager.Stub。

ActivityTaskManager是無法在app層引用的(是一個hide的類,但其實也是有辦法的,大家可以自己去探索一下)。

我們繼續查詢;

最終發現ActivityManager提供了這麼一個方法來獲取ActivityTaskManagerService,但是很不幸,getTaskService是一個黑名單方法,被禁止呼叫。

最後我們發現ActivityTaskManagerService的getLaunchedFromUid方法其實是被ActivityManageService包裝了一下的。

public class ActivityManagerService extends IActivityManager.Stub
        implements Watchdog.Monitor, BatteryStatsImpl.BatteryCallback {
 
 
    @VisibleForTesting
    public ActivityTaskManagerService mActivityTaskManager;
 
 
    @Override
    public boolean updateConfiguration(Configuration values) {
        return mActivityTaskManager.updateConfiguration(values);
    }
 
    @Override
    public int getLaunchedFromUid(IBinder activityToken) {
        return mActivityTaskManager.getLaunchedFromUid(activityToken);
    }
 
    public String getLaunchedFromPackage(IBinder activityToken) {
        return mActivityTaskManager.getLaunchedFromPackage(activityToken);
    }
 
}

所以可以使用ActivityManageService來呼叫就可以了,程式碼如下(注意不同的系統的版本可能程式碼並不一樣)。

private String reRealPackage() {
    try {
        Method getServiceMethod = ActivityManager.class.getMethod("getService");
        Object sIActivityManager = getServiceMethod.invoke(null);
        Method sGetLaunchedFromUidMethod = sIActivityManager.getClass().getMethod("getLaunchedFromUid", IBinder.class);
        Method sGetActivityTokenMethod = Activity.class.getMethod("getActivityToken");
        IBinder binder = (IBinder) sGetActivityTokenMethod.invoke(this);
        int uid = (int) sGetLaunchedFromUidMethod.invoke(sIActivityManager, binder);
        return getPackageManager().getPackagesForUid(uid)[0];
    } catch (Exception e) {
        e.printStackTrace();
    }
    return "null";
}

使用Uid來置換PackageName是不是就萬無一失了呢?這裡面是否還有其他玄機?這裡先賣個關子,小夥伴們可以在評論區討論一下。

四、總結

mReferrer很容易通過重寫context的getBasePackageName()被偽造,在使用時一定要小心。通過ActivityManageService獲取的Uid是無法被偽造的,可以考慮使用Uid來轉換PackageName。

作者:vivo網際網路客戶端團隊-Chen Long

相關文章