首页 > 代码库 > zergRush (CVE-2011-3874) 安卓内核漏洞成因分析

zergRush (CVE-2011-3874) 安卓内核漏洞成因分析

部分内容参考自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] = 0xaabbccddargv[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时就劫持了控制流。

 

zergRush (CVE-2011-3874) 安卓内核漏洞成因分析