@binarym 的回答非常好。他已经解释了缓冲区溢出背后的原因、如何找到简单的溢出以及我们如何使用 corefile 和/或 GDB 查看堆栈。我只想添加两个额外的细节:
- 一个更深入的黑盒测试示例,即:
描述如何持续检测缓冲区溢出(黑盒测试)
- 编译器怪癖,即黑盒测试失败的地方(或多或少,它更像是黑盒生成的有效负载可能失败的地方)。
我们将使用的代码稍微复杂一些:
#include <stdio.h>
#include <string.h>
void do_post(void)
{
char curr = 0, message[128] = {};
int i = 0;
while (EOF != (curr = getchar())) {
if ('\n' == curr) {
message[i] = 0;
break;
} else {
message[i] = curr;
}
i++;
}
printf("I got your message, it is: %s\n", message);
return;
}
int main(void)
{
char curr = 0, request[8] = {};
int i = 0;
while (EOF != (curr = getchar())) {
request[i] = curr;
if (!strcmp(request, "GET\n")) {
printf("It's a GET!\n");
return 0;
} else if (!strcmp(request, "POST\n")) {
printf("It's a POST, get the message\n");
do_post();
return 0;
} else if (5 < strlen(request)) {
printf("Some rubbish\n");
return 1;
} /* else keep reading */
i++;
}
printf("Assertion error, THIS IS A BUG please report it\n");
return 0;
}
我用 POST 和 GET 请求来取笑 HTTP。而且我getchar()
习惯于逐个字符地阅读 STDIN(这是一个糟糕的实现,但它具有教育意义)。该代码将区分 GET、POST 和“垃圾”(无论其他),并使用或多或少正确编写的循环(没有溢出)来做到这一点。
然而,在解析 POST 消息时,message[128]
缓冲区中存在溢出。不幸的是,缓冲区在程序内部很深(嗯,不是很深,但是一个简单的长参数不会找到它)。让我们编译它并尝试长字符串:
[~]$ gcc -O2 -o over over.c
[~]$ perl -e 'print "A"x2000' | ./over
Some rubbish
是的,那是行不通的。因为我们知道代码,所以我们知道如果我们在开头添加“POST\n”,我们将触发溢出。但是如果我们不知道代码怎么办?还是代码太复杂?进入黑盒测试。
黑盒测试
最流行的黑盒测试技术是模糊测试。几乎所有其他(黑盒)技术都是它的变体。Fuzzing 只是简单地为程序提供随机输入,直到我们发现一些有趣的东西。我写了一个简单的 fuzzing 脚本来检查这个程序,我们来看看:
#!/usr/bin/env python3
from itertools import product
from subprocess import Popen, PIPE, DEVNULL
prog = './over'
valid_returns = [ 0, 1 ]
all_chars = list(map(chr, range(256)))
# This assumes that we may find something with an input as small as 1024 bytes,
# which isn't realistic. In the real world several megabytes of need to be
# tried.
for input_size in range(1,1024):
input = [p for p in product(all_chars, repeat=input_size)]
for single_input in input:
child = Popen(prog, stdin=PIPE, stdout=DEVNULL)
byte_input = (''.join(single_input)).encode("utf-8")
child.communicate(input=byte_input)
child.stdin.close()
ret = child.wait()
if not ret in valid_returns:
print("INPUT", repr(byte_input), "RETURN", ret)
exit(0)
# The exit(0) is not realistic either, in the real world I'd like to have a
# full log of the entire search space.
它只是这样做:为程序提供越来越大的随机输入。(警告:该脚本需要大量 RAM)我运行它,几个小时后我得到一个有趣的输出:
INPUT b"POST\nXl_/.\xc3\x93\xc3\x90\xc2\x87\xc3\xa6dh\xc3\xaeH\xc2\xa0\xc2\x836\x16.\xc3\xb7\x1be\x1e,\xc3\x98\xc3\xa4\xc2\x81\xc2\x83 su\xc2\xb1\xc3\xb2\xc3\x8d^\xc2\xbc\xc2\xa11/\xc2\x9f\x12vY\x12[0\x0c]\xc3\xb6\x19zI\xc2\xb8\xc2\xb5\xc3\xbb\xc2\x9e\xc3\xab>^\xc2\x85\xc2\x91\xc2\xb5\xc2\xb5\xc3\xb6u\xc3\x8e).\xc3\xbcn\x1aM\xc3\xbb+{\x1c\xc3\x9a\xc3\x8b&\xc2\x93\xc2\xa1D\xc3\xad\xc3\xad\xc3\x81\xc2\xbd\xc2\x8d\xc2\xa3 \xc3\x87_\xc2\x82\xc3\x9asv\xc3\x92\xc2\x85IP\xc2\xb8\x1bS\xc3\xbe\xc3\x9e\\\xc2\x8e\xc3\x9f\xc2\xb1\xc3\xa4\xc2\xbe\x1fue\xc3\x81\xc3\x8a\xc2\x8b'\xc3\xaf\xc2\xa1\xc3\x95'\xc2\xaa\xc3\xa8P\xc2\xa7\xc2\x8f\xc3\x99\xc2\x94S5\xc2\x83\xc3\x85U" RETURN -11
进程退出-11,是段错误吗?让我们来看看:
kill -l | grep SIGSEGV
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
好吧,这是一个分段错误(请参阅此答案以进行澄清)。现在我确实有一个输入样本,我可以用它来模拟这个段错误并发现(使用 GDB)溢出在哪里。
编译器怪癖
你在上面看到了什么奇怪的东西吗?有一条信息我省略了,我在下面使用了一个剧透标签,所以你可以回去尝试弄清楚。答案在这里:
为什么我用的见鬼gcc -O2 -o over over.c
?为什么一个平原gcc -o over over.c
还不够?-O2
在这种情况下,编译器优化 ( ) 有什么特别之处?
说句公道话,我自己也很惊讶我能在这样一个简单的程序中找到这种行为。出于性能原因,编译器在编译期间会重写大量代码。编译器也确实试图减轻一些风险(例如清晰可见的溢出)。通常,相同的代码在启用和不启用优化的情况下可能看起来非常不同。
让我们看看这个特定的怪癖,但让我们回到 perl,因为我们已经知道这个漏洞:
[~]$ gcc -O2 -o over over.c
[~]$ perl -e 'print "POST\n" . "A"x2000' | ./over
It's a POST, get the message
I got your message, it is: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAins
Segmentation fault (core dumped)
是的,这正是我们所期望的。但是现在,让我们禁用优化:
[~]$ gcc -o over over.c
[~]$ perl -e 'print "POST\n" . "A"x2000' | ./over
It's a POST, get the message
I got your message, it is: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAÿ}
$ echo $?
0
我勒个去!编译器设法修补了我非常喜欢制作的漏洞。如果您查看该消息的长度,您会发现它有 141 个字节长。缓冲区确实溢出了,但是编译器添加了某种程序集来停止写入,以防溢出变得重要。
对于怀疑论者,这是我用来获得上述行为的编译器版本:
[~]$ gcc --version
gcc (GCC) 6.2.1 20160830
Copyright (C) 2016 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
这个故事的寓意是,如果由相同的编译器和相同的优化(甚至其他参数)编译,大多数缓冲区溢出漏洞只能在相同的有效负载下工作。编译器会对你的代码做一些坏事以使其运行得更快,尽管有效载荷很有可能在由两个编译器编译的同一程序上工作,但这并不总是正确的。
后记
我做这个答案是为了好玩,并为自己做一个记录。我不应该得到赏金,因为我没有完全回答你的问题,我只回答了赏金定义中添加的额外问题。bynarym 的回答值得赏金,因为他回答了原始问题的更多部分。