栈溢出与基本ROP

栈溢出原理

由于C语言对数组引用不做任何边界检查从而导致缓冲区溢出(buffer overflow)成为一种很常见的漏洞。根据溢出发生的内存位置通常可以分为栈溢出和堆溢出°其中’由于栈上保存着局部变量和一些状态信息(寄存器值、返回地址等),一旦发生严重的溢出,攻击者就可以通过覆写返回地址来执行任意代码利用方法包括shellcode注入、ret2libc、ROP等。同时防守方也发展出多种利用缓解机制。

函数调用栈

下面直接举例说明(分为x86和x86-64)

image-20211110210843467

image-20211110210910349

x86

image-20211110211135210

首先被调用函数func()的8个参数从后问前依次入栈,当执行call指令时,下一条指令的地址0x08048415作为返回地址入栈。然后程序跳转到func(),在函数开头,将调用函数的ebp压栈保存并更新为当前的栈顶地址esp,作为新的栈基址,而esp则下移为局部变量开辟空间。函数返回时则相反,通过leave指令将esp恢复为当前的ebp,并从栈中将调用者的ebp弹出,最后ret指令弹出返回地址作为eip,程序回到main()函数中,最后抬高esp清理被调用者的参数,一次函数调用的过程就结束了。

x86-64

image-20211110211746152

对于x86-64的程序,前6个参数分别通过rdirsirdxrcxr8r9进行传递,剩余参数才像x86一样从后向前依次压栈。除此之外,我们还发现func()没有下移rsp开辟栈空间的操作,导致rbprsp的值是相同的,其实这是一项编译优化:根据AMD64 ABI文档的描述rsp以下128字节的区域被称为red zone,这是一块被保留的内存,不会被信号或者中断所修改。于是func()作为叶子函数就可以在不调整栈指针的情况下,使用这块内存保存临时数据。

危险函数

第一类危险函数——scanfgets

1
2
3
4
5
6
7
char buf[10];

scanf("%s", buf); // 没有限制读取长度,存在栈溢出
scanf("%10s", buf); // 限制读取长度为10,但由于scanf函数会在字符串末尾自动添加"\0",如果输入长度正好为10,就会溢出
scanf("%9s", buf); // 限制长度为9,这是安全的

// gets函数不限制读取长度,很容易构成溢出漏洞

第二类危险函数——strcpystrcatsprintf

1
2
3
4
5
6
7
int len;
char srcbuf[20];
char destbuf[10];

len = read(0, srcbuf, 19); // 这里限制了可读入长度,是安全的
src[len] = 0;
strcpy(destbuf, srcbuf); // 这里将srcbuf拷贝到destbuf,这时候可能造成溢出

可以用来代替上面的的安全函数:strncpystrcatsnprintf,因为这些函数都有一个size参数用来限制长度

ret2text

ret2shellcode

ret2shellcode,即控制程序执行 shellcode 代码。shellcode 指的是用于完成某个功能的汇编代码,常见的功能主要是获取目标系统的 shell。一般来说,shellcode 需要我们自己填充。这其实是另外一种典型的利用方法,即此时我们需要自己去填充一些可执行的代码

在栈溢出的基础上,要想执行 shellcode,需要对应的 binary 在运行时,shellcode 所在的区域具有可执行权限。

举例:https://lht.wiki/20211026_ctf_ret2shellcode/

ret2libc

参考

写在最后

一些东西写得很简略,比如ret2xxx部分,虽然是学习笔记而已,大致理解就行,但终究还是因为水平不够才写不出来,希望以后能补充更新