1. 程式人生 > >Android adb setuid提權漏洞的分析

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行建立一個管道。

rageagainstthecage.c
if (fork() > 0) exit(0); setsid(); pipe(pepe);

重頭戲發生在下面的122到138行,程式碼如下:

rageagainstthecage.c
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行,我們可以看到如下程式碼:

android_src/system/core/adb/adb.c
/* then switch user and group to "shell" */ if (setgid(AID_SHELL) != 0) { exit(1); } if (setuid(AID_SHELL) != 0) { exit(1); }

這已經是漏洞修補以後的程式碼。在漏洞最初被發現時,程式碼如下:

android_src/system/core/adb/adb.c
/* 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),其中有如下說明:

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行,其中一部分程式碼如下:

kernel/sys.c
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下的安全問題,很多並非全新的,而且個人判斷將來還會有大量漏洞、惡意程式碼產生於傳統思路,而作用於新的平臺。面對這一新的平臺,我們是否能搶先於攻擊者做好防範準備,是一個需要我們思考和實踐的問題。