目录
一、栈
1.栈的简介
栈主要是用于存储程序运行过程中的局部信息,大小不一,动态增长。栈的内存一般根据函数栈来进行划分(不用函数的程序很少见),不同的函数栈之间是互相隔离的,从而能实现函数的有效切换。函数栈上存储的信息一般包括:临时变量(包括栈保护哨carry)、函数栈的返回栈基址(bp)、函数的返回地址(ip)。
程序栈
2.函数栈的调用机制
程序运行时,为了实现函数之间的互相隔离,需要在进入新函数之前保存当前函数的状态,而这些状态信息全在栈上,为了实现状态的隔离,由此引出函数栈的概念,当前函数栈的边界就是栈顶指针(sp)和栈底指针(bp)所知的区域所指的区域。sp主要指的是esp(x86)和rsp(x64),bp主要指ebp(x86)和rbp(x64)
在函数调用(即进入子函数时)时,首先将参数入栈,然后压入返回地址和栈底指针寄存器bp(也有不压bp的情况),其中压入返回地址是通过call实现的。
在函数结束时,将sp重新指向bp的位置,并弹出bp(与前面是否压入bp保持一致)和返回地址ip,通常,弹出bp是通过leave或者pop bp(pop rbp或者pop ebp)来实现。
x86程序参数传递实例
x64程序参数传递实例
修改bp寄存器,然后执行ret,函数状态将恢复成进入子函数时的状态,实现了函数栈的切换。
在函数栈中,bp中存储上个函数栈的基址,而ip存储的是调用处的下一条指令位置,返回当前函数,会从栈上弹出这两个值,从而恢复上一个函数的信息。
3.函数参数传递
由于函数的传参规则受函数调用协议的影响,首先介绍函数调用的协议,_stdcall、_cdecl、_fastcall是三种函数调用,函数调用协议会影响函数参数的入栈方式、栈平衡的修复方式、编译器函数名的修饰规则。
(1)调用协议的常用场合
_stcall:windows API默认的函数调用协议
_cdecl:C/C++默认的函数调用协议
_fastcall:适用于对性能要求较高的场景
(2)函数参数的入栈方式
_stcall:函数参数从左向右入栈
_cdecl:函数参数从右向左入栈
_fastcall:从左开始将小于4字节的参数放入CPU的ecx和edx寄存器,其余参数从右往左入栈
(3)栈平衡修复方式
_stcall:函数调用结束后由被调用函数来平衡栈
_cdecl:函数调用结束后函数调用者来平衡栈
_fastcall:函数调用结束后由被调用函数来平衡栈
对于linux来说,通常采用_cdecl的调用方式
(4)对于x86程序
普通函数传参
参数基本压在栈上(有寄存器传参的情况)
syscall传参
eax对应系统调用号,ebx,ecx,edx,esi,edi,ebp分别对应前6个参数。多余的参数压在栈上。
(5)对于x64程序
普通函数传参
先使用rdi,rsi,rdx,rex,r8,r9寄存器作为函数参数的前六个参数,多余的参数会依次压在站上。
syscall传参
rax对应系统调用号,传参规则与普通函数传参一致。
(6)对于ARM程序
R0,R1,R2,R3,依次对应前四个参数,多余的参数会依次压在栈上。
普通函数传参如下:
x86汇编代码如下
x64汇编代码如下:
二、栈溢出
1.介绍
栈溢出指的是缓冲区被填入了过多的数据,超出了边界,从而导致栈上原有的数据被覆盖。栈溢出是缓冲区溢出的一种类型。
由前面的可知,局部变量、bp、ip是比较重要的部分。主要作用如下:
(1)局部变量
局部变量在函数中的作用很大,如构造危险输入,影响条件分支的转移等,这些都能起到改变控制流或者方便构造更强大漏洞的作用。
(2)bp
函数栈栈底指针,会直接影响到返回函数的栈,如果恢复bp的代码是“leave”或者后续代码存在“mov sp,bp”,则会间接影响控制流,同时,有些参数以及临时变量在代码中很有可能是根据bp来索引的,因此也能影响局部变量或者参数的使用,从而影响控制流。
(3)ip
程序返回地址,能直接影响控制流,如ip指向危险函数,直接调用rop等。
2.覆盖栈缓冲区的具体作用
(1)数据不可执行(NX/DEP)
数据不可执行(NX/DEP)主要是防止直接在缓冲区(堆、栈、数据段)存放可执行的代码,增加漏洞利用难度,一般情况下,首先会检查程序的保护机制,尤其需要关注NX是否开启,检查命令为“checksec./proc”。关闭NX的编译选项为“-z execstack",默认是开启的。
示例代码如下
编译命令如下:
开启NX:gcc -o proc_nx proc.c;proc_nx程序执行后直接异常报错退出。
关闭NX:gcc -o proc proc.c-z execstack;proc程序执行后直接获取shell
(2)栈保护哨(canary)
主要是存放在函数栈靠近肺部位置的一个临时变量中,防止栈缓冲区覆盖存放在栈底的栈底寄存器(bp)和返回地址(ip)。
canary是程序在每次进入函数前会被赋予的值,在函数返回检查该值是发生了改变,若检测出改变则报异常并退出,这样即便发生了缓冲区溢出,由于canary的存在,也难覆盖bp,ip,从而可防止直接利用这两个值进行控制流劫持,减轻了对栈缓冲区的危害,虽然canary在程序中每次运行时都是随机的,但是程序的一次运行来说。大部分情况下,不同函数中的canary值时固定的。
关闭canary保护的编译选项为“-fno-stack-protector”,canary保护功能默认是开启的:
示例代码如下:
开启canary
gcc -o proc_canary sample_canary.c
关闭canary
gcc -o proc sample.c -fno-stack-protector
proc_canary的反汇编结果
带有canary的程序反汇编结果
不带canary的程序反汇编结果
(3)劫持控制流
函数栈底部存放的返回地址是返回到父函数调用处的下一个位置,如果缓冲区覆盖了返回地址,那么函数结束后,将会跳转到所修改的低智商去,从而劫持控制流,如图:
实例代码如下:
编译命令:
gcc -o overwirte_ret overwirte_ret .c -fno-stack-protecrot
反汇编代码
可以看到tardet_func的地址为0x4005C6,栈buff大小为0x10的大小,运行到gets函数调用处,内存状态如下:
输入命令ni单步跳过后,然后输入AAAAAAAABBBBBBBBCCCCCCCCDDDDDDDD,其中AAAAAAAABBBBBBBB占据了buff空间,CCCCCCCCDDDDDDDD则用于填充rbp和rip的位置,内存状态如下:
单步执行si,运行到ret指令,可以看到返回地址DDDDDDDD,即0x4444444444444444而rbp为CCCCCC,即0x4343434343434343,如图:
将输入中的DDDDDDD用target_func的地址0x4005C6来代替,就能执行到target_func,图示:
由于能够劫持控制流,在这种情况下,较为复杂的情况就是rot代码,可以根据前面的函数栈的调用方式来理解rop代码
对于x86程序来说,参数传递是通过栈来实现的,调用完之后,需要清除栈中的参数,所以一般函数调用完之后需要用形如:pop * ; pop *;.........;ret;的gadget来调整栈,因为函数调用时返回地址会压入栈中,即汇编的call func指令等同于push ret_addr,jmp func,所以执行jmp func的时候,ret_addr 已经压到栈里面去了,进入函数时栈的状态,如图:
通过ret指令将返回到sp所指向的地址,弹出该地址,并调整sp,因此如果当前指令是ret,那么此时的栈状态如图:
这也是rop的构造原理,将ret_addr改成pop*;ret指令的gadget,用来弹出后续的args,即成rop形式,如图:
对于x64程序来说,一般情况下,函数的参数较少,通常主要利用寄存器来传递参数的,所以在进入函数之前,应先将寄存器设置好,对于rop的理解也比较容易。
(3)覆盖栈中的临时变量
覆盖栈中的临时变量的情况比较简单,且常见,主要是被覆盖的临时变量很有可能在后续代码中起到很大的作用。
实例代码如下
编译命令如下:
gcc -o overview_var overview_var.c -fno-stack-protector
通过打印的地址可以知道,buff与info的地址相差0x1c,因此输入0x1d个字符就能够覆盖info值,从而改变info,并影响控制流。
(4)覆盖栈底寄存器bp
使得与bp相关的信息发生改变,主要针对的是汇编代码,栈底寄存器bp的主要作用是确定函数栈的栈底是否发生改变,一旦发生改变,函数中所引用的信息都会发生变化。
(5)敏感函数
通常发生栈覆盖时,可以关注能够产生缓冲区溢出的函数、循环赋值逻辑等,但此时需要留意其能够读取与覆盖的范围大小,看是否超过申请的值,包括:
常见的覆盖危险函数形式gets(buff)、scanf("%s",buff)等;
潜在的覆盖函数有read,strcpy,memcpy等
三、栈的特殊利用
1.libc信息泄露
main函数的栈底存放的是_libc_start_main_ret,因此一旦能使其返回地址泄露,就可以利用libc.so文件或者使用libc_database来计算的基址以及system地址等
2.多级指针:path指针
多用于格式化字符串,由于函数栈的下面存放有环境变量、argv等指针,该指针通常可以用来泄露信息。
3.环境变量修改
环境变量指针参数会压在栈上,修改可以达到特定目的,在进入程序主逻辑之前,环境变量已经压入了栈底,而这些环境变量可能会影响程序的行为。
4.通过libc泄露栈地址
栈地址通常都比较关键,很多时候都需要泄露出该地址,如果出现知道libc地址但不知道栈地址的情况,可以根据libc中的Environ偏移来计算
5.栈上写rop的技巧
对于rop布置的不好的情况,可以选择离当前函数栈比较远的地方进行写入,然后通过返回达到rop位置,如果可以在main函数的栈底布置rop,触发main函数返回条件,就会执行rop处。