部分内容参考自http://www.cnblogs.com/daishuo/p/4002963.html
zergRush是我接触的第一个CVE漏洞,该漏洞影响安卓2.2-2.3.6版本系统。CVE-2011-3874描述得很明白,这个漏洞的本质是"use after free"。
漏洞存在于/system/bin/vold这个root身份的系统程序。具体地,vold调用了libsysutils.so,真正有问题的是这个 so。对应源码在/system/core/libsysutils/src/FrameworkListener.cpp的 FrameworkListener::dispatchCommand方法。
FrameworkListener.cpp源码
1 bool FrameworkListener::onDataAvailable(SocketClient *c) { 2 char buffer[255]; 3 int len; 4 5 if ((len = read(c->getSocket(), buffer, sizeof(buffer) -1)) < 0) { 6 SLOGE("read() failed (%s)", strerror(errno)); 7 return errno; 8 } else if (!len) 9 return false; 10 11 int offset = 0; 12 int i; 13 14 for (i = 0; i < len; i++) { 15 if (buffer[i] == ‘\0‘) { 16 dispatchCommand(c, buffer + offset); 17 offset = i + 1; 18 } 19 } 20 return true; 21 } 27 void FrameworkListener::dispatchCommand(SocketClient *cli, char *data) { 28 FrameworkCommandCollection::iterator i; 29 int argc = 0; 30 char *argv[FrameworkListener::CMD_ARGS_MAX]; //数组长度固定 31 char tmp[255]; 32 char *p = data; 33 char *q = tmp; 34 bool esc = false; 35 bool quote = false; 36 int k; 37 38 memset(argv, 0, sizeof(argv)); 39 memset(tmp, 0, sizeof(tmp)); 40 while(*p) { 41 if (*p == ‘\\‘) { 42 if (esc) { 43 *q++ = ‘\\‘; 44 esc = false; 45 } else 46 esc = true; 47 p++; 48 continue; 49 } else if (esc) { 50 if (*p == ‘"‘) 51 *q++ = ‘"‘; 52 else if (*p == ‘\\‘) 53 *q++ = ‘\\‘; 54 else { 55 cli->sendMsg(500, "Unsupported escape sequence", false); 56 goto out; 57 } 58 p++; 59 esc = false; 60 continue; 61 } 62 63 if (*p == ‘"‘) { 64 if (quote) 65 quote = false; 66 else 67 quote = true; 68 p++; 69 continue; 70 } 71 72 *q = *p++; 73 if (!quote && *q == ‘ ‘) { 74 *q = ‘\0‘; 75 argv[argc++] = strdup(tmp); //没有检查长度 76 memset(tmp, 0, sizeof(tmp)); 77 q = tmp; 78 continue; 79 } 80 q++; 81 } 82 83 argv[argc++] = strdup(tmp); 84 #if 0 85 for (k = 0; k < argc; k++) { 86 SLOGD("arg[%d] = ‘%s‘", k, argv[k]); 87 } 88 #endif 89 90 if (quote) { 91 cli->sendMsg(500, "Unclosed quotes error", false); 92 goto out; 93 } 94 95 for (i = mCommands->begin(); i != mCommands->end(); ++i) { 96 FrameworkCommand *c = *i; 97 98 if (!strcmp(argv[0], c->getCommand())) { 99 if (c->runCommand(cli, argc, argv)) { 100 SLOGW("Handler ‘%s‘ error (%s)", c->getCommand(), strerror(errno)); 101 } 102 goto out; 103 } 104 } 105 106 cli->sendMsg(500, "Command not recognized", false); 107 out: 108 int j; 109 for (j = 0; j < argc; j++) 110 free(argv[j]); 111 return; 112 }
1.程序流程逻辑
1-1.onDataAvailable方法监听socket输入,接收数据包后以‘\0‘为分隔符,将buffer内容分段传给dispatchCommand函数做进一步处理。
比如收到"aaa bbb ccc\0ddd eeee ff\0" 第一次传递"aaa bbb ccc\0" 第一个a的pos给dispatchCommand 第二次传递"ddd eeee ff\0" 第一个d的pos给dispatchCommand
1-2.dispatchCommand将接受的字符串以空格分割,调用strdup函数在堆中生成复制,把堆中地址保存到argv数组
"aaa bbb ccc\0"被保存成 argv[0]=&"aaa" argv[1]=&"bbb" argv[2]=&"ccc"
1-3.95行开始,将argv[0]与FrameworkCommand内置命令比对,若匹配执行命令,因此argv[0]是命令,argv[1]开始是对应的参数
1-4.执行完命令后free argv数组(因为strdup是在堆中生成复制,所以free理所当然)
107 out: 108 int j; 109 for (j = 0; j < argc; j++) 110 free(argv[j]); 111 return; 112 }
2.漏洞代码
30 char *argv[FrameworkListener::CMD_ARGS_MAX]; //数组长度固定 31 char tmp[255];
72 *q = *p++; 73 if (!quote && *q == ‘ ‘) { 74 *q = ‘\0‘; 75 argv[argc++] = strdup(tmp); //没有检查长度 76 memset(tmp, 0, sizeof(tmp)); 77 q = tmp; 78 continue; 79 } 80 q++; 81 } 82 83 argv[argc++] = strdup(tmp);
30行定义的定长数组,但75行向数组加元素时没检查边界,导致数组越界,CMD_ARGS_MAX=16,因此操作argv[16]实际覆盖了tmp的前4个字节
3.利用思路
108 int j; 109 for (j = 0; j < argc; j++) 110 free(argv[j]); 111 return;
109行有个free,在越界后可以free掉tmp中的内容。现在的思路是控制argv数组的内容,但argv本身不可控(因为都是strdup返回地址),若tmp内容可控,则实现了free(任意地址),恰好可以这么做。
最后一次数组元素时(83行)保证了tmp的前几个字节是攻击者构造的命令的参数,也就是argv[16]开始可以被攻击者控制
攻击字符串 "00 11 22 33 44 55 66 77 88 99 aa bb cc dd ee xx \x78\x56\x34\x12 \xdd\xcc\xbb\xaa" 整理argv数组的操作后 argv[0] = &"00" argv[1] = &"11" . . argv[15] = &"ee" argv[16] = 0xaabbccdd argv[17] = &"\x78\x56\x34\x12" argv[18] = &"\xdd\xcc\xbb\xaa"
这时在110行free(argv[16])可以free(任意地址)
107 out: 108 int j; 109 for (j = 0; j < argc; j++) 110 free(argv[j]); 111 return; 112 }
可以利用free来攻击虚函数。
zergRush攻击的思路先free对象c,这时这片堆空间恢复成空闲态,当有新的申请时这个空间很可能再次被分配,若能控制新申请时堆中的内容,则相当于控制了虚表内容。
幸运的是程序允许我们这么干。
95 for (i = mCommands->begin(); i != mCommands->end(); ++i) { 96 FrameworkCommand *c = *i; 97 98 if (!strcmp(argv[0], c->getCommand())) { 99 if (c->runCommand(cli, argc, argv)) { 100 SLOGW("Handler ‘%s‘ error (%s)", c->getCommand(), strerror(errno)); 101 } 102 goto out; 103 }
具体的,99行的runCommand函数是虚函数,先想办法得到c的地址填入argv[17],在free时free掉c
之后的思路是想办法在调用c->runCommand前将c的堆空间内容控制住,若这时向dispatchCommand传入新的命令片段,则在第一次strdup时有可能申请的堆空间就是刚刚c的空间,恰好strdup的堆块内容是传入的参数的复制品,若构造好参数则控制了堆中内容,也就控制了虚表。这时程序执行到c->runCommand时就劫持了控制流。