在main(:72)函數中,首先擷取了RLIMIT_NPROC的值(:83),這個值是Linux核心中定義的每個使用者可以運作的最大程序數。
然後,調用find_adb()函數(:94)來搜尋Android系統中adb程序的PID,具體而言,該函數讀取每個程序對應的檔案的/proc/<pid>/cmdline,根據其是否等于”/sbin/adb”來判斷是否adb程序。
接下來,fork了一個新的程序(:109),父程序退出,而子程序繼續。接下來,在113行建立一個管道。
<code>if</code>
<code>(fork() > 0)</code>
<code> </code><code>exit</code><code>(0);</code>
<code>setsid();</code>
<code>pipe(pepe);</code>
重頭戲發生在下面的122到138行,代碼如下:
<code>(fork() == 0) {</code>
<code> </code><code>close(pepe[0]);</code>
<code> </code><code>for</code>
<code>(;;) {</code>
<code> </code><code>if</code>
<code>((p = fork()) == 0) {</code>
<code> </code><code>exit</code><code>(0);</code>
<code> </code><code>}</code><code>else</code>
<code>if</code> <code>(p < 0) {</code>
<code> </code><code>if</code>
<code>(new_pids) {</code>
<code> </code><code>printf</code><code>(</code><code>"\n[+] Forked %d childs.\n"</code><code>, pids);</code>
<code> </code><code>new_pids = 0;</code>
<code> </code><code>write(pepe[1], &c, 1);</code>
<code> </code><code>close(pepe[1]);</code>
<code> </code><code>}</code>
<code>{</code>
<code> </code><code>++pids;</code>
<code> </code><code>}</code>
<code> </code><code>}</code>
<code>}</code>
建立一個程序後,在子程序之中,exploit代碼不斷地fork()(:125),而新的子程序不斷退出,進而産生大量的僵屍程序(占據shell使用者的程序數)。最終,程序數達到上限,fork()傳回小于0,于是列印目前已經建立多少子程序,并向管道輸入一個字元(:131)。
在這裡,管道的作用是和(:122)fork出來的父程序同步,該程序在141行read這一管道,因而阻塞直至僵屍程序已經達到上限(:131)。
進一步的,exploit殺掉adb程序,并在系統檢測到這一現象并重新開機一個adb之前,再一次fork(),将前一個adb留下的程序空位占據。最後,在152行,exploit調用wait_for_root_adb(),等待系統重新開機一個adb,這個建立的adb就會具有root權限。
為什麼在shell使用者的程序數達到上限RLIMIT_NPROC以後,建立的adb會具有root權限?我們來看adb的源碼。
在<android_src>/system/core/adb/adb.c的第918行,我們可以看到如下代碼:
<code>/* then switch user and group to "shell" */</code>
<code>(setgid(AID_SHELL) != 0) {</code>
<code> </code><code>exit</code><code>(1);</code>
<code>(setuid(AID_SHELL) != 0) {</code>
這已經是漏洞修補以後的代碼。在漏洞最初被發現時,代碼如下:
<code>setgid(AID_SHELL);</code>
<code>setuid(AID_SHELL);</code>
簡而言之,原來沒有檢查setuid()函數的傳回值。事實上,在此之前,adb.c中的代碼都是以root權限運作,以完成部分初始化工作。在這一行,通過調用setuid()将使用者從root切換回shell,但setuid()在shell使用者程序數達到上限RLIMIT_NPROC時,會失敗,是以adb.c繼續以root身份運作,而沒有報錯。
我們來看setuid()的man手冊(man 2 setuid),其中有如下說明:
<code>RETURN VALUE</code>
<code> </code><code>On success, zero is returned. On error, -1 is returned, and errno is</code>
<code> </code><code>set appropriately.</code>
<code>ERRORS</code>
<code> </code><code>EAGAIN The uid does not match the current uid and uid brings process</code>
<code> </code><code>over its RLIMIT_NPROC resource limit.</code>
可以看到,setuid是可能發生錯誤的,并且在uid的程序數超過RLIMIT_NPROC極限時,發生EAGAIN錯誤。
在android的源碼中,setuid()定義于<android_src>/bionic/libc/unistd/setuid.c,實際上引用了一個外部符号__setuid,這個符号在<android_src>/bionic/libc/arch_xxx/syscalls/__setuid.S中定義,最終是一個%eax=$__NR_setuid32,%ebx=uid的int 0×80中斷。
因為隻是要分析原理,我們不再鏖戰于Android,轉而看向Linux核心。
在最新的kernel2.6中,setuid()位于kernel/sys.c的682行,其中,在697行,一切正常的情況下,它會調用set_user()來完成使用者切換。
set_user()實作于同一檔案的587行,其中一部分代碼如下:
<code>(atomic_read(&new_user->processes) >= rlimit(RLIMIT_NPROC) &&</code>
<code> </code><code>new_user != INIT_USER) {</code>
<code> </code><code>free_uid(new_user);</code>
<code> </code><code>return</code>
<code>-EAGAIN;</code>
含義很明顯,當目标使用者的程序數達到上限,那系統就不能再将一個程序配置設定給它,因而傳回-EAGEIN。然後再setuid()中,直接跳過後面的代碼,而傳回錯誤。
至此,整個漏洞的原理已經分析完畢。整理如下:
1、在Android的shell使用者下,制造大量的僵屍程序,直至達到shell使用者的程序數上限RLIMIT_NPROC;
2、kill目前系統中的adb程序,并再次占據其程序位置以保持達到上限;
3、系統會在一段時間後重新開機一個adb程序,該程序最初是root使用者,在完成少許初始化工作後,調用setuid()切換至shell使用者;
4、此時shell使用者的程序數已經達到上限,是以setuid()失敗,傳回-1,并且使用者更換沒有完成,adb還是root權限;
5、adb沒有檢查setuid()的傳回值,繼續後續的工作,是以産生了一個具有root權限的adb程序,可以被用于與使用者的下一步互動。
是以,這并不是一個全新的漏洞。我們可以得出幾點結論:
1、函數傳回值一直是忽略的對象,因為getuid()永遠不會失敗,程式員可能會認為setuid()也不會失敗——至少沒有遇到過,是以忽略了對傳回值的檢查。檢查一個系統函數是否調用失敗是一個常識,但又是很麻煩的事,如果為了省事而忽略,問題就可能産生了。
2、Android下的安全問題,很多并非全新的,而且個人判斷将來還會有大量漏洞、惡意代碼産生于傳統思路,而作用于新的平台。面對這一新的平台,我們是否能搶先于攻擊者做好防範準備,是一個需要我們思考和實踐的問題。