Android adb setuid提權漏洞的分析
去年的Android adb setuid提權漏洞被用於各類root刷機,漏洞發現人Sebastian Krahmer公佈的利用工具RageAgainstTheCage(rageagainstthecage-arm5.bin)被用於z4root等提權工具、Trojan.Android.Rootcager等惡意程式碼之中。下面我們來分析這一漏洞的產生原因。
The Android Exploid Crew小組在後來發布了一份PoC程式碼:rageagainstthecage.c。從這份程式碼開始著手。
在main(:72)函式中,首先獲取了RLIMIT_NPROC的值(:83),這個值是Linux核心中定義的每個使用者可以執行的最大程序數。
然後,呼叫find_adb()函式(:94)來搜尋Android系統中adb程序的PID,具體而言,該函式讀取每個程序對應的檔案的/proc/<pid>/cmdline,根據其是否等於”/sbin/adb”來判斷是否adb程序。
接下來,fork了一個新的程序(:109),父程序退出,而子程序繼續。接下來,在113行建立一個管道。
if
(fork() > 0)
exit (0);
setsid();
pipe(pepe);
|
重頭戲發生在下面的122到138行,程式碼如下:
if
(fork() == 0) { close(pepe[0]);
for
(;;) {
if
((p = fork()) == 0) {
exit (0);
}
else
if (p < 0) {
if
(new_pids) {
printf ( "\n[+] Forked %d childs.\n" , pids);
new_pids = 0;
write(pepe[1], &c, 1);
close(pepe[1]);
}
}
else {
++pids;
}
}
}
|
新建一個程序後,在子程序之中,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行,我們可以看到如下程式碼:
/* then switch user and group to "shell" */
if
(setgid(AID_SHELL) != 0) {
exit (1);
}
if
(setuid(AID_SHELL) != 0) {
exit (1);
}
|
這已經是漏洞修補以後的程式碼。在漏洞最初被發現時,程式碼如下:
/* then switch user and group to "shell" */
setgid(AID_SHELL);
setuid(AID_SHELL);
|
簡而言之,原來沒有檢查setuid()函式的返回值。事實上,在此之前,adb.c中的程式碼都是以root許可權執行,以完成部分初始化工作。在這一行,通過呼叫setuid()將使用者從root切換回shell,但setuid()在shell使用者程序數達到上限RLIMIT_NPROC時,會失敗,因此adb.c繼續以root身份執行,而沒有報錯。
我們來看setuid()的man手冊(man 2 setuid),其中有如下說明:
RETURN VALUE
On success, zero is returned. On error, -1 is returned, and errno is
set appropriately.
ERRORS
EAGAIN The uid does not match the current uid and uid brings process
over its RLIMIT_NPROC resource limit.
|
可以看到,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行,其中一部分程式碼如下:
if
(atomic_read(&new_user->processes) >= rlimit(RLIMIT_NPROC) &&
new_user != INIT_USER) {
free_uid(new_user);
return
-EAGAIN;
}
|
含義很明顯,當目標使用者的程序數達到上限,那系統就不能再將一個程序分配給它,因而返回-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程序,可以被用於與使用者的下一步互動。
實際上,setuid在目標使用者程序數達到RLIMIT_NPROC極限時返回錯誤,這一問題可能產生的安全隱患最早可以追溯到2000年。而在2006年,出現了真正利用這一編碼問題的漏洞(CVE-2006-2607)。
因此,這並不是一個全新的漏洞。我們可以得出幾點結論:
1、函式返回值一直是忽略的物件,因為getuid()永遠不會失敗,程式設計師可能會認為setuid()也不會失敗——至少沒有遇到過,因此忽略了對返回值的檢查。檢查一個系統函式是否呼叫失敗是一個常識,但又是很麻煩的事,如果為了省事而忽略,問題就可能產生了。
2、Android下的安全問題,很多並非全新的,而且個人判斷將來還會有大量漏洞、惡意程式碼產生於傳統思路,而作用於新的平臺。面對這一新的平臺,我們是否能搶先於攻擊者做好防範準備,是一個需要我們思考和實踐的問題。