從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的安全機制。
《Android系統原始碼情景分析》一書正在進擊的程式設計師網(
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許可權進行控制,避免被惡意應用偷偷地使用。
在傳統的UNIX以及類UNIX系統中,程序的許可權只劃分兩種:特權和非特權。UID等於0的程序就是特權程序,它們可以通過一切的許可權檢查。UID不等於0的程序就非特權程序,它們在訪問一些敏感資源或者呼叫一個敏感API時,需要進行許可權檢查。這種純粹通過UID來做許可權檢查的安全機制來粗放了。於是,Linux從2.2開始,從程序的許可權進行了細分,稱為Capabilities。一個程序所具有Capabilities可以通過capset和prctl等系統API來設定。也就是說,當一個程序呼叫一個敏感的系統API時,Linux核心除了考慮它的UID之外,還會考慮它是否具有對應的Capability。
以上就是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漏洞來看一下有多大。
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。