Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android逆向之旅---Android中的sharedUserId屬性詳解

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

編輯:關於Android編程

一、前言

今天我們來看一下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等,大家可能會想到現在有一個比較流行的技術叫做插件開發,的確如此,這個我在之前的文章也有說過。

但是我們今天不說這個插件怎麼搞,今天就來看看如何在一個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有著不同簽名的應用的,會出現安裝失敗。

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

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved