Android逆向之旅---Android中的sharedUserId屬性詳解

yangxi_001發表於2016-12-02

一、前言

今天我們來看一下Android中一個眾人熟悉的一個屬性:shareUserId,關於這個屬性可能大家都很熟悉了,最近在開發專案,用到了這個屬性,雖然知道一點知識,但是感覺還是有些迷糊,所以就寫篇文章來深入研究一下。

關於Android中的sharedUserId的概念這裡就簡單介紹一下:

Android給每個APK程式分配一個單獨的空間,manifest中的userid就是對應一個分配的Linux使用者ID,並且為它建立一個沙箱,以防止影

響其他應用程式(或者其他應用程式影響它)。使用者ID 在應用程式安裝到裝置中時被分配,並且在這個裝置中保持它的永久性。

通常,不同的APK會具有不同的userId,因此執行時屬於不同的程式中,而不同程式中的資源是不共享的,在保障了程式執行的穩定。然後在有些時候,我們自己開發了多個APK並且需要他們之間互相共享資源,那麼就需要通過設定shareUserId來實現這一目的。

通過Shared User id,擁有同一個User id的多個APK可以配置成執行在同一個程式中.所以預設就是可以互相訪問任意資料. 也可以配置成執行成不同的程式, 同時可以訪問其他APK的資料目錄下的資料庫和檔案.就像訪問本程式的資料一樣。

用法也很簡單:

在需要共享資源的專案的每個AndroidMainfest.xml中新增shareuserId的標籤。
android:sharedUserId="com.example"
id名自由設定,但必須保證每個專案都使用了相同的sharedUserId。一個mainfest只能有一個Shareuserid標籤。


二、問題延伸

我們今天先來看一個場景:Android中一個App如何能夠訪問到其他App的資訊和資源?

這個可能很多人感覺是兩個App之間的通訊,其實不是,比如我們在早期遇到支付寶有一個快捷支付,那麼我們會看到手機中會安裝兩個app,一個是支付寶app,一個是快捷支付app,那麼在開啟快捷支付的時候,就會呼叫快捷支付app等,大家可能會想到現在有一個比較流行的技術叫做外掛開發,的確如此,這個我在之前的文章也有說過,不清楚的同學可以點選這裡:Android中外掛開發篇

但是我們今天不說這個外掛怎麼搞,今天就來看看如何在一個app中去訪問另外一個app的程式碼和資源等資訊?

在說這個知識點之前,我們需要了解的一個知識點,就是我們可以通過一個包名來得到對應的Context的全域性變數,可以直接使用Context的一個靜態方法:createPackageContext

關於這個方法其實很簡單,他有兩個引數:

第一個引數:需要構造出來Context的包名字串

第二個引數:構造出來的Context的開啟模式

下面我們可以直接使用一個例子來看看效果:

首先我們弄一個外掛工程:ShareUserIdPlugin

這個工程很簡單,我們編譯安裝執行即可。


在弄一個宿主工程:ShareUserIdHost


這裡有一個核心方法,我們首先通過外掛工程的包名:cn.wjdiankong.shareuseridplugin;建立出一個Context物件。

這裡看到第二引數有兩個模式:

Context.CONTEXT_INCLUDE_CODE:這個標誌是在我們需要執行外掛中的某段程式碼需要加上的值。

CONTEXT_IGNORE_SECURITY:這個標誌是必須的,是忽視安全性,如果沒有這個值的話,那麼我們訪問什麼都是失敗的。

得到了Context變數之後,我們下面就可以通過反射來執行程式碼和獲取資源了,這裡需要注意的是,一定要先拿到Context對應的ClassLoader,然後才能載入對應的類,ClassLoader一定是Context的,是外掛工程中的類載入器。


下面我們執行結果看看:


執行成功了啦~~是不是很簡單呢。

下面如果我們把CONTEXT_INCLUDE_CODE去掉,在執行:


發現報錯了,找不到指定的類。所以如果想執行程式碼的話,這個值一定要加上。


我們再把CONTEXT_IGNORE_SECURITY去掉,執行結果:


看到了,爆出了安全錯誤,所以要想構造成功Context出來,必須要加上這個值。


三、步入正題

好了,到這裡我們就介紹瞭如何通過包名構造一個Context變數出來,然後執行對應的程式碼和獲取資源。那麼這個我們看到工程中貌似沒有用到shareUserId這個屬性呢?那這個和我們今天要介紹的知識點有什麼關係嗎?其實沒什麼關係,上面的例子只能說是做一個簡單的引子,那有些同學可能困惑了,為何都沒有使用shareUserId屬性,這兩件事還可以做呢?那豈不是很不安全?其實我們在接觸過逆向知識的時候會發現,關於Android中的一個App中的程式碼和資源說的直白點其實沒有安全性可言,比如,我想獲取一個一個app中的指定資源,可以使用反編譯或者直接解壓apk就可以得到,想看到app中的一段程式碼的含義或者執行結果,反編譯也可以做到,所以說這個說的直白點關於程式碼和資源在Android中其實沒什麼安全性可說。有辦法可以去搞定的。

當然我們在後面可以用這種構造Context的方式,去實現我們想要的一些功能,比如我們知道了一個app的資源名或者是方法名,想直接在我的工程中用,那麼可以使用這種方式就可以啦,不過這個還是很不靠譜的,當然也是一種方式,比如A應用實現了一個很複雜的一個方法,我自己的應用和他沒任何關係,但是也需要這個方法,那麼可以直接使用這種方式去呼叫即可。但是前提是A應用安裝了。當然正規公司的app都不會這麼傻逼的去做的,其實我們在研究逆向app的時候可能會用到哦~~


那麼說了這麼多,shareUserId的屬性的最大作用是什麼呢?

前面都說了,Android中每個app都對應一個uid,每個uid都有自己的一個沙箱,這是基於安全考慮的,那麼說到沙箱,我們會想到的是data/data/XXXX/目錄下面的所有資料,因為我們知道這個目錄下面的所有資料是一個應用私有的,一般情況下其他應用是沒有許可權訪問的,當然root之後是另外情況,這裡就不多說了。這裡只看沒有root的情況,下面我們在來看一個場景:

A應用和B應用都是一家公司的,現在想在A應用中能夠拿到B引用儲存的一些值,那麼這時候該怎麼辦呢?

這時候就需要用到了shareUserId屬性了,但是這裡我們在介紹shareUserId屬性前,我們先來看一個簡單的例子:

還是使用上面的兩個工程:

ShareUserIdPlugin中的MainActivity.java程式碼如下:


這裡很簡單,我們使用SharedPreferences來儲存一個密碼,注意模式是:Context.MODE_PRIVATE,關於這裡,有很多種模式,後面會詳細介紹。


下面在來看一下宿主工程中的程式碼,獲取密碼。

執行宿主工程結果:


我們看到執行結果列印出來了幾個值,我先不管其他的,看到最後pwd的值是預設值,那說明我們宿主工程中獲取外掛工程中的密碼失敗了。

我們在去看看外掛工程中那個shareperference的xml檔案的許可權:


這裡使用root了之後檢視的:-rw-rw----

關於這個值,不瞭解的同學可以網上去看一些資料:

Linux檔案許可權你分開三段來看:
首位代表是目錄還是檔案,一般不用管,後面的三段每段3位,r代表可讀,w代表可寫,x代表可執行,第一段是代表檔案所屬的使用者對它的許可權,第二段是所屬使用者組的使用者對它的許可權,第三段是其他使用者對它的許可權。
第一段:rw- ,所屬使用者(比如是root)對這個檔案可讀可寫
第二段:rw- ,所屬使用者組使用者,對這個檔案可讀可寫
第三段:--- ,其他使用者對這個檔案什麼都幹不了

那麼從上面的分析可以看出來,這個檔案對於其他使用者(不同uid的)訪問是失敗的。所以我們獲取密碼失敗。

那麼這個xml的許可權在哪裡設定的呢?其實就是在外掛工程中的那個建立SharedPreferences的時候:

其實Context提供了幾種模式:

1、Context.MODE_PRIVATE:為預設操作模式,代表該檔案是私有資料,只能被應用本身訪問,在該模式下,寫入的內容會覆         蓋原檔案的內容,如果想把新寫入的內容追加到原檔案中。可以使用Context.MODE_APPEND
2、Context.MODE_APPEND:模式會檢查檔案是否存在,存在就往檔案追加內容,否則就建立新檔案。
3、Context.MODE_WORLD_READABLE和Context.MODE_WORLD_WRITEABLE用來控制其他應用是否有許可權讀寫該檔案。
MODE_WORLD_READABLE:表示當前檔案可以被其他應用讀取;
MODE_WORLD_WRITEABLE:表示當前檔案可以被其他應用寫入


我們可以檢視原始碼ContextImpl.java


這裡獲取一個SharedPreferencesImpl物件,這個物件是實現了SharedPreferences介面的。這裡我們看到採用了快取機制,將xml的名字和sp物件一一對應起來,所以我們可以得知,一個app中,最好簡化xml的個數,儘量將值都定義到一個xml中,減少記憶體佔用。

我們在看看SharedPreferencesImpl.java類原始碼:


有一個全域性變數儲存了mode值,再看看mMode在哪裡用到了:


在writeToFile這個方法中用到了,這個方法其實後面會分析的,就是SP將記憶體中的值儲存到磁碟中。

然後再看看ContextImpl的setFilePermissionsFromMode方法:


好了,到這裡,我們可以看到,通過傳遞進來的mode值,來設定檔案的許可權。

那麼程式碼看完了,下面我們在改一下外掛工程中的那個建立sp的程式碼:

Context.MODE_WORLD_READABLE|Context.MODE_WORLD_WRITEABLE 為讀寫模式


再來測試一下:


看到這裡取出來密碼了,成功了,關於空指標後面會詳細介紹的,這裡先不管了。我們再來看一下sp的xml檔案許可權:


看到了,其他使用者是可以進行讀寫操作的了,所以取出來的密碼是成功的了。

到這裡我們就弄清楚了Context提供的那幾個建立sp檔案的幾種模式的區別,所以我們這裡也可以看到,這個模式很重要,對於安全性來說,不過這個預設模式就是private的,也是挺好的。


補充:

第一:不需要root來檢視sp檔案的許可權

前面我們看到我們是使用root之後檢視檔案的許可權的,其實還有一種方式,不root也是可以的,那就是run-as命令,關於這個命令不熟的同學可以自行google了,這個命令的作用是:可以檢視指定包名應用的data目錄下面的資料,也就是隻能檢視data/data/XXX/目錄下面的內容,而且他的侷限性也很大,只有debug模式下才能起作用,下面我們來看看怎麼使用:

run-as 需要檢視內容的應用包名


是不是這裡也是可以檢視的,但是他只能在debug下面才能使用,比如我們現在用它去檢視非debug的應用:


看到了吧,很蛋疼,非debug模式還不能用。好吧,不過這裡只是做了一個知識點的補充,記住有這個命令,在debug環境下也是蠻有用的。


第二:關於上面日誌中的異常是怎麼回事?

我們回去看看宿主工程中,用反射去訪問了SP內部的一些變數值。為什麼訪問這些呢?源於我之前除錯一個bug,但是這裡引出來了一些問題,下面就來分析一下。


為了分析,這裡我們還是需要去看SharePreferencesImpl原始碼:


程式碼邏輯不是很複雜,首先建立備份檔案,然後載入xml內容到記憶體的map物件,用於後面的getXXX方法直接獲取值,提高效率,然後將解析之後的map賦值給全域性的map物件,如果解析出來的map為空,那麼就直接賦值一個空資料的map。最後一行程式碼很重要,就是需要喚醒其他所有的wait地方,看完這段程式碼我們就可以很好理解上面的異常崩潰了:


首先檔案是可讀的,所以進入到了if語句中,開始解析xml到記憶體中,但是這時候需要注意的是,解析工作實在子執行緒中工作的,但是我們去訪問全域性map是在主執行緒做的,那麼這時候解析還沒有完成,那麼只能獲取到null值了,所以丟擲一個空指標,但是後面我們使用getString方法的時候,可以獲取到正確值了

下面我們來看看getString的原始碼:


看看awaitLoadedLocked方法:


這個方法什麼都沒幹,就是wait住了,等待喚醒,這個也就和上面的那個notifyAll方法對應起來了。

那麼既然都分析到這裡了,我們乾脆再來看一下常用的commit和apply兩個方法吧:

commit方法:


這裡主要就連個方法,首先來看看commitToMemory方法,這個是整理提交前的map資料結構,用於寫到檔案前的操作準備

整理好了記憶體中的資料,開始寫入到磁碟中了,其實commit從記憶體寫檔案是在當前調運執行緒中直接執行的。那我們再來看看這個寫記憶體到磁碟方法中真正的寫方法writeToFile:


分析完了commit方法,我們總結一下:

如果用commit()方法提交資料,其過程是先把資料更新到記憶體,然後在當前執行緒中寫檔案操作,提交完成返回提交狀態


接下來繼續看apply方法:


這裡也是呼叫了enqueueDiskWrite方法:


其實這個方法是commit和apply公用的,主要用isFromSyncCommit來進行區分的,postWriteRunnalbe==null就是commit方式。如果不為null的話,就是apply方式。

總結一下apply方法:

如果用的是apply()方法提交資料,首先也是寫到記憶體,接著在一個新執行緒中非同步寫檔案,然後沒有返回值。

其實這裡算是分析完了SharePreferences的原始碼,我們可以總結如下:

1、SharedPreferences在例項化時首先會從sdcard非同步讀檔案,然後快取在記憶體中;接下來的讀操作都是記憶體快取操作而不是檔案操作。
2、在SharedPreferences的Editor中如果用commit()方法提交資料,其過程是先把資料更新到記憶體,然後在當前執行緒中寫檔案操作,提交完成返回提交狀態;如果用的是apply()方法提交資料,首先也是寫到記憶體,接著在一個新執行緒中非同步寫檔案,然後沒有返回值。

3、由於上面分析了,在寫操作commit時有三級鎖操作,所以效率很低,所以當我們一次有多個修改寫操作時等都批量put完了再一次提交確認,這樣可以提高效率。


上面算是開了一個小差,順道分析了一下SharePreferences的原始碼,下面來說正題了,我們在上面的例子已經知道了,通過設定Context的檔案建立模式來設定安全性。那麼現在如果我們想讓A應用訪問到B應用的資料,我們可以這麼做:把B應用建立模式改成可讀模式的,那麼A應用就可以操作了,那麼這就有一個問題,A應用可以訪問了,其他應用也可以訪問了,這樣所有的應用都可以訪問B應用的沙盒資料了,太危險了,所以要用另外的一種方式,那麼這時候就要用到shareUserId屬性了,我們只需要將B應用建立方式還是private的,然後A應用和B應用公用一個uid即可,我們下面就來修改一下程式碼,還是上面的那兩個工程,修改他們的AndroidManifest.xml,新增shareUserId即可。


這時候,我們發現把ShareUserIdPlugin中的模式改成private的,A應用任然可以訪問資料了,其實也好理解,他們兩個的uid都相同了,A的檔案就是B的,B的就是A的了,他們兩個沒有沙盒的概念了,資料也是透明的了。

所以這裡我們就看到了,使用shareUserId可以達到多個應用之間的資料透明性互相訪問。


那麼問題來了,假如現在我手機沒有root,想訪問某個應用的沙盒資料,我把自己的應用修改成和他一樣的shareUserId即可。

注意:這裡有一個誤點,就是這裡所有的修改的前提是這個應用的AndroidManifest.xml本身就定義了這個屬性,然後我們可以反編譯看到這個值,把我們自己的shareUserId改成他的就可以了,但是如果這個應用本身沒有這個屬性,那麼這裡就沒有辦法的,為什麼呢,如果要新增,那就是另外一條路了,就是逆向,修改AndroidManifest.xml之後,還需要從新打包在驗證,但是這時候沒必要了,我們也知道有時候回編譯還是很艱難的,如果都能回編譯了,那都不需要這些工作了,所以這裡需要注意的一個前提

那麼修改之後是不是真的可以呢?

答案是肯定不可以的,如果可以的話,那google也太傻比了,其實Android系統中有一個限制,就是說如果多個應用的uid相同的話,那麼他們的apk簽名必須一致,不然是安裝失敗的,如下錯誤:


我們可以檢視PackageManagerService.java原始碼:


看到了,這裡會作比較的,不過這裡我們在深入看一下這個方法的呼叫鏈:


在scanPackageLI方法中呼叫的verifySignaturesLP方法,那麼scanPackageLI方法在哪呼叫的呢?繼續跟蹤:


在這裡,這裡其實是一個檔案監聽類AppDirObserver:


這裡會監聽/data/app目錄,如果有新的檔案增加,就會呼叫scanPackageLI方法,然後在呼叫verifySignaturesLP方法來進行驗證apk檔案資訊。同時我們也發現了,系統的安裝和解除安裝apk的廣播也是在這裡傳送的。果然這裡的知識點還是很多的。

通過上面的分析,我們就知道了,Android中是不允許相同的uid的不同簽名的應用。

那麼我們上面的猜想就是失敗的。及時改成目標應用相同的shareUserId,也是安裝不成功的。


四、知識梳理

1、我們知道如何通過包名來構建一個Context,同時需要注意兩種模式:

Context.CONTEXT_INCLUDE_CODE和Context.CONTEXT_IGNORE_SECURITY

構造完成之後,我們可以訪問資源和執行一些模組程式碼,這些其實不算是一個應用的沙盒概念了,所以不會牽扯到shareUserId的知識點。

2、我們在實驗A應用去訪問B應用的SharedPreferences中的值時,發現建立sp的xml有幾種模式:

Context.MODE_PRIVATE:為預設操作模式,代表該檔案是私有資料,只能被應用本身訪問,在該模式下,寫入的內容會覆蓋原檔案的內容,如果想把新寫入的內容追加到原檔案中。可以使用Context.MODE_APPEND
Context.MODE_APPEND:模式會檢查檔案是否存在,存在就往檔案追加內容,否則就建立新檔案。
Context.MODE_WORLD_READABLE和Context.MODE_WORLD_WRITEABLE用來控制其他應用是否有許可權讀寫該檔案。

這三種模式的區別,我們最保險的操作就是設定成private的,不過預設也是這種模式

3、我們通過分析SharedPreferences的原始碼,知道這三種模式對應的就是設定xml檔案的訪問許可權,同時我們順便分析了commit,apply,getXXX等方法的實現,也算是對SP的更深入的理解了。其實SharedPreferences內部為了高效率,會第一次載入xml內容到記憶體中的map中,每次getXXX資料的時候,都是直接從map中取,每次儲存資料,是首先儲存到記憶體的map中,呼叫commit和apply方法只有在將資料寫入到磁碟中的區別。apply是非同步的沒有返回值,commit是同步的有返回值

4、我們再次實驗使用shareUserId屬性來做到多個應用之間的資料共享和透明性,同時我們也做了一個猜想就是把自己的shareUserId修改成和目標應用相同來訪問目標應用的資料,但是這個猜想是錯誤的,因為我們通過分析PackageManagerService原始碼知道,Android中是不允許相同的shareUserId的應用有著不同的簽名檔案的,會出現安裝失敗的情況。


五、遺留的問題

關於檔案建立還有一種模式:Context.MODE_MULTI_PROCESS,這個模式其實我們知道是用來多程式訪問的,這裡關於原始碼就不在分析了,在ContextImpl.java中的getSharedPreferences方法中會做一次多程式的資料重新整理載入操作:


不過這個方法已經廢棄了,google建議還是使用ContentProvider比較靠譜,同樣,上面的Context.MODE_WORLD_READABLE和Context.MODE_WORLD_WRITEABLE這兩種模式也是被廢棄了,也算是google為了增強安全性考慮吧。


六、總結

這篇文章就介紹了使用sharedUserId屬性,來實現我們想要的應用資料共享效果,但是引出來的知識點有點多,所以說的就有點多了,不過我們就記住一點:

在建立檔案時,一定要設定成Context.MODE_PRIVATE或者是Context.MODE_APPEND模式,為了做到應用的資料共享可以考慮shareUserId屬性。同時Android中是不允許相同的sharedUserId有著不同簽名的應用的,會出現安裝失敗。

分析的好累呀~~,跪求點贊啦啦~~

相關文章