¶前言
国庆假期打算学学pwn,文章内容主要是跟着 ctfwiki 一路学习下来。
¶栈的介绍
什么是栈,相信修过数据结构的同学们应该都了解清楚了。栈是一种先进后出的数据结构,其操作主要有入栈(push)和出栈(pop)两种操作,如下图所示
在高级语言运行时会转换为汇编语言,而汇编语言运行的过程中,使用栈的数据结构,用于保存函数的调用信息和局部变量。在学习栈时需要注意 程序的栈是从进程地址空间的高地址向低地址增长
¶栈溢出原理
栈溢出指的是向栈中某个变量写入的字节数超过了这个变量本身所申请的字节数,因此导致了与其相邻的栈中的变量的值被改变。栈溢出漏洞轻则导致程序的崩溃,重则导致攻击者可以控制程序的执行流程。理解概念后,就可以梳理清楚栈溢出漏洞的前提
- 程序必须向栈上写入数据。
- 写入的数据大小没有被良好地控制。
最经典的栈溢出利用是覆盖程序的返回地址为攻击者所控制的地址(当然这个控制地址必须有可执行的权限),我们以一个例子来呈现
1 |
#include <stdio.h> |
首先将该文件进行编译
1 |
gcc -m32 -fno-stack-protector -no-pie stack1.c -o stack1 |
gcc编译指令中,-m32
表示生成32位程序;-fno-stack-protector
指的是不开始堆栈溢出保护,即不生成canary;-no-pie
表示关闭PIE,避免基址被打乱,生成后用 checksec 检查编译出的程序PIE是否关闭
从图中可以看出,在编译程序的时候就提醒了我们gets
函数是一个危险函数。因为它不检查输入的字符串长度,而是以回车来判断结束,因此容易导致栈溢出漏洞的产生。
接着上面说的 PIE 保护,实质上,linux系统存在地址空间分布随机化(ASLR)机制,简单说就是即使可执行文件开启了 PIE 保护,还需要系统来开启 ASLR 才会真正打乱基址,否则依旧会加载在一个固定的基址上
我们可以通过cat /proc/sys/kernel/randomize_va_space
来查看ASLR启动与否
- 0,关闭 ASLR,没有随机化。栈、堆、.so 的基地址每次都相同。
- 1,普通的 ASLR。栈基地址、mmap 基地址、.so 加载基地址都将被随机化,但是堆基地址没有随机化。
- 2,增强的 ASLR,在 1 的基础上,增加了堆基地址随机化。
我们通过echo 0 > /proc/sys/kernel/randomize_va_space
来关闭ASLR
接下来,我们将生成的可执行文件导入到 ida 进行查看,在main函数中会进入到 vulnerable() 函数
双击char s
,可以看到栈空间大小为0x14
,到函数的返回值为0x4-(-0x14)=0x18
,也就是只需要18个字节,我们就可以覆盖返回地址的内容。
我们知道函数的调用过程是,当子函数执行完毕,就会把主函数下一个命令的地址出栈并赋值给 EIP 寄存器,随后 EIP 寄存器就会去该地址执行。而我们在ida中可以看到含有 flag 的 success 函数地址为0x08048456
,所以我们exp如下
1 |
from pwn import * |
成功执行 success 函数获取到flag
¶基本ROP
随着 NX 保护的开启,以往直接向栈或者堆上直接注入代码的方式难以继续发挥效果。攻击者们也提出来相应的方法来绕过保护,目前主要的是 ROP(Return Oriented Programming),其主要思想是在 栈缓冲区溢出的基础上,利用程序中已有的小片段 (gadgets) 来改变某些寄存器或者变量的值,从而控制程序的执行流程。 所谓 gadgets 就是以 ret 结尾的指令序列,通过这些指令序列,我们可以修改某些地址的内容,方便控制程序的执行流程。
常用的汇编指令如下
1 |
MOV %src %dest :把数据从源地放到目的地 |
ROP,其核心在于利用了指令集中的ret指令,改变了指令流的执行顺序。ROP攻击一般需要满足如下条件
- 程序存在溢出,并且可以控制返回地址。
- 可以找到满足条件的 gadgets 以及相应 gadgets 的地址。
但是如果 gadgets 每次的地址是不固定的,那我们就需要想办法动态获取对应的地址了
¶ret2text
ret2text是一种控制程序执行程序本身已有的的代码的攻击方式,我们控制执行程序已有的代码的时候也可以控制程序执行好几段不相邻的程序已有的代码 (也就是 gadgets),这就是我们所要说的 ROP
点击下载文件:ret2text
首先使用checksec
检查文件,发现是32位程序,且只开启了NX保护(shellcode不可执行)
拖进 ida 中观察,main 函数使用了gets
1 |
int __cdecl main(int argc, const char **argv, const char **envp) |
并且还有一个 secure() 函数,其中含有/bin/sh
其地址为0x0804863A
,接着就是计算 s 相对于返回地址的偏移,首先打开 gdb 运行文件
接着另起一个窗口使用 cyclic 工具生成字符,比如这里生成150的字符
然后放到gdb中回车
可以看到报错的地址为0x62616164
然后用 cyclic 计算偏移大小为112
所以exp如下
1 |
from pwn import * |
¶ret2shellcode
ret2shellcode,即控制程序执行 shellcode 代码。shellcode 指的是用于完成某个功能的汇编代码,常见的功能主要是获取目标系统的 shell。一般来说,shellcode 需要我们自己填充。这其实是另外一种典型的利用方法,即此时我们需要自己去填充一些可执行的代码。
在栈溢出的基础上,如果要执行 shellcode,需要对应的 binary 在运行时,shellcode 所在的区域具有可执行权限
点击下载:ret2shellcode
首先使用 checksec 检查文件
是一个32位程序,并且几乎没有什么保护,拖进 ida 中按F5查看 mian 函数
1 |
int __cdecl main(int argc, const char **argv, const char **envp) |
使用了 gets,存在基本的栈溢出漏洞,此外还将字符串 s 复制到了 buf2,双击buf2
可以看到 buf2 在 bss 段上,地址为0x0804A080
,使用 gdb 调试查看该段是否有可执行的权限,依次执行命令
1 |
gdb ret2shellcode #使用gdb |
可以看到该段是具有可执行的权限,而0x0804A080
正好在此区间
1 |
0x0804a000 0x0804b000 rwxp /home/hacker/Desktop/gongfangword-pwn/ret2shellcode |
所以我们可以控制程序读取 shellcode 之后,执行 bss 段处的 shellcode,首先和上面 ret2text
一样的办法先计算出偏移
得到112,然后开始写exp,其中 shellcode 可以由 pwntools 生成
1 |
|
¶ret2syscall
ret2syscall,简单来说就是控制程序执行系统调用获取 shell,继续以题目为例
点击下载: ret2syscall
首先使用 checksec 检查文件
32位程序,并且开启了NX保护(即shellcode不可执行),ida 查看依然是一个典型栈溢出
1 |
int __cdecl main(int argc, const char **argv, const char **envp) |
由于我们不能像之前一样直接利用程序中的某段代码或者自己填写代码拿到shell,所以需要利用程序中的 gadgets 来获得 shell
首先我们需要了解一下 Linux 的系统调用知识点:
Linux的系统调用通过 int 80h 实现,并且用系统调用号来区分入口的函数
调用流程如下:
1、将系统调用编号存入 eax 寄存器中
2、将函数的参数存入其他通用的寄存器(ebx、ecx、edx…)
2、触发 0x80 号中断
比如我们通过系统调用执行:execve("/bin/sh", NULL, NULL)
,如图所示
首先计算栈空间偏移,用之前提到的办法计算
值求得为112,所以需要填充的空间大小为112
接着需要知道 execve
的系统调用号(32位)
1 |
cat /usr/include/asm/unistd_32.h | grep execve |
可以看到值为11,转换为16进制就是0xb
,接着我们使用 ROPgadget 工具开始寻找含有pop eax
的指令地址
1 |
ROPgadget --binary rop --only 'pop|ret' | grep 'eax' |
上述几个都可以控制 eax 寄存器,我们这里选取第二个,即地址为0x080bb196
接着寻找pop ebx
的指令地址
1 |
ROPgadget --binary rop --only 'pop|ret' | grep 'ebx' |
可以发现在找pop ebx
的过程中发现这个地址同时包含控制 ecx、edx 的操作,所以我们选取地址0x0806eb90
进行使用
接着寻找/bin/sh
的地址了
1 |
ROPgadget --binary rop --string '/bin/sh' |
地址为0x080be408
,最后只剩下int 80h
的地址了
1 |
ROPgadget --binary rop --only 'int' |
地址为0x08049421
,地址都找齐了,就可以开始写exp了
1 |
|
¶ret2libc
ret2libc 简单来说就是使我们的 ret 不跳转到 vuln 或者 shellcode 上,而是跳转到了某个函数的 plt 或者该函数对应的 got 表内容,从而控制函数去执行 libc 中的函数,一般情况下使用system("/bin/sh")
在 wiki 中,一共给了三道题,分别对应不同情况
1 |
1、有system 有/bin/sh |
¶有system 有/bin/sh
点击下载: ret2libc1
首先检查文件
32位程序,并且开启了 NX 保护,用 ida 查看
1 |
int __cdecl main(int argc, const char **argv, const char **envp) |
典型的栈溢出,看到有 system 函数
地址为0x08048460
,接着查找 /bin/sh 的地址
1 |
ROPgadget --binary ret2libc1 --string '/bin/sh' |
地址为0x08048720
,然后找偏移大小
大小为112,可以写exp了
1 |
|
¶有system 无/bin/sh
这次在ida中查看,发现没有 /bin/sh 字符串,但是 main 中有 gets
1 |
int __cdecl main(int argc, const char **argv, const char **envp) |
并且 plt 端中也有gets 函数
所以思路就是,如果 bss 段可写,我们就通过 gets 接收我们输入的/bin/sh
字符串,然后写入到 bss 段中,接着跳转到 system 函数中执行,整个流程图如图所示
我们先去看一下 bss 段地址
为0x0804A080
,我们用 gdb 查看一下是否具有可写的权限
可以看到该区间是具有可写的权限,栈空间由之前的办法求得为112,所以可以写exp了
1 |
|
¶无system 无/bin/sh
在前面的基础上,再次将 system 函数地址去掉,所以这次我们需要同时找到 system 和 /bin/sh
点击下载: ret2libc3
首先检查文件
依然开启了 NX 堆栈不可执行保护,放进 ida 中查看源码
1 |
int __cdecl main(int argc, const char **argv, const char **envp) |
还是典型的 gets 栈溢出漏洞,没有 system 和 /bin/sh ,我们可以使用 libc 中的 system 和 /bin/sh。如果我们知道了 libc 中的某一个函数地址就可以确定该程序使用的libc版本,从而知道其他函数的地址。
获得libc的某个函数地址通常使用 got 表泄露的方式(即输出某个函数对应的got表项内容),但是由于 libc 的延迟绑定,我们需要泄露的是已经执行过的函数地址
在 main 函数中我们可以看到执行了 puts,所以我们思路如下:
1 |
1.先通过一次栈溢出,将puts的plt地址放到返回处,通过代码中的puts(输出)功能泄露出执行过的函数(puts)的got地址 |
编写exp
1 |
|
上面说的都是32位程序,而在64位程序中函数的调用栈是不同的,这也是我们写 payload 时需要注意的地方
- 32位程序
- 函数参数 在 函数返回地址 的上方
- 64位程序
- 采用寄存器传参,所以我们需要覆盖寄存器
- 前六个参数按顺序存储在寄存器 rdi, rsi, rdx, rcx, r8, r9 中,参数超过六个时,从第七个开始压入栈中
- 内存地址不能大于 0x00007FFFFFFFFFFF, 6 个字节长度 ,否则会抛出异常。
¶结语
继续努力学习,后面遇见新的知识点会继续补充这篇文章
参考链接:
https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/stack-intro/
https://www.yuque.com/hxfqg9/bin/ug9gx5
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论