system() 如何影响 x64 linux 中的堆栈?

How does system() affect the stack in x64 linux?

我正在通读 Jon Erickson 的优秀著作 "Hacking: the Art of Exploitation" 并试图理解他对缓冲区溢出的阐述。这本书确实有点过时了。在他的示例中,他是 运行 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 字节,然后注入保存在 "./notesearch '" 中的数据shellcode,然后用地址 ret 填充剩余分配的内存,并以 '.

结束字符串

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

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

然而,这根本没有用,所以为了调试,我在 notesearch.c 中添加了一行打印地址 searchstring 和在 exploit_notesearch.c 中打印地址 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> 中,这没有提供任何见解。

所以,我的主要问题是:

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


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

Upon executing the line system(command), the system will push a new stack frame for the main function of notesearch onto the stack

没有。那是完全错误的。 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 替换为新的。新建栈,启动新程序

how does system() interact with the stack?

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

的进程的地址-space 有任何影响

is this done differently in x64 linux than it was in x86, or have I really misunderstood the code written by Erickson?

你肯定误解了代码。 system() 所做的只是 运行 将易受攻击的程序与精心制作的 argv[1] 连接起来,导致缓冲区溢出并覆盖 main() 的 return 地址导致 RIP 覆盖和控制执行的函数。

It seems like calling system() changes the stack in really unpredictable ways, sometimes putting the new stack frame below the old one and sometimes putting it above.

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

is there a way to disable these security measures on x64 linux when compiling, so that I can follow along with Erickson's code while I am learning?

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

sudo sysctl -w kernel.randomize_va_space=0

gdb 应该已经为您执行此操作,但前提是该过程是从 gdb.

内部启动的