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

一、前言

目前有很多的業務模塊提供了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字段。

image

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

2.2 使用斷點調試跟蹤調用棧

我們在Attach方法上添加斷點,通過斷點來跟蹤Attach的調用;

image

紅框中就是Attach的調用路徑,該調用棧在主線程中執行;從調用棧中看出Attach是ActivityThread.performLaunchActivity調用的。

image

performLaunchActivity調用Attach時傳入的是r的referrer參數,r是一個ActivityClientRecord對象。

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

image

在構造函數中添加斷點,查看調用棧;

image

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

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

image

說明LaunchActivityItem在本地進程中,是一個被序列化后反序列化生成的對象。

在Activity中,序列化對象傳輸通常是使用binder來完成的,而binder的服務端是在System進程中。這里實現了反序列化,那么在遠端的binder服務中一定有序列化的過程。我們可以在System進程中調試這個斷點,應該就是序列化的過程。

2.3 斷點調試

對System進程調試的方式也比較簡單;

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

  • step2:在調試的時候選擇System進程。

image

通過調試,我們找到賦值堆棧(注意這里堆棧顯示的進程已經是Binder進程了)。

image

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

這里跟進到ActivityStackSupervisor的realStartActivityLoacked方法。

image

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

2.4 對象實例化過程

在初始化方法中添加斷點進行堆棧調試;

image

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

image

通過搜索Request的callingPackage對象對的Vaule write,mRequest.callingPackage的來源是ActivityStarter的setCallingPackage方法,一定是調用了setCallingPackage方法來實現了callingPackage內容的注入。

image

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

image

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

2.5 遠程服務Binder調用的分析

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

要想找他遠程調用的地方,我們就要先找到IActivityTaskManager.Stub是如何被調用方拿到的。

全局搜索IActivityTaskManager.Stub或者搜索IActivityTaskManager.Stub.asInterface,這里為了方便使用了在線的Android源碼搜索平臺。

image

我們在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進程調用(遠程服務調用 )的方法上面添加斷點。那么你只會得到如下調用棧。

image

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

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

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

image

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

image

點擊下一步斷點(Step Over)剛好就是ActivityTaskManager.getService().startActivity的方法調用。

image

于是我們得到如下調用棧;

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;

image

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

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

下面就是要驗證一下Uid是否有被偽造的可能了。調試查找Uid的來源,在ActivityRecord的初始化方法中斷點查看callingUid的來源。

image

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

3.3 使用Uid置換PackageName

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

image

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

image

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

我們繼續查找;

image

最終發現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

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,565評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,115評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,577評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,514評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,234評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,621評論 1 326
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,641評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,822評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,380評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,128評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,319評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,879評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,548評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,970評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,229評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,048評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,285評論 2 376

推薦閱讀更多精彩內容