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

來源:互聯網
上載者:User

標籤:android   style   blog   http   color   io   os   使用   ar   

從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,歡迎關注!

        Android是一個基於Linux核心的移動作業系統。Linux是一個支援多使用者的系統,系統中的檔案的存取權限是通過使用者ID(UID)和使用者組ID(GID)來控制的。換句話說,就是Linux的安全機制是基於UID和GID來實現的。Android在Linux核心提供的基於UID和GID的安全機制的基礎上,又實現了一套稱為Permission的安全機制,1所示:


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

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

        我們首先看一下Linux基於UID和GID的安全機制,它包含三個基本角色:使用者、進程和檔案,2所示:


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

        Linux中的每一個使用者都分配有一個UID,然後所有的使用者又按組來進劃分,每一個使用者組都分配有一個GID。注意,一個使用者可以屬於多個使用者組,也就是說,一個UID可以對應多個GID。在一個使用者所對應的使用者組中,其中有一個稱為主使用者組,其它的稱為補充使用者組。

        Linux中的每一個檔案都具有三種許可權:Read、Write和Execute。這三種許可權又按照使用者屬性劃分為三組:Owner、Group和Other。3所示:


圖3 Linux的檔案許可權劃分

        從圖3就可以看出檔案acct:1. 所有者為root,可讀可寫可執行;2. 所有者所屬的主使用者組為root,在這個組中的其它使用者可讀可執行;3. 其餘的使用者可讀可執行。

        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應用程式進程:

/* * Utility routine to fork zygote and specialize the child process. */static pid_t forkAndSpecializeCommon(const u4* args, bool isSystemServer){       pid_t pid;        uid_t uid = (uid_t) args[0];    gid_t gid = (gid_t) args[1];    ArrayObject* gids = (ArrayObject *)args[2];    ......        pid = fork();        if (pid == 0) {        ......                err = setgroupsIntarray(gids);        ......                err = setgid(gid);        ......                err = setuid(uid);        ......    }           .....        return pid;}   

        參數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函數的實現:

static int ptrace_attach(struct task_struct *task, long request,             unsigned long addr,             unsigned long flags){    ......    task_lock(task);    retval = __ptrace_may_access(task, PTRACE_MODE_ATTACH);    task_unlock(task);    if (retval)        goto unlock_creds;    ......unlock_creds:    mutex_unlock(&task->signal->cred_guard_mutex);out:    ......    return retval;}
          gdbserver在調試一個APP之前,首先要通過ptrace_attach來附加到該APP進程去。ptrace_attach在執行實際操作之後,會調用__ptrace_may_access來檢查調用進程的許可權:

int __ptrace_may_access(struct task_struct *task, unsigned int mode){    const struct cred *cred = current_cred(), *tcred;    ......    if (task == current)        return 0;    rcu_read_lock();    tcred = __task_cred(task);    if (cred->user->user_ns == tcred->user->user_ns &&        (cred->uid == tcred->euid &&         cred->uid == tcred->suid &&         cred->uid == tcred->uid  &&         cred->gid == tcred->egid &&         cred->gid == tcred->sgid &&         cred->gid == tcred->gid))        goto ok;    if (ptrace_has_cap(tcred->user->user_ns, mode))        goto ok;    rcu_read_unlock();    return -EPERM;ok:    ......    return security_ptrace_access_check(task, mode);}
         這裡我們就可以看到,如果調用進程與目標進程具有相同的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來看到:

[email protected]:/ # ls -l /system/bin/run-as                                      -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的實現:

int __ptrace_may_access(struct task_struct *task, unsigned int mode){    const struct cred *cred = current_cred(), *tcred;    ......    int dumpable = 0;    ......ok:    rcu_read_unlock();    smp_rmb();    if (task->mm)        dumpable = get_dumpable(task->mm);    if (!dumpable  && !ptrace_has_cap(task_user_ns(task), mode))        return -EPERM;    return security_ptrace_access_check(task, mode);}
        我們再來看看當一個APK在其AndroidManifext.xml裡必須將android:debuggable屬性設定為true時會發生什麼事情。ActivityManagerService在請求Zygote進程為其fork一個應用程式進程時,會將它的DEBUG_ENABLE_DEBUGGER標誌位設定為1,並且以參數的形式傳遞給Zygote進程。Zygote進程在調用我們在上面分析的函數forkAndSpecializeCommon來fork應用程式進程時,就會相應的處理,如下所示:

static pid_t forkAndSpecializeCommon(const u4* args, bool isSystemServer){    pid_t pid;    ......    u4 debugFlags = args[3];    ......    pid = fork();    if (pid == 0) {        ......        /* configure additional debug options */        enableDebugFeatures(debugFlags);        ......    }    ......    return pid;}
         參數args[3]包含的就是調試標誌位,函數enableDebugFeatures的實現如下所示:

void enableDebugFeatures(u4 debugFlags){    ......    if ((debugFlags & DEBUG_ENABLE_DEBUGGER) != 0) {        /* To let a non-privileged gdbserver attach to this         * process, we must set its dumpable bit flag. However         * we are not interested in generating a coredump in         * case of a crash, so also set the coredump size to 0         * to disable that         */        if (prctl(PR_SET_DUMPABLE, 1, 0, 0, 0) < 0) {            ALOGE("could not set dumpable bit flag for pid %d: %s",                 getpid(), strerror(errno));        } else {            struct rlimit rl;            rl.rlim_cur = 0;            rl.rlim_max = RLIM_INFINITY;            if (setrlimit(RLIMIT_CORE, &rl) < 0) {                ALOGE("could not disable core file generation for pid %d: %s",                    getpid(), strerror(errno));            }        }    }    ......}
        這樣當一個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的原始碼:

int main(int argc, char **argv){    const char* pkgname;    int myuid, uid, gid;    PackageInfo info;    ......    /* check userid of caller - must be ‘shell‘ or ‘root‘ */    myuid = getuid();    if (myuid != AID_SHELL && myuid != AID_ROOT) {        panic("only ‘shell‘ or ‘root‘ users can run this program\n");    }    /* retrieve package information from system */    pkgname = argv[1];    if (get_package_info(pkgname, &info) < 0) {        panic("Package ‘%s‘ is unknown\n", pkgname);        return 1;    }    /* reject system packages */    if (info.uid < AID_APP) {        panic("Package ‘%s‘ is not an application\n", pkgname);        return 1;    }    /* reject any non-debuggable package */    if (!info.isDebuggable) {        panic("Package ‘%s‘ is not debuggable\n", pkgname);        return 1;    }    /* Ensure that we change all real/effective/saved IDs at the     * same time to avoid nasty surprises.     */    uid = gid = info.uid;    if(setresgid(gid,gid,gid) || setresuid(uid,uid,uid)) {        panic("Permission denied\n");        return 1;    }    ......    /* Default exec shell. */    execlp("/system/bin/sh", "sh", NULL);    panic("exec failed\n");    return 1;}
          這裡我們就可以看到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。

原文連結本文由豆約翰部落格備份專家遠程一鍵發布

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

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.