【问题标题】:How does system() affect the stack in x64 linux?system() 如何影响 x64 linux 中的堆栈?
【发布时间】:2020-08-06 01:58:36
【问题描述】:

我正在阅读 Jon Erickson 的优秀著作“黑客:剥削的艺术”,并试图理解他对缓冲区溢出的阐述。这本书似乎有点过时了。在他的示例中,他运行的是 x86 linux,而我在 x64 上复制结果时遇到了麻烦(我确实知道近年来增加了更大的堆栈保护)。特别是我正在努力复制他的exploit_notesearch.c 程序。

在本书的开头,他演示了一个程序 notesearch.c,该程序运行 suid root 并在库包含和函数声明之后具有以下初始行:

int main(int argc, char *argv[]) {
    int userid, printing=1, fd;
    char searchstring[100];

    if(argc>1)
        strcpy(searchstring, argv[1]);
    else
        searchstring[0]=0;
...

现在,Erickson 稍后演示了该程序的一个漏洞利用程序,称为exploit_notesearch.c,其骨架是:

char shellcode[]=
"\x31\xc0\x31\xdb\x31\xc9\x99\xb0\xa4\xcd\x80\x6a"
"\x0b\x58\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69"
"\x6e\x89\xe3\x51\x89\xe2\x53\x89\xe1\xcd\x80";

int main(int argc, char *argv[]) {
    unsigned int i, *ptr, ret, offset=170;
    char *command, *buffer;
...
    if(argc>1)
        offset=atoi(argv[1]);
    ret=(unsigned int) &i-offset;
...
    system(command);
    free(command);
}

我省略的区域只是将正确的数据复制到command,首先写入"./notesearch '",然后注入60个NOP字节,然后注入shellcode中保存的数据,然后填充分配的其余内存地址为ret,字符串以' 结尾。

据我了解,exploit的思路应该如下。在执行system(command) 行时,系统会将notesearchmain 函数的新堆栈帧推入堆栈。这个堆栈帧的底部是EIP 在完成主函数后应该返回的地址,中间的某个地方是为searchstring 缓冲区分配的空间。 ret 旨在近似分配给searchstring 的空间的开始,我们用NOP 指令(作为一个软糖因素)、shellcode(在执行时打开一个根shell)和几十个副本覆盖它地址ret 以确保我们将覆盖EIP 的返回地址。系统正常执行main,但随后,不是返回到exploit_notesearch代码中的地址,而是返回到地址ret,并继续执行所需的shellcode。定义ret 背后的想法是,i 位于堆栈帧正上方的某个堆栈帧中,用于notesearch.cmain 函数,因此searchstring 不应该离i 太远,并且因此,通过尝试不同的偏移量,我们应该能够找到一个有效的偏移量。 (NOP 雪橇意味着我们不必完全精确。)

我想我大部分都正确理解了这一点,但也有一些问题。主要的一点是,我对system() 工作原理的理解是基于猜测,因为 Erickson 没有详细说明它是如何工作的。为了了解发生了什么,我尝试重写此程序以与 x64 linux 兼容,并进行了以下更改:

  • 用为 x64 编写的 shellcode 替换 Erickson 给出的 shellcode
  • iretoffset 更改为无符号长整数并将for 循环中i 的增量更改为8,以考虑更大的内存地址
  • 使用-fno-stack-protector 标志编译notesearch.cexploit_notesearch.c

然而,这根本不起作用,所以为了调试,我在notesearch.c 添加了一行打印地址searchstring 和一行到exploit_notesearch.c 打印地址ret。运行./exploit_notesearch 几次后,我得到了奇怪的结果:

trial 1:
ret:          0x7ffdc21f25ce
searchstring: 0x7ffee3c209a0

trial 2:
ret:          0x7fff6115703e
searchstring: 0x7ffd1233afb0

trial 3:
ret:          0x7ffeab00781e
searchstring: 0x7fff3c8a8760

那么,这里发生了什么?似乎调用system() 会以非常不可预测的方式更改堆栈,有时将新堆栈帧放在旧堆栈帧的下方,有时将其放在上面。使用gdb 进行调试没有帮助,因为似乎system() 的整个调用被捆绑到一行call 0x555555554710 <system@plt> 中,这并没有提供任何洞察力。

所以,我的主要问题是:

  • system() 调用的shell 命令如何与堆栈交互?
  • 这在 x64 linux 中与在 x86 中的做法是否不同,还是我真的误解了 Erickson 编写的代码?
  • 有没有办法在编译时禁用 x64 linux 上的这些安全措施,以便我在学习时可以跟随 Erickson 的代码?

对冗长的问题表示歉意,并提前致谢。


编辑:根据下面 Jester 的建议,我已禁用 ASLR,现在程序可以正常运行。那么作为一个后续问题,是否有人对理解 ASLR 有任何参考?干杯!

【问题讨论】:

  • retsearchstring 处于不同的进程中。它们的地址没有任何关系,尤其是在堆栈被随机化的情况下。您可以尝试关闭 ASLR,但即使这样也可能会使堆栈随机化。
  • system() 只是一个普通函数,它使用堆栈的方式与调用任何其他函数的方式相同。
  • @Jester 非常感谢;两个问题。首先,您有了解流程和 ASLR 的参考资料吗?我以前没有听说过这些条款。其次,这些保护措施是在 2008 年左右吗?如果不是,埃里克森的代码应该如何工作?再次感谢!
  • @Barmar 抱歉,澄清一下——我不是指system() 的堆栈帧在哪里,而是使用system() 调用的shell 命令的堆栈帧在哪里
  • 对不起 - 由于主题的性质和安全性的极端相关性,这些天的漏洞利用(我希望!)通常在任何信息公开之前就已修复。您可能不得不徘徊在互联网的阴暗角落,才有机会真正尝试利用漏洞。不,我不知道任何更暗的角落:-o

标签: c linux assembly stack-overflow buffer-overflow


【解决方案1】:

在执行system(command)这一行时,系统会将notesearch的主函数的新堆栈帧压入堆栈

没有。这是完全错误的。 system(xxx)execve 系统调用的便捷库包装器,它首先执行 fork 以作为子进程运行:

system("xxx");

// Roughly equivalent to:

int wstatus;
pid_t child = fork();

if (child == -1) {
    return -1;
} else if (child == 0) {
    execve("/bin/sh", ["/bin/sh", "-c", "xxx"], envp); // execute shell in child
} else {
    waitpid(child, &wstatus, 0); // wait for child to complete in parent
    return WEXITSTATUS(wstatus);
}

它启动一个新的 shell 来执行你作为参数传递的程序(或命令[s])。当您这样做时,fork 创建一个与父级相等的新子级,然后,在子级中,程序从操作系统中擦除,并替换为新的一位来自execve。创建了一个新堆栈,并启动了新程序。

system() 如何与堆栈交互?

它不会以任何特定的方式进行交互,它只是一个普通的库函数,就像我上面所说的那样。当execve 系统调用被执行时,这个进程的分叉克隆被内核替换为一个新初始化的进程,它拥有自己的虚拟地址空间(单独为它做ASLR)。然后它 waits 让那个 shell 进程退出。这对调用system()的进程的地址空间没有任何影响。

这在 x64 linux 中与在 x86 中的做法是否不同,还是我真的误解了 Erickson 编写的代码?

您肯定误解了代码。 system() 所做的只是简单地使用精心设计的argv[1] 运行易受攻击的程序,以导致缓冲区溢出并覆盖main() 函数的返回地址,从而导致RIP 覆盖并控制执行。

似乎调用system() 会以非常不可预测的方式更改堆栈,有时将新堆栈帧放在旧堆栈帧的下方,有时将其放在上方。

当然,因为system() 只是用execve 创建一个新进程。

有没有办法在编译时禁用 x64 linux 上的这些安全措施,以便我可以在学习时跟随 Erickson 的代码?

是的,您可以禁用 ASLR 以阻止内核随机化堆栈的位置:

sudo sysctl -w kernel.randomize_va_space=0

gdb 应该已经为您执行此操作,但前提是该进程是从 gdb 内部启动的。

【讨论】:

  • 其实systemfork+exec 的包装,所以原来的进程不会被抹去:)
  • 严格来说不是glibc。任何 POSIX 系统 will have it as fork/exec.
  • @S.S.Anne 是的,我太习惯于到处假设glibc。谢谢。
  • 换句话说,exploit_notesearch 并没有做任何特别偷偷摸摸的事情;整个hack是在命令行上给notesearch的字节序列。理论上,您可以通过粘贴正确的字符序列直接从 shell 运行 hack,其中许多字符是非 ASCII。那将是一个巨大的痛苦,并且难以复查;因此需要额外的程序来为你做这件事。
  • @AtticusStonestrom fork 后孩子与父母相同。即使启用了 ASLR。 execve() 改变了执行新进程的所有内容。一般来说,使用system() 更简单,并且允许检查子进程的返回值,而execve()(如果成功)只会替换正在运行的进程并且永远不会返回。
猜你喜欢
  • 1970-01-01
  • 2018-09-23
  • 2018-05-23
  • 2021-05-02
  • 1970-01-01
  • 2021-03-29
  • 2015-01-05
  • 1970-01-01
  • 2011-09-16
相关资源
最近更新 更多