二进制安全之——栈相关漏洞

目录

一、栈

1.栈的简介

2.函数栈的调用机制

3.函数参数传递

(1)调用协议的常用场合

(2)函数参数的入栈方式

(3)栈平衡修复方式

(4)对于x86程序

普通函数传参

syscall传参

(5)对于x64程序

普通函数传参

syscall传参

(6)对于ARM程序

二、栈溢出

1.介绍

(1)局部变量

(2)bp

(3)ip

2.覆盖栈缓冲区的具体作用

(1)数据不可执行(NX/DEP)

(2)栈保护哨(canary)

(3)劫持控制流

(3)覆盖栈中的临时变量

(4)覆盖栈底寄存器bp

(5)敏感函数

、栈的特殊利用

1.libc信息泄露

2.多级指针:path指针

3.环境变量修改

4.通过libc泄露栈地址

5.栈上写rop的技巧


一、栈

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处。


版权声明:本文为qq_60503432原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。