從NDK在非Root手機上的調試原理探討Android的安全機制

       最近都在忙着研究Android的安全攻防技術,好長一段時間沒有寫博客了,準備迴歸老本行中--Read the funcking android source code。這兩天在看NDK文檔的時候,看到一句話「Native debugging ... does not require root or privileged access, aslong as your application is debuggable」。咦,NDK調試不就是經過ptrace來實現調試的麼?在非Root的手機上是怎麼進行ptrace的呢?借這兩個問題正好能夠介紹一下Android的安全機制。

老羅的新浪微博:http://weibo.com/shengyangluo,歡迎關注! html

        Android是一個基於Linux內核的移動操做系統。Linux是一個支持多用戶的系統,系統中的文件的訪問權限是經過用戶ID(UID)和用戶組ID(GID)來控制的。換句話說,就是Linux的安全機制是基於UID和GID來實現的。Android在Linux內核提供的基於UID和GID的安全機制的基礎上,又實現了一套稱爲Permission的安全機制,如圖1所示: linux

圖1 Linux的UID/GID安全機制與Android的Permission安全機制 android

        那麼,這兩個安全機制是如何對應起來的呢? shell

        咱們首先看一下Linux基於UID和GID的安全機制,它包含三個基本角色:用戶、進程和文件,如圖2所示: 安全

圖2 Linux基於UID/GID的安全機制的三個角色 app

        Linux中的每個用戶都分配有一個UID,而後全部的用戶又按組來進劃分,每個用戶組都分配有一個GID。注意,一個用戶能夠屬於多個用戶組,也就是說,一個UID能夠對應多個GID。在一個用戶所對應的用戶組中,其中有一個稱爲主用戶組,其它的稱爲補充用戶組。 socket

        Linux中的每個文件都具備三種權限:Read、Write和Execute。這三種權限又按照用戶屬性劃分爲三組:Owner、Group和Other。如圖3所示: 函數

圖3 Linux的文件權限劃分 工具

        從圖3就能夠看出文件acct:1. 全部者爲root,可讀可寫可執行;2. 全部者所屬的主用戶組爲root,在這個組中的其它用戶可讀可執行;3. 其他的用戶可讀可執行。 ui

        Linux中的每個進程都關聯有一個用戶,也就是對應有一個UID,如圖4所示:

圖4 Linux的進程

         因爲每個用戶都對應有一個主用戶組,以及若干個補充用戶組,所以,每個進程除了有一個對應的UID以外,還對應有一個主GID,以及若干個Supplementary GIDs。這些UID和GID就決定了一個進程所能訪問的文件或者所能調用的系統API。例如,在圖4中,PID爲340的進程通常來講,就只能訪問全部者爲u0_a19的文件。

         一個進程的UID是怎麼來的呢?在默認狀況下,就等於建立它的進程的UID,也就是它的父進程的UID。Linux的第一個進程是init進程,它是由內核在啓動完成後建立的,它的UID是root。而後系統中的全部其它進程都是直接由init進程或者間接由init進程的子進程來建立。因此默認狀況下,系統的全部進程的UID都應該是root。可是實際狀況並不是如此,由於父進程在建立子進程以後,也就是在fork以後,能夠調用setuid來改變它的UID。例如,在PC中,init進程啓動以後,會先讓用戶登陸。用戶登陸成功後,就對應有一個shell進程。該shell進程的UID就會被setuid修改成所登陸的用戶。以後系統中建立的其他進程的UID爲所登陸的用戶。

        進程的UID除了來自於父進程以外,還有另一種途徑。上面咱們說到,Linux的文件有三種權限,分別是Read、Wirte和Execute。其實還有另一個種權限,叫作SUID。例如,咱們對Android手機進行root的過程當中,會在裏面放置一個su文件。這個su文件就具備SUID權限,如圖5所示:

圖5 su的SUID和SGID

        一個可執行文件一旦被設置了SUID位,那麼當它被一個進程經過exec加載以後,該進程的UID就會變成該可執行文件的全部者的UID。也就是說,當上述的su被執行的時候,它所運行在的進程的UID是root,因而它就具備最高級別的權限,想幹什麼就幹什麼。

        與SUI相似,文件還有另一個稱爲SGID的權限,不過它描述的是用戶組。也就是說,一個可執行文件一旦被設置了GUID位,麼當它被一個進程經過exec加載以後,該進程的主UID就會變成該可執行文件的全部者的主UID。

        如今,小夥伴們應該能夠理解Android手機的root原理了吧:一個普通的進程經過執行su,從而得到一個具備root權限的進程。有了這個具備root權限的進程以後,就能夠想幹什麼就幹什麼了。su所作的事情其實很簡單,它再fork另一個子進程來作真正的事情,也就是咱們在執行su的時候,後面所跟的那些參數。因爲su所運行在的進程的UID是root,所以由它fork出來的子進程的UID也是root。因而,子進程也能夠想幹什麼就幹什麼了。

        不過呢,用來root手機的su還會配合另一個稱爲superuser的app來使用。su在fork子進程來作真正的事情以前,會將superuser啓動起來,詢問用戶是否容許fork一個UID是root的子進程。這樣就能夠對root權限進行控制,避免被惡意應用偷偷地使用。

        這裏是su的源代碼,小夥伴們能夠根據上面所講的知識讀一讀:https://code.google.com/p/superuser/source/browse/trunk/su/su.c?r=2

        在傳統的UNIX以及類UNIX系統中,進程的權限只劃分兩種:特權和非特權。UID等於0的進程就是特權進程,它們能夠經過一切的權限檢查。UID不等於0的進程就非特權進程,它們在訪問一些敏感資源或者調用一個敏感API時,須要進行權限檢查。這種純粹經過UID來作權限檢查的安全機制來粗放了。因而,Linux從2.2開始,從進程的權限進行了細分,稱爲Capabilities。一個進程所具備Capabilities能夠經過capset和prctl等系統API來設置。也就是說,當一個進程調用一個敏感的系統API時,Linux內核除了考慮它的UID以外,還會考慮它是否具備對應的Capability。

        這裏就是Linux所設計的Capabilities列表,有興趣的小夥伴能夠再讀一讀:http://man7.org/linux/man-pages/man7/capabilities.7.html

        以上就是Linux基於UID/GID的安全機制的核心內容。接下來咱們再看Android基於Permission的安全機制,它也有三個角色:apk、signature和permission,如圖6所示:

圖6 Android的Permission安全機制

        

        Android的APK通過PackageManagerService安裝以後,就至關於Linux裏面的User,它們都會被分配到一個UID和一個主GID,而APK所申請的Permission就至關因而Linux裏面的Supplementary GID。

        咱們知道,Android的APK都是運行在獨立的應用程序進程裏面的,而且這些應用程序進程都是Zygote進程fork出來的。Zygote進程又是由init進程fork出來的,而且它被init進程fork出來後,沒有被setuid降權,也就是它的uid仍然是root。按照咱們前面所說的,應用程序進程被Zygote進程fork出來的時候,它的UID也應當是root。可是,它們的UID會被setuid修改成所加載的APK被分配的UID。

       參照Android應用程序進程啓動過程的源代碼分析一文的分析,ActivityManagerService在請求Zygote建立應用程序進程的時候,會將這個應用程序所加載的APK所分配獲得的UID和GID(包括主GID和Supplementary GID)都收集起來,而且將它們做爲參數傳遞給Zygote進程。Zygote進程經過執行函數來fork應用程序進程:

[cpp]  view plain copy 在CODE上查看代碼片 派生到個人代碼片
  1. /* 
  2.  * Utility routine to fork zygote and specialize the child process. 
  3.  */  
  4. static pid_t forkAndSpecializeCommon(const u4* args, bool isSystemServer)  
  5. {     
  6.     pid_t pid;  
  7.       
  8.     uid_t uid = (uid_t) args[0];  
  9.     gid_t gid = (gid_t) args[1];  
  10.     ArrayObject* gids = (ArrayObject *)args[2];  
  11.     ......  
  12.       
  13.     pid = fork();  
  14.       
  15.     if (pid == 0) {  
  16.         ......  
  17.           
  18.         err = setgroupsIntarray(gids);  
  19.         ......  
  20.           
  21.         err = setgid(gid);  
  22.         ......  
  23.           
  24.         err = setuid(uid);  
  25.         ......  
  26.     }     
  27.       
  28.     .....  
  29.       
  30.     return pid;  
  31. }     

        參數args[0]、args[1]和args[]保存的就是APK分配到的UID、主GID和Supplementary GID,它們分別經過setuid、setgid和setgroupsIntarray設置給當前fork出來的應用程序進程,因而應用程序進程就再也不具備root權限了。

        那麼,Signature又充當什麼做用呢?兩個做用:1. 控制哪些APK能夠共享同一個UID;2. 控制哪些APK能夠申請哪些Permission。

        咱們知道,若是要讓兩個APK共享同一個UID,那麼就須要在AndroidManifest中配置android:sharedUserId屬性。PackageManagerService在安裝APK的時候,若是發現兩個APK具備相同的android:sharedUserId屬性,那麼它們就會被分配到相同的UID。固然這有一個前提,就是這兩個APK必須具備相同的Signature。這很重要,不然的話,若是我知作別人的APK設置了android:sharedUserId屬性,那麼我也在本身的APK中設置相同的android:sharedUserId屬性,就能夠去訪問別人APK的數據了。

        除了能夠經過android:sharedUserId屬性申請讓兩個APK共享同一個UID以外,咱們還能夠將android:sharedUserId屬性的值設置爲「android.uid.system」,從而讓一個APK的UID設置爲1000。UID是1000的用戶是system,系統的關鍵服務都是運行在的進程的UID就是它。它的權限雖然不等同於root,不過也足夠大了。咱們能夠經過Master Key漏洞來看一下有多大。

        Master Key漏洞發佈時,曾轟動了整個Android界,它的具體狀況老羅就不分析了,網上不少,這裏是一篇官方的文章:http://bluebox.com/corporate-blog/bluebox-uncovers-android-master-key/。如今就簡單說說它是怎麼利用的:

        1. 找到一個具備系統簽名的APP,而且這個APP經過android:sharedUserId屬性申請了android.uid.system這個UID。

        2. 經過Master Key向這個APP注入惡意代碼。

        3. 注入到這個APP的惡意代碼在運行時就得到了system用戶身份。

        4. 修改/data/local.prop文件,將屬性ro.kernel.qemu的值設置爲1。

        5. 重啓手機,因爲ro.kernel.qemu的值等於1,這時候手機裏面的adb進程不會被setuid剝奪掉root權限。

        6. 經過具備root權限的adb進程就能夠向系統注入咱們熟悉的su和superuser.apk,因而整個root過程完成。

        注意,第1步之因此要找一個具備系統簽名的APP,是由於經過android:sharedUserId屬性申請android.uid.system這個UID須要有系統簽名,也就是說不是誰能夠申請system這個UID的。另外,/data/local.prop文件的Owner是system,所以,只有得到了system這個UID的進程,才能夠對它進行修改。

        再說說Signature與Permission的關係。有些Permission,例如INSTALL_PACKAGE,不是誰均可以申請的,必需要具備系統簽名才能夠,這樣就能夠控制Suppementary GID的分配,從而控制應用程序進程的權限。具備哪些Permission是具備系統簽名才能夠申請的,能夠參考官方文檔:http://developer.android.com/reference/android/Manifest.html,就是哪些標記爲「Not for use by third-party applications」的Permission。

        瞭解了Android的Permission機制以後,咱們就能夠知道:

         1. Android的APK就至關因而Linux的UID。

         2. Android的Permission就至關因而Linux的GID。

         3. Android的Signature就是用來控制APK的UID和GID分配的。

         這就是Android基於Permission的安全機制與Linux基於UID/GID的安全機制的關係,歸納來講,咱們常說的應用程序沙箱就是這樣的:

圖7 Android的Application Sandbox

       接下來咱們就終於能夠步入正題分析NDK在非root手機上調試APP的原理了。首先們須要知道的是,NDK是經過gdbclient和gdbserver來調試APP的。具體來講,就是經過gdbserver經過ptrace附加上目標APP進程去,而後gdbclient再經過socket或者pipe來連接gdbserver,而且向它發出命令來對APP進程進行調試。這個具體的過程能夠參考這篇文章,講得很詳細的了:http://ian-ni-lewis.blogspot.com/2011/05/ndk-debugging-without-root-access.html。老羅但願小夥伴們認真看完這篇文章再來看接下來的內容,由於接下來咱們只講這篇文章的關鍵點。

        第一個關鍵點是每個須要調試的APK在打包的時候,都會帶上一個gdbserver。由於手機上面不帶有gdbserver這個工具。這個gdbserver就負責用來ptrace到要調度的APP進程去。

        第二個關鍵點是ptrace的調用。通常來講,只有root權限的進程只能夠調用。例如,若是咱們想經過ptrace向目標進程注入一個SO,那麼就須要在root過的手機上經過向su申請root權限。可是,這不是絕對的。若是一個進程與目標進程的UID是相同的,那麼該進程就具備調用ptrace的權限。咱們能夠看看ptrace_attach函數的實現:

[cpp]  view plain copy 在CODE上查看代碼片 派生到個人代碼片
  1. static int ptrace_attach(struct task_struct *task, long request,  
  2.              unsigned long addr,  
  3.              unsigned long flags)  
  4. {  
  5.     ......  
  6.   
  7.     task_lock(task);  
  8.     retval = __ptrace_may_access(task, PTRACE_MODE_ATTACH);  
  9.     task_unlock(task);  
  10.     if (retval)  
  11.         goto unlock_creds;  
  12.     ......  
  13.   
  14. unlock_creds:  
  15.     mutex_unlock(&task->signal->cred_guard_mutex);  
  16. out:  
  17.     ......  
  18.   
  19.     return retval;  
  20. }  
          gdbserver在調試一個APP以前,首先要經過ptrace_attach來附加到該APP進程去。ptrace_attach在執行實際操做以後,會調用__ptrace_may_access來檢查調用進程的權限:
[cpp]  view plain copy 在CODE上查看代碼片 派生到個人代碼片
  1. int __ptrace_may_access(struct task_struct *task, unsigned int mode)  
  2. {  
  3.     const struct cred *cred = current_cred(), *tcred;  
  4.     ......  
  5.   
  6.     if (task == current)  
  7.         return 0;  
  8.     rcu_read_lock();  
  9.     tcred = __task_cred(task);  
  10.     if (cred->user->user_ns == tcred->user->user_ns &&  
  11.         (cred->uid == tcred->euid &&  
  12.          cred->uid == tcred->suid &&  
  13.          cred->uid == tcred->uid  &&  
  14.          cred->gid == tcred->egid &&  
  15.          cred->gid == tcred->sgid &&  
  16.          cred->gid == tcred->gid))  
  17.         goto ok;  
  18.     if (ptrace_has_cap(tcred->user->user_ns, mode))  
  19.         goto ok;  
  20.     rcu_read_unlock();  
  21.     return -EPERM;  
  22. ok:  
  23.     ......  
  24.   
  25.     return security_ptrace_access_check(task, mode);  
  26. }  
         這裏咱們就能夠看到,若是調用進程與目標進程具備相同的UID和GID,那麼權限檢查就經過。不然的話,就要求調用者進程具備執行ptrace的capability,這是經過另一個函數ptrace_has_cap來檢查的。若是是調用進程的UID是root,那麼ptrace_has_cap必定會檢查經過。固然,經過了上述兩個權限檢查以後,還要接受內核安全模塊的檢查,這個就不是經過UID或者Capability這一套機制來控制的了,咱們能夠忽略這個話題。

        第三個關鍵點是如何讓gdbserver進程的UID與要調試的APP進程的UID同樣。由於在沒有root過的手機上,要想得到root權限是不可能的了,所以只能選擇以目標進程相同的UID運行這個方法。這就要用到另一個工具了:run-as。

        runs-as實際上是一個與su相似的工具,它在設備上是自帶的,位於/system/bin目錄下,它的SUID位也是被設置了,而且它的全部者也是root,咱們能夠經過ls -l /system/bin/run-as來看到:

[plain]  view plain copy 在CODE上查看代碼片 派生到個人代碼片
  1. root@android :/ # ls -l /system/bin/run-as                                        
  2. -rwsr-s--- root     shell        9528 2013-12-05 05:32 run-as  
        可是與su不一樣,run-as不是讓一個進程以root身份運行,而是讓一個進程以指定的UID來運行,這也是經過setuid來實現的。run-as可以這樣作是由於它運行的時候,所得到的UID是root。

        第四個關鍵點是被調試的APK在其AndroidManifext.xml裏必須將android:debuggable屬性設置爲true。這是爲何呢?原來,當一個進程具備ptrace到目標進程的權限時,還不可以對目標進程進行調試,還要求目標進程將本身設置爲可dumpable的。咱們再回過頭來進一步看看__ptrace_may_access的實現:

[cpp]  view plain copy 在CODE上查看代碼片 派生到個人代碼片
  1. int __ptrace_may_access(struct task_struct *task, unsigned int mode)  
  2. {  
  3.     const struct cred *cred = current_cred(), *tcred;  
  4.     ......  
  5.   
  6.     int dumpable = 0;  
  7.     ......  
  8.   
  9. ok:  
  10.     rcu_read_unlock();  
  11.     smp_rmb();  
  12.     if (task->mm)  
  13.         dumpable = get_dumpable(task->mm);  
  14.     if (!dumpable  && !ptrace_has_cap(task_user_ns(task), mode))  
  15.         return -EPERM;  
  16.   
  17.     return security_ptrace_access_check(task, mode);  
  18. }  
        咱們再來看看當一個APK在其AndroidManifext.xml裏必須將android:debuggable屬性設置爲true時會發生什麼事情。ActivityManagerService在請求Zygote進程爲其fork一個應用程序進程時,會將它的DEBUG_ENABLE_DEBUGGER標誌位設置爲1,而且以參數的形式傳遞給Zygote進程。Zygote進程在調用咱們在上面分析的函數forkAndSpecializeCommon來fork應用程序進程時,就會相應的處理,以下所示:
[cpp]  view plain copy 在CODE上查看代碼片 派生到個人代碼片
  1. static pid_t forkAndSpecializeCommon(const u4* args, bool isSystemServer)  
  2. {  
  3.     pid_t pid;  
  4.     ......  
  5.   
  6.     u4 debugFlags = args[3];  
  7.     ......  
  8.   
  9.     pid = fork();  
  10.   
  11.     if (pid == 0) {  
  12.         ......  
  13.   
  14.         /* configure additional debug options */  
  15.         enableDebugFeatures(debugFlags);  
  16.         ......  
  17.   
  18.     }  
  19.   
  20.     ......  
  21.   
  22.     return pid;  
  23. }  
         參數args[3]包含的就是調試標誌位,函數enableDebugFeatures的實現以下所示:
[cpp]  view plain copy 在CODE上查看代碼片 派生到個人代碼片
  1. void enableDebugFeatures(u4 debugFlags)  
  2. {  
  3.     ......  
  4.   
  5.     if ((debugFlags & DEBUG_ENABLE_DEBUGGER) != 0) {  
  6.         /* To let a non-privileged gdbserver attach to this 
  7.          * process, we must set its dumpable bit flag. However 
  8.          * we are not interested in generating a coredump in 
  9.          * case of a crash, so also set the coredump size to 0 
  10.          * to disable that 
  11.          */  
  12.         if (prctl(PR_SET_DUMPABLE, 1, 0, 0, 0) < 0) {  
  13.             ALOGE("could not set dumpable bit flag for pid %d: %s",  
  14.                  getpid(), strerror(errno));  
  15.         } else {  
  16.             struct rlimit rl;  
  17.             rl.rlim_cur = 0;  
  18.             rl.rlim_max = RLIM_INFINITY;  
  19.             if (setrlimit(RLIMIT_CORE, &rl) < 0) {  
  20.                 ALOGE("could not disable core file generation for pid %d: %s",  
  21.                     getpid(), strerror(errno));  
  22.             }  
  23.         }  
  24.     }  
  25.   
  26.     ......  
  27. }  
        這樣當一個APK在其AndroidManifext.xml裏必須將android:debuggable屬性設置爲true時,它所運行在的進程就會經過prctl將PR_SET_DUMPABLE設置爲1,這樣gdbserver才能對它進行調試。

        這下咱們就明白NDK在非root手機上調試APP的原理了:gdbserver經過run-as得到與目標進程相同的UID,而後就能夠ptrace到目標進程去調試了。

        這一下就引出了run-as這個工具,貌似很強大的樣子,那咱們是否是也能夠利用它來作壞事呢?例如,咱們能夠在adb shell中運行run-as(run-as屬於shell組,所以能夠執行),而且指定run-as以某一個APK的UID運行,那麼不就是能夠讀取該APK的數據了嗎?從而突破了Android的應用程序沙箱。可是這是不可能作到的。

        咱們能夠看一下run-as的源代碼:

[cpp]  view plain copy 在CODE上查看代碼片 派生到個人代碼片
  1. int main(int argc, char **argv)  
  2. {  
  3.     const char* pkgname;  
  4.     int myuid, uid, gid;  
  5.     PackageInfo info;  
  6.     ......  
  7.   
  8.     /* check userid of caller - must be 'shell' or 'root' */  
  9.     myuid = getuid();  
  10.     if (myuid != AID_SHELL && myuid != AID_ROOT) {  
  11.         panic("only 'shell' or 'root' users can run this program\n");  
  12.     }  
  13.   
  14.     /* retrieve package information from system */  
  15.     pkgname = argv[1];  
  16.     if (get_package_info(pkgname, &info) < 0) {  
  17.         panic("Package '%s' is unknown\n", pkgname);  
  18.         return 1;  
  19.     }  
  20.   
  21.     /* reject system packages */  
  22.     if (info.uid < AID_APP) {  
  23.         panic("Package '%s' is not an application\n", pkgname);  
  24.         return 1;  
  25.     }  
  26.   
  27.     /* reject any non-debuggable package */  
  28.     if (!info.isDebuggable) {  
  29.         panic("Package '%s' is not debuggable\n", pkgname);  
  30.         return 1;  
  31.     }  
  32.     /* Ensure that we change all real/effective/saved IDs at the 
  33.      * same time to avoid nasty surprises. 
  34.      */  
  35.     uid = gid = info.uid;  
  36.     if(setresgid(gid,gid,gid) || setresuid(uid,uid,uid)) {  
  37.         panic("Permission denied\n");  
  38.         return 1;  
  39.     }  
  40.   
  41.     ......  
  42.   
  43.     /* Default exec shell. */  
  44.     execlp("/system/bin/sh""sh", NULL);  
  45.   
  46.     panic("exec failed\n");  
  47.     return 1;  
  48. }  
          這裏咱們就能夠看到run-as在啓動的時候作了不少安全檢查,包括:

          1. 檢查自身是否是以shell或者root用戶運行。

          2. 檢查指定的UID的值是不是在分配給APK範圍內的值,也就是隻能夠指定APK的UID,而不能夠指定像system這樣的UID。

          3. 指定的UID所對應的APK的android:debuggable屬性必需要設置爲true。

          綜合了以上三個條件以後,咱們才能夠成功地執行run-as。

          這裏還有一點須要提一下的就是,咱們在運行run-as的時候,指定的參數實際上是一個package name。run-as經過這個package name到/data/system/packages.xml去得到對應的APK的安裝信息,包括它所分配的UID,以及它的android:debuggable屬性。文件/data/system/packages.xml的全部者是system,run-as在讀取這個文件的時候的身份是root,所以有權限對它進行讀取。

         這下咱們也明白了,你想經過run-as來作壞事是不行的。同時,這也提醒咱們,在發佈APK的時候,必定不要將android:debuggable屬性的值設置爲true。不然的話,就提供了機會讓別人去讀取你的數據,或者對你進行ptrace了。

         至些,咱們就經過NDK在非Root手機上的調試原理完成了Android安全機制的探討了,不知道各位小夥伴們理解了嗎?沒理解的不要緊,能夠關注老羅的新浪微博,上面有不少的乾貨分享:http://weibo.com/shengyangluo

相關文章
相關標籤/搜索