Basic
首先给出两个常用shellcode仓库,可以检索需要的shellcode
常见指令及其编码
- push系列指令
1 | from pwn import * |
- pop系列指令
1 | from pwn import * |
- 运算指令
1 | from pwn import * |
- 移位指令
移位指令第二个操作数只能是立即数
1 | from pwn import * |
- 比较和条件指令
1 | from pwn import * |
nop(0x90)
- 空操作,通常用于填充或对齐
ret(0xC3)
- 从函数返回
incdec(0x40 - 0x4F)
- 自增/自减寄存器值。
leave(0xC9)
- 函数尾部用于释放栈帧的指令,等价于
mov rsp, rbp; pop rbp。
xchg(0x90 与寄存器编码)
- 交换
EAX与另一个寄存器的值,XCHG EAX, EAX实际上相当于NOP。
stosb/stosw/stosd(0xAA / 0xAB)
- 字节、字或双字存储,将
AL、AX或EAX的值存入RDI或EDI指针指向的位置。
cbw/cwde/cdqe(0x98)
- 进行符号扩展,将
AL扩展到AX,或AX扩展到EAX,或EAX扩展到RAX
- 获取fs和gs寄存器
1 | mov rax, fs:[rax] |
- 获取xmm寄存器的值
1 | movd eax, xmm0 ;将xmm0的低32位移到eax中。 |
lea(Load Effective Address)
1 | lea rax, [rdi+1] ; 计算 rdi 偏移 0x1 的地址 |
接下来给出几个尽可能短的shellcode
1 | ; excve('/bin/sh','sh',0) |
最短shellcode
特征与条件
长度为22字节
主要是通过cdq将rdx高位为0,减小了长度,另一种方法是通过mul r/m64指令,实现清空rax和rdx
- eax 高二位必须为0,一般是满足的
汇编
1 | xor rsi, rsi |
1 | 48 31 f6 xor rsi, rsi |
字节码
1 | // int |
orw
特征与条件
长度为0x28字节
主要是通过异或实现了取代了mov减少长度
- rsp指向的地址必须是可用的
- 存在NULL字符
汇编
1 | // rdx为写入数量 |
字节码
1 | 0x6800000200c2c748 |
可指定地址orw
1 | shellcode = """ |
侧信道爆破
1 | code = asm( |
字符限制
编码工具
| ae64 | alpha3 | |
|---|---|---|
| Encode x32 alphanumeric shellcode | x | ✔ |
| Encode x64 alphanumeric shellcode | ✔ | ✔ |
| Original shellcode can contain zero bytes | ✔ | x |
| Base address register can contain offset | ✔ | x |
Alpha3
限制只能使用字母或者数字
alpha3使用:
alpha3需要python2环境,所以先安装python2
1 | from pwn import * |
1 | python2 ALPHA3.py x64 ascii mixedcase rdx --input="sc.bin" > out.bin |
可以选择架构、编码、限制的字符
AE64
AE64可以直接在python中导入,使用相对较为方便且限制较少
1 | from ae64 import AE64 |
手动绕过
主要是通过sub、add、xor等指令对于非字母数字指令进行加密。
可以先根据限制筛选出受限制后的指令列表,然后根据指令列表进行组合,从而实现绕过。
另一种方法是通过shellcode先实现write读取到shellcode的位置,然后输入新的无限制的
shellcode来完成绕过。
https://nets.ec/Alphanumeric_shellcode
特定位置字符限制
在最近的*CTF中存在一个用浮点数输入字符,并对浮点数做限制写shellcode的题目,实际上是限制了每八位需要有两位是特定字符,这里给出两种绕过思路:
1 | mov rcx, im64 |
这里im是可以由我们自由控制的立即数,因此我们可以通过插入这些无关指令填充来绕过限制,上面这些指令涵盖了3、4、5字节,可以灵活插入来达到需要的效果
1 | jmp short |
通过jmp短跳转直接跳过中间指令,从而绕过限制
jmp指令本身只有两个字节,更为灵活。
对于orw的限制
如果程序还对orw等系统调用作出了限制呢?
w的限制还好说,可以通过侧信道leak出flag,而如果禁用了open,orw就 很难进行下去了。
但是还有一种方法。
利用32位调用绕过orw
x86与x64的syscall number是不一样的,如果能够跳转到32位执行相应的shellcode,就可一绕过限制。
x86 sys_number
| sys_number | | | | |
|—|—|—|—|—|—|
|3|read|0x03|unsigned int fd|char *buf|size_t count|
|4|write|0x04|unsigned int fd|const char *buf|size_t count|
|5|open|0x05|const char *filename|int flags|umode_t mode|
而程序是由32位还是64位执行是由cs寄存器决定的,而retfq指令可以对其作出更改,从而切换寄存器状态,所以可以由此实现orw。
值得注意的是, 对于32位程序, 由于kernel 也要对其作出相应支持, 所以内核代码中有一个操作系统层面的arch判断, personality, 这会影响mmap之类的操作
x32 ABI
x32 ABI 是一个应用程序二进制接口 (ABI),也是 Linux 内核的接口之一。 x32 ABI 在 Intel 和 AMD 64 位硬件上提供 32 位整数、长整数和指针。
可以通过 查看内核源代码 unistd_x32.h 查看
1 | cat /usr/src/kernels/6.4.7-200.fc38.x86_64/arch/x86/include/generated/uapi/asm/unistd_x32.h |
1 |
即可以通过0x40000000+syscall_number 来调用一些系统调用。所以可以绕过对syscall的限制。
不过这个特性似乎在大多数发行版中不受支持。
io_uring
io_uring 本身可以实现所有orw乃至socket连接操作, 在linux5.xx最少需要mmap和 io_uring_setup 两个syscall, 之后增加了 IORING_SETUP_NOMMAP 则可以只用一个syscall来实现orw
常见系统调用
open系syscall
- open
- openat
- openat2
1 |
|
- name_to_handle_at+name_to_handle_at
write系
- write
- sendto
- sendmsg
- sendfile
- sendmmsg
杂
- rename
- mprotect
- mmap
- execve
- ptrace
- seccomp
对于syscall指令的过滤
- vdso: 通过vdso 中的syscall获取, vdso地址可以从栈中获取
- sysenter: sysenter 指令可以替代syscall
- int 80: int 80指令也可以替代
获取有用的地址
shellcode运行前还可能会使用汇编清理程序的寄存器,这时有:
- 浮点相关寄存器(xmm/ymm)可能有残留地址,特别的,在存在输入函数的情况下,程序可能存在libc地址(一般是stdin)
- fs_base指向进程tcb,一般的,这是一个libc地址
使用C来写shellcode
一般而言,shellcode要尽可能小并且不依赖其他代码并具有地址无关性,所以应该尽量不使用函数调用,而应该直接使用syscall,然而,由于部分syscall需要传递的参数是一个复杂的结构体,比如io_uring相关syscall。
首先,为了编译器不生成依赖地址的代码,首先应该尽量使用栈上变量,在栈上准备好相关结构体,然后使用inline asm来完成syscall, 然后就可以直接提取相应函数:
1 | /* SPDX-License-Identifier: MIT */ |
tricks
- 对于一些题目,对shellcode的检查用到了strlen,那么可以通过先使用一些存在NULL截断的指令,从而使得后面的字符串绕过限制。
- 在无法获取shellcode运行地址时,可以运行syscall,运行后,rcx会被改写为下一条指令的地址。在32位程序中,还可以通过call指令获取将运行地址压入栈中,在64位地址中,可以直接通过
lea rax, [rip]来获取rip地址