查看堆栈

信息安全 网络 缓冲区溢出 记忆 逆向工程 黑盒子
2021-08-31 09:23:54

我最近开始学习缓冲区溢出及其工作原理。有人分享了一个二进制文件来练习(在虚拟机中,不用担心)。我一直在向二进制文件打开的套接字提供字符串,我注意到在一定长度下,字符串将导致程序不响应它应该响应的消息。此外,如果我提供另一个特定长度的字符串,则部分消息会通过套接字从服务器发送回,但其他部分只是打印到服务器端的控制台。我不完全确定是什么原因造成的(这不是这篇文章的官方问题,但我很想在评论中听到答案)。

这让我想到了我的问题:是否有任何应用程序可以生成堆栈图像或转储它以及通常写入的内容?我认为当我提供不同长度的插座串时,这对于查看正在发生的事情非常有帮助。如果堆栈的每个“部分”的大小(不知道它叫什么)在图像中以相对于其他部分的大小表示,我会喜欢它(所以,我可以可视化堆栈的大小) ,或以可读的方式。

如果您的答案是关于生成图像,这样的事情会很棒,除非它显示写入了多少内容会很好(这样我可以看到它何时溢出)......

堆栈图像

当我启动程序时,我可能会生成一个图像,并且在我为套接字提供巨大的值之后。那我比较一下。如果有任何其他方式/更好的学习方式,我很想听听。

编辑#1:我是黑盒测试。

编辑#2:虽然这个问题已经有一个公认的答案,但我也很感激其他答案。信息和响应越多,我就能学到越多。因此,我将用赏金奖励新的答案(如果值得的话)。欣赏它!

2个回答

以简单的方式获取内存转储

您可以简单地向易受攻击的进程发送 SIGSEGV (kill -SEGV pid),如果允许 coredump (ulimit -c unlimited),您将获得一个包含所有内存的漂亮核心转储文件。

例子:

在 1 号航站楼:

/tmp$ ./test 
idling...
idling...
Segmentation fault <---- HERE I SEND THE 1st SIGSEGV
/tmp$ ulimit -c unlimited
/tmp$ ./test 
idling...
idling...
Segmentation fault (core dumped) <---- HERE IS THE 2d SIGSEGV
/tmp$ ls test
test    test.c  
/tmp$ ls -lah core 
-rw------- 1 1000 1000 252K Oct 10 17:42 core

在 2 号航站楼

/tmp$ ps aux|grep test
1000  6529  0.0  0.0   4080   644 pts/1    S+   17:42   0:00 ./test
1000  6538  0.0  0.0  12732  2108 pts/2    S+   17:42   0:00 grep test
/tmp$ kill -SEGV 6529
/tmp$ ps aux|grep test
1000  6539  0.0  0.0   4080   648 pts/1    S+   17:42   0:00 ./test
1000  6542  0.0  0.0  12732  2224 pts/2    S+   17:42   0:00 grep test
/tmp$ kill -SEGV 6539

请注意,这将在二进制文件获得 SIGSEGV 时为您提供状态转储。因此,如果您的二进制文件由 main() 和 evil_function() 组成,并且在接收 SIGSEV 时,您的程序正在运行 evil_function(),您将获得 evil_function() 堆栈。但是您也可以四处检查以返回 main() 堆栈。

关于 Aleph One 论文的所有内容的好指针:http: //insecure.org/stf/smashstack.html

自己猜“映射”

如果我们想象你的二进制文件正在实现一个基本的缓冲区溢出,就像在这个代码片段中一样:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>


int evil_function(char *evil_input)
{
    char stack_buffer[10];
    strcpy(stack_buffer, evil_input);
    printf("input is: %s\n", stack_buffer);
    return 0;
}


int main (int ac, char **av)
{
    if (ac != 2) 
    {
        printf("Wrong parameter count.\nUsage: %s: <string>\n",av[0]);
        return EXIT_FAILURE;
    }
    evil_function(av[1]);

    return (EXIT_SUCCESS);
}

仅使用 gdb 就可以很容易地猜测应该将缓冲区地址写入何处。让我们试试上面的示例程序:

/tmp/bo-test$ ./test-buffer-overflow $(perl -e "print 'A'x10")
input is: AAAAAAAAAA
/tmp/bo-test$ ./test-buffer-overflow $(perl -e "print 'A'x11")
input is: AAAAAAAAAAA
/tmp/bo-test$ ./test-buffer-overflow $(perl -e "print 'A'x12")
input is: AAAAAAAAAAAA
/tmp/bo-test$ ./test-buffer-overflow $(perl -e "print 'A'x13")
input is: AAAAAAAAAAAAA
/tmp/bo-test$ ./test-buffer-overflow $(perl -e "print 'A'x14")
input is: AAAAAAAAAAAAAA
/tmp/bo-test$ ./test-buffer-overflow $(perl -e "print 'A'x15")
input is: AAAAAAAAAAAAAAA
/tmp/bo-test$ ./test-buffer-overflow $(perl -e "print 'A'x16")
input is: AAAAAAAAAAAAAAAA
Segmentation fault (core dumped)

好的,所以在给了 6 个额外的字符后,堆栈开始被搞砸了......让我们看看堆栈:

/tmp/bo-test$ gdb test-buffer-overflow core
GNU gdb (Debian 7.7.1+dfsg-5) 7.7.1
[...]
Core was generated by `./test-buffer-overflow AAAAAAAAAAAAAAAA'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x00007f2cb2c46508 in ?? () from /lib/x86_64-linux-gnu/libc.so.6
(gdb) bt
#0  0x00007f2cb2c46508 in ?? () from /lib/x86_64-linux-gnu/libc.so.6
#1  0x0000000000000000 in ?? ()
(gdb) Quit

让我们继续喂它更多额外的字符:

/tmp/bo-test$ ./test-buffer-overflow $(perl -e "print 'A'x26")
input is: AAAAAAAAAAAAAAAAAAAAAAAAAA
Segmentation fault (core dumped)
/tmp/bo-test$ gdb test-buffer-overflow core
GNU gdb (Debian 7.7.1+dfsg-5) 7.7.1
[...]
Core was generated by `./test-buffer-overflow AAAAAAAAAAAAAAAAAAAAAAAAAA'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x0000000000004141 in ?? ()
(gdb) 

嘿......看看这个地址:0x00000000000041410x41 是 ...'A' 的十六进制 ascii 代码:p 我们只是重写了 RET 地址 :) 现在,最后一次尝试,只是为了看看:

/tmp/bo-test$ ./test-buffer-overflow AAAAAAAAAAAAAAAAAAAAAAAAABCDEFGHI
input is: AAAAAAAAAAAAAAAAAAAAAAAAABCDEFGHI
Segmentation fault (core dumped)
/tmp/bo-test$ gdb test-buffer-overflow core GNU gdb 
Core was generated by `./test-buffer-overflow AAAAAAAAAAAAAAAAAAAAAAAAABCDEFGHI'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x0000000000400581 in evil_function (
    evil_input=0x7fff7e2712a6 'A' <repeats 25 times>, "BCDEFGHI")
    at test-buffer-overflow.c:12
12  }
(gdb) bt
#0  0x0000000000400581 in evil_function (
    evil_input=0x7fff7e2712a6 'A' <repeats 25 times>, "BCDEFGHI")
    at test-buffer-overflow.c:12
#1  0x4847464544434241 in ?? ()
#2  0x00007fff7e260049 in ?? ()
#3  0x0000000200000000 in ?? ()
#4  0x0000000000000000 in ?? ()

这一次,再看地址:0x4847464544434241 ……现在你知道具体写在哪里了……

@binarym 的回答非常好。他已经解释了缓冲区溢出背后的原因、如何找到简单的溢出以及我们如何使用 corefile 和/或 GDB 查看堆栈。我只想添加两个额外的细节:

  1. 一个更深入的黑盒测试示例,即:

描述如何持续检测缓冲区溢出(黑盒测试)

  1. 编译器怪癖,即黑盒测试失败的地方(或多或少,它更像是黑盒生成的有效负载可能失败的地方)。

我们将使用的代码稍微复杂一些:

#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 请求来取笑 HT​​TP。而且我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 的回答值得赏金,因为他回答了原始问题的更多部分。