zergRush (CVE-2011-3874) 提权漏洞分析
扫描二维码
随时随地手机看文章
最近(终于)转Android了,2011年著名的zergrush是接触的第一个ROOT漏洞。虽然它已经过气了,只影响Android 2.2 - 2.3.6,但觉得还是有必要记录一下分析所得。
市面上各种ROOT工具基本都包含zergrush,大多是开源的zergRush.c直接编译而来。已有的分析文章:
tomken_zhang,漏洞 — zergRush,漏洞 — zergRush (补充)
Claud, Android提权代码zergRush分析
分析内容集中在zergRush.c的代码结构上,对漏洞原理没有解析,或者错误地认为是栈溢出。其实CVE-2011-3874已经描述得很明白,这个漏洞的本质是"use after free"。
1. 栈溢出?No.
漏洞存在于/system/bin/vold这个root身份的系统程序。具体地,vold调用了libsysutils.so,真正有问题的是这个so。再具体地,问题出在/system/core/libsysutils/src/FrameworkListener.cpp的FrameworkListener::dispatchCommand方法。
它在栈上分配了一个固定大小的数组argv,
void FrameworkListener::dispatchCommand(SocketClient *cli, char*data) { FrameworkCommandCollection::iterator i; intargc =0; char*argv[FrameworkListener::CMD_ARGS_MAX]; chartmp[255]; char*p = data; char*q = tmp; bool esc =false; bool quote =false; intk;
FrameworkListener::CMD_ARGS_MAX = 16。但后面填充argv数组时,代码并没有检查是否发生了越界。
if (!quote && *q == ' ') { *q =' '; argv[argc++] = strdup(tmp); memset(tmp,0, sizeof(tmp)); q = tmp; continue; }
让argv越界是很容易的,越界的数据会被写入argv下面的tmp数组中。zergRush实际上只是向argv中填充了CMD_ARGS_MAX + 2个char *,越界了8字节而已,255字节的tmp数组完全接得住,并没有破坏dispatchCommand的栈。其实,就算dispatchCommand的栈溢出了,也没什么事,因为它被编译进了__stack_chk,一旦返回地址改变,会异常结束。所以,这个漏洞不是“栈溢出”。
2. free(任意地址)
用户程序向system/bin/vold发送的数据会到达FrameworkListener::onDataAvailable。onDataAvailable会从数据中提取命令字符串,并调用dispatchCommand处理命令。用户数据可能包含多条命令,每条命令是一个以' '结尾的字符串,由名称和若干参数构成,名称和参数由空格分隔。例如,
"cmd1 arg11 arg12 cmd2 arg21 arg22 arg23 "
这样的数据进入onDataAvailable后,会提取出2条命令字符串:"cmd1 arg11 arg12" 和 "cmd2 arg21 arg22 arg23",对每条命令调用一次dispatchCommand。dispatchCommand接收到命令字符串后,会进一步解析出命令名称和参数,并存入argv数组。比如,对于"cmd1 arg11 arg12",dispatchCommand完成解析后,
argv[0] =="cmd1"; argv[1] =="arg11"; argv[2] =="arg12"
名称和每个参数,最初都是解析到tmp中,argv的每个字符串指针都是strdup(tmp)出来的,在dispatchCommand返回前要free掉,
int j; for (j = 0; j < argc; j++) free(argv[j]);
这是一个很正常的逻辑,但在argv数组越界,与tmp发生交叠的情况下,有意思的事情就发生了。假设有一条命令带有17个参数,
"cmd p1 p2 p3 p4 p5 p6 p7 p8 p9 p10 p11 p12 p13 p14 p15 p16 x78x56x34x12"
解析后,argv数组的内容是:
argv[0] =="cmd"; argv[1] =="p1"; argv[2] =="p2"; argv[3] =="p3"; argv[4] =="p4"; argv[5] =="p5"; argv[6] =="p6"; argv[7] =="p7"; argv[8] =="p8"; argv[9] =="p9"; argv[10] =="p10"; argv[11] =="p11"; argv[12] =="p12"; argv[13] =="p13"; argv[14] =="p14"; argv[15] =="p15"; argv[16] ==0x12345678; argv[17] =="x78x56x34x12"
argv数组大小只有16项,argv[16]实际上是tmp数组的前4个字节,而最后一个参数是"x78x56x34x12",它被解析出来后缓存到tmp里,这样tmp的前4字节就成了0x78,0x56, 0x34, 0x12 ,因此argv[16]==0x12345678。之后,argv[16]被free,即执行了free(0x12345678)。我们可以任意控制参数内容,即可实现:free(任意地址)。
3. 指针复用和vtable覆盖
能够free掉任意指针了,如何实现exploit呢?最简单的办法是free掉一个带vtable的C++对象指针,然后重用这个指针,从而控制vtable。很幸运的是,dispatchCommand的代码逻辑支持这么做。它会对比接收到的命令和已注册的FrameworkCommand,如果匹配,就去运行匹配的FrameworkCommand。
for (i = mCommands->begin(); i != mCommands->end(); ++i) { FrameworkCommand *c = *i; if(!strcmp(argv[0], c->getCommand())) { if(c->runCommand(cli, argc, argv)) { SLOGW("Handler '%s' error (%s)", c->getCommand(), strerror(errno)); } gotoout; } }
runCommand是FrameworkCommand的virtual方法,FrameworkCommand对象指针就是要free掉然后复用的目标。mCommands是FrameworkListener的成员,它包含了若干FrameworkCommand对象指针,如果知道了FrameworkListener *this,寻址到FrameworkCommand不在话下,这也是zergRush.c的heap_oracle() 做的事情。
假设我们成功free掉了一个FrameworkCommand指针,怎样复用这个指针呢?只需要再调用dispatchCommand解析一条命令,在解析过程中,第一次strdup(tmp)返回的就是这个指针,并且已经把tmp字符串复制到这个地址了。即:命令的前4个字节已经成为这个FrameworkCommand对象的vtable,实现了vtable的任意指定。观察上面C++代码对应的汇编:
.text:000029F6 LDR.W R6, [R8] .text:000029FA MOV R0, R10 ; s1 .text:000029FC LDR R1, [R6,#4] ; s2 .text:000029FE BLX strcmp .text:00002A02 CBNZ R0, loc_2A38 .text:00002A04 LDR R0, [R6] ;R0 = vtable .text:00002A06 MOV R1, R5 .text:00002A08 MOV R2, R4 .text:00002A0A MOV R3, R7 .text:00002A0C LDR.W R12, [R0,#8] ;R12 = runCommand .text:00002A10 MOV R0, R6 .text:00002A12 BLX R12 ;Call runCommand .text:00002A14 CBZ R0, loc_2A50 .text:00002A16 LDR R5, [R6,#4] .text:00002A18 BLX __errno .text:00002A1C LDR R0, [R0] ; errnum .text:00002A1E BLX strerror .text:00002A22 LDR R2, =(aFrameworkliste -0x2A2E) .text:00002A24 LDR R3, =(aHandlerSErrorS -0x2A30) .text:00002A26 MOVS R1, #5 .text:00002A28 STR R5, [SP,#0x484+var_484] .text:00002A2A ADD R2, PC ;"FrameworkListener" .text:00002A2C ADD R3, PC ;"Handler '%s' error (%s)" .text:00002A2E STR R0, [SP,#0x484+var_480] .text:00002A30 MOVS R0, #3 .text:00002A32 BLX __android_log_buf_print .text:00002A36 B loc_2A50
调用runCommand,实际是调用了[vtable + 8]。比如第二次传给dispatchCommand的命令是"AAAA blablabla",vtable会被覆盖成0x41414141,PC将被指向 [0x41414149]。至此,已经成功的控制了指令流,剩下的就是构造攻击数据的奇淫技巧了,比如ROP等,和漏洞本身没有直接关系了。zergRush.c成功地调用system()执行任意命令,达到了ROOT的目的。
4. 总结
CVE-2011-3874这个漏洞源于栈上的数组越界,用户程序通过向/system/bin/vold发送一段包含2条特殊命令的数据,可以控制root身份的vold执行system(),借以提权。用户数据中的第1条命令会free掉一个FrameworkCommand对象;而第2条命令会重新占用对象地址,进而覆盖vtable实现控制指令流。
在Android 2.2-2.3.6没有ASLR的前提下,漏洞利用过程只有2个必要数据无法直接获取:dispatchCommand的this指针和栈地址SP。不幸(幸运)的是,强大的logcat填上了这个坑。有了logcat,即使有ASLR,对这个漏洞也没有防护能力。
没有进行补丁的源码
void FrameworkListener::dispatchCommand(SocketClient *cli, char *data) { FrameworkCommandCollection::iterator i; int argc = 0; char *argv[FrameworkListener::CMD_ARGS_MAX]; char tmp[CMD_BUF_SIZE]; char *p = data; char *q = tmp; char *qlimit = tmp + sizeof(tmp) - 1; bool esc = false; bool quote = false; int k; bool haveCmdNum = !mWithSeq; memset(argv, 0, sizeof(argv)); memset(tmp, 0, sizeof(tmp)); while(*p) { if (*p == '\') { if (esc) { if (q >= qlimit) goto overflow; *q++ = '\'; esc = false; } else esc = true; p++; continue; } else if (esc) { if (*p == '"') { if (q >= qlimit) goto overflow; *q++ = '"'; } else if (*p == '\') { if (q >= qlimit) goto overflow; *q++ = '\'; } else { cli->sendMsg(500, "Unsupported escape sequence", false); goto out; } p++; esc = false; continue; } if (*p == '"') { if (quote) quote = false; else quote = true; p++; continue; } if (q >= qlimit) goto overflow; *q = *p++; if (!quote && *q == ' ') { *q = ' '; if (!haveCmdNum) { char *endptr; int cmdNum = (int)strtol(tmp, &endptr, 0); if (endptr == NULL || *endptr != ' ') { cli->sendMsg(500, "Invalid sequence number", false); goto out; } cli->setCmdNum(cmdNum); haveCmdNum = true; } else { argv[argc++] = strdup(tmp); } memset(tmp, 0, sizeof(tmp)); q = tmp; continue; } q++; } *q = ' '; argv[argc++] = strdup(tmp); #if 0 for (k = 0; k < argc; k++) { SLOGD("arg[%d] = '%s'", k, argv[k]); } #endif if (quote) { cli->sendMsg(500, "Unclosed quotes error", false); goto out; } if (errorRate && (++mCommandCount % errorRate == 0)) { /* ignore this command - let the timeout handler handle it */ SLOGE("Faking a timeout"); goto out; } for (i = mCommands->begin(); i != mCommands->end(); ++i) { FrameworkCommand *c = *i; if (!strcmp(argv[0], c->getCommand())) { if (c->runCommand(cli, argc, argv)) { SLOGW("Handler '%s' error (%s)", c->getCommand(), strerror(errno)); } goto out; } } cli->sendMsg(500, "Command not recognized", false); out: int j; for (j = 0; j < argc; j++) free(argv[j]); return; overflow: LOG_EVENT_INT(78001, cli->getUid()); cli->sendMsg(500, "Command too long", false); goto out; }