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] = 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时就劫持了控制流。

 

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。