栈溢出-Stack smash
栈溢出-Stack smash
原理
Stack-smash简单来说就是绕过Canary保护机制的技术。当程序开启了Canary保护后,会在缓冲区中加入一个Canary值,如果这个Canary值在程序运行中被改变了,会触发__stack_chk_fail函数。这个技术的关键就在于
这个函数会打印信息,我们可以通过这个函数打印我们想要的信息。打印的是argv[0]指针所指向的字符串。正常情况下指向程序名。
__stack_chk_fail函数介绍
1 | void __attribute__ ((noreturn)) __stack_chk_fail (void) |
所以我们想打印我们想要的内容,就需要把argv[0]处的内容覆盖我们想输出的字符串地址。那么就会通过
__fortify_fail函数打印出这个字符串。
示例
基本思路
检查程序保护机制
开启了NX保护和Canary保护,以及FORTIFY保护,再到ida中静态分析一下
程序两次输出,因为gets函数对输入不做限制,所以这两处输入都是溢出点。第二次输入是先输入到stdin,然后最后保存在byte_600D20。查看一下这个byte_600D20
提示说flag在服务器端,所以这道题并不是让我们拿getshell,而是通过栈溢出打印出远程服务器上真正的flag。
再回到程序中,观察到还有一个memset函数
1 | memset((void *)((signed int)v0 + 0x600D20LL), 0, (unsigned int)(32 - v0)); |
memset函数
1 | void* memset (void *_Dst, int _Val, size_t _Size) |
所以这个函数的意思就是从v0+0x600D200LL这个地址往后32-v1字节的内容都以0替代。
那么不管我们输入的是什么,flag的内容都会被覆盖掉(byte_600D20),所以此时就需要利用到”ELF重映射”的特点:
1 | 在ELF内存映射时,bss段会被映射两次,所以我们可以使用另一处的地址进行输出 |
第一次可能有点不理解,gdb调试看一下内存映射
注意开头处,可以看到pwn(这个程序)被映射到两处地址中,所以只要在二进制文件(offset)0x00000000~0x00001000范围内的内容都会被映射到到内存中,并且分别以0x600000和0x400000作为起始地址。flag在0x00000d20(0x600d20-0x600000),所以在0x400d20处也被映射了flag(相当于备份)
既然知道了在0x400d20也是flag,那么我们就可以利用__stack_chk_fail函数将其打印出来。
此时的思路就是:
- 寻找argv[0]指针的位置,将其指向的内容覆盖为0x400d20
- 寻找输入时栈顶的位置
- 构造溢出触发__stack_chk_fail
寻找argv[0]指针位置
argv[0]有一个明显特征,就是会指向程序名,所以可以在在main函数处下断点来寻找
0x7fffffffe008就是argv[0]的地址
也可以在gdb中使用命令
1 | p & __libc_argv[0] |
寻找栈顶位置
为什么这一步要找输入栈顶的位置呢?到gdb调试下就知道了。
首先先看gets函数调用的位置,在IDA中查看:
从汇编中可以看出在call gets之前,程序将参数放在了rdi中,由于有mov rdi,rsp存在,因此gets的参数一开始是放在栈里的。在gets(0x40080E)下断点,查看栈内容。
可以看到当前的rdi寄存器中的值为rsp寄存器的内容,因为在64位程序中rdi寄存器中存放的是当前执行函数的一参,所以当前的栈顶就是gets函数的一参。所以当前栈顶的位置到刚才的argv[0]的偏移距离就是我们的栈溢出长度,所以我们通过计算偏移=0x218(我自己gdb调的时候不是这个,不知道为什么,只能用0x218算才能打通),也就是输入内容要在0x218以后才能把argv[0]给覆盖掉,并且输入0x218内容之后把0x00400d20(flag)地址写上就可以。
EXP:
1 | from pwn import* |