在GCC中禁用堆栈保护无效

3

我正在尝试使用strcpy的经典溢出重现堆栈缓冲区溢出,使用以下函数:

#include <stdio.h>
#include <string.h>
void main(int argc, char **argv) {
    char buf[100];
    strcpy(buf,argv[1]);
    printf("Done!\n");
}

我尝试使用所有不同的标志编译代码,以便去除堆栈保护 gcc -o vuln vuln.c -fno-stack-protector -g -z execstack,并使用 sudo echo 0 > /proc/sys/kernel/randomize_va_space 去除ASLR。

我可以让我的nop-shellcode地址覆盖已保存的EIP,但在RET处崩溃。我发现可以通过gdb禁用SIGSEGV等来解决,但是在GDB外运行时,无论我将返回地址设置在什么地方,都会继续收到Segmentation Fault错误。

除了获取旧版本的gcc之外,还有什么想法吗?目前使用的是gcc版本6.1.1。

2个回答

4

Linux遵循W^X原则:除了代码段,它将内存页面标记为不可执行。这超出了您编译的应用程序的范围,而且有充分的理由。操作系统承担此责任以防止来自系统上执行的任何程序的缓冲区溢出攻击;特别是那些积极尝试执行缓冲区溢出攻击的程序,比如您的程序。

您正在尝试通过buf在堆栈上编写代码,并覆盖函数的返回地址以跳转执行到新注入的代码。当函数返回时,程序计数器被设置为被覆盖的返回地址,它现在指向堆栈内存。由于堆栈内存页面的执行权限被撤销,当程序计数器尝试执行下一条指令时,会抛出SIGSEGV

要从堆栈中执行,必须禁用操作系统的堆栈保护。

幸运的是,Linux提供了execstack,供用户自行决定使用。

查看this Unix & Linux Stack Exchange 帖子以获取更一般的信息。


调试注入数据

如果您在gdb中收到SIGSEGV错误,那么很可能是您的缓冲区溢出尝试中出现了一些错误。为了避免干扰main结束时的清理工作,建议创建一个函数,在主函数中调用该函数来执行缓冲区溢出操作:

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

char injection_code[] = ""; // buffer overrun data here


void foo () {
    char buf[100];

    // memcpy used to copy the full injection string, including any nested 0s
    memcpy(buf, injection_code, 108); // +8 here to handle 64-bit system RAs
}

int main (int argc, char** argv) {
    foo();
    printf("Done!\n");
    return 0;
}

使用GDB进行调试。建议在foo的结尾处设置断点,并使用info registers检查寄存器是否与预期相符。您可能最感兴趣的是指令指针,您可以使用info registers rip(64位)或info registers eip(32位)来检查它。
您可以使用print或GDB中的x/x探索堆栈的情况。例如,您可以检查$rbp+8以查看堆栈上的返回地址(RA)。
如果RA指向无效位置,则GDB将在ret指令上发生SIGSEGV异常:
Program received signal SIGSEGV, Segmentation fault.
0x00000000004005bc in foo () at bufferoverflow.c:27

你可以通过检查故障时指令指针地址(在上面的示例中为0x00000000004005bc)与程序的汇编代码(在GDB中使用disassemble function_name)来检查是否在ret指令上出现故障。
如果返回地址指向堆栈,可能会抛出SIGILL表示非法指令,这意味着你的返回地址可能没有正确对齐,或者你注入的指令存在问题。
Program received signal SIGILL, Illegal instruction.
0x00007fffffffdc13 in ?? ()

再次,您可以使用GDB来探索您的堆栈以查看原因。

GDB对于正确获取缓冲区溢出的结构非常有用。但是一旦它按预期工作,记住当在没有调试标志(-g)的情况下编译时,内存地址可能会发生偏移,特别是在执行时超出GDB环境。您将不得不稍微调整返回地址,以使其在“实时”环境中达到所需位置。


一个旁白

一般来说,直接运行注入代码的缓冲区溢出攻击在今天的堆栈保护机制下通常是不可行的。通常需要存在其他漏洞才能执行任意代码。


2
当然,这样做的目的是为了挫败OP所设想的攻击方式。这种功能的广泛可用性使得这种攻击比以前不那么可行了。 - John Bollinger
1
我使用了gcc -z execstacksudo execstack -s vuln两种方式来使用execstack,并通过readelf -l vuln验证了GNU栈被设置为RWE(读、写、执行)。但是我仍然遇到了类似的错误。您认为我做错了什么?我注意到我需要溢出缓冲区的数量不是108字节,而是116字节。 - nrabbit
@nrabbit 我现在没有时间做这件事,但我会在我的机器上进行实验,并回复你。 - Stewart Smith
@nrabbit 看起来我的机器可以正常工作。我添加了有关调试缓冲区溢出尝试的详细信息。 - Stewart Smith
谢谢@sdsmith,我会在GDB中再试一下,看看我的shellcode是否出了问题。 - nrabbit
1
我终于找到了问题所在。GCC将我的指令和变量对齐到16字节边界。因此,在序言中它会添加汇编指令,例如lea ecx,[esp+0x4]等等。在函数的结尾处也会有类似的操作,这些操作会使堆栈指针发生一些偏移,并且会影响堆栈的布局。 - nrabbit

1
我终于找到了问题所在。GCC将我的指令和变量对齐到16字节边界。因此,在序言和类似函数的尾声中,它会添加汇编指令,例如lea ecx,[esp + 0x4]等,这会使堆栈指针有些偏移,并且会干扰堆栈的布局。我重新使用-mpreferred-stack-boundary = 2进行了编译,在GDB中解决了这个问题。现在我只需要像你提到的那样处理返回地址。谢谢!

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接