move seccomp filter utility into separate package

This commit is contained in:
criyle 2019-04-17 18:36:56 -04:00
parent ba976039cf
commit 8f4e4c3237
8 changed files with 376 additions and 283 deletions

117
README.md
View File

@ -7,12 +7,12 @@ Goal is to reimplement [uoj-judger/run_program](https://github.com/vfleaking/uoj
Install:
+ install go compiler: `apt install golang-go`
+ install libseccomp-dev: `apt install libseccomp-dev`
+ install: `go get -d github.com/criyle/go-judger`
+ install: `go install github.com/criyle/go-judger/...`
Features (same as uoj-judger/run_program):
1. Restricted computing resource: Time / Memory (Stack) / Output
2. Restricted syscall access (by libseccomp / ptrace)
3. Restricted file access (read / write / access / exec)
1. Restricted computing resource: Time & Memory (Stack) & Output
2. Restricted syscall access (by libseccomp & ptrace)
3. Restricted file access (read & write & access & exec)
Default file access action:
+ check file read / write: `open`, `openat`
@ -22,33 +22,110 @@ Default file access action:
+ check file exec: `execve`
Packages:
+ Tracee: fork-exec with seccomp loaded
+ Tracer: ptrace tracee and provides syscall trap
+ Secutil: provides common utility function that wrappers libseccomp
+ Tracee: ptraced fork-exec with seccomp loaded
+ Tracer: ptrace tracee and provides syscall trap context
Executable:
+ run_program: under construction
TODO:
Planned example config file format(yaml):
Planned example config file format(yaml) `run_program.yaml`:
``` yaml
python2:
+ extra_syscall_allow:
- clone
+ extra_syscall_count:
- set_tid_address: 1
+ extra_syscall_ban:
- socket
+ extra_file_read:
- /usr/bin
+ extra_file_write:
- ./
+ extra_file_stat:
- /usr/bin
syscall:
extraAllow:
- clone
extraCount:
set_tid_address: 1
extraBan:
- socket
file:
extraRead:
- /usr/bin
extraWrite:
- ./
extraStat:
- /usr/bin
extraBan:
- /etc/passwd
```
Planned config file format for compiler `compiler.yaml`:
``` yaml
python:
exec: python ...
...:
```
Planned runtime spec(yaml) `run.yaml`:
``` yaml
inputFile: input.txt
outputFile: output.txt
...:
```
+ allow multiple traced programs
+ FD table instead of file names and allow pipes
+ allow pipes
+ Percise resource limits (s -> ms, mb -> kb)
+ More architectures (arm32, x86)
+ ...
Default Allowed Syscalls:
``` go
// file access through fd
"read", "write", "readv", "writev",
"close",
"fstat",
"lseek",
"dup", "dup2", "dup3",
"ioctl", "fcntl",
// memory action
"mmap", "mprotect", "munmap", "brk",
"mremap", "msync", "mincore", "madvise",
// signal action
"rt_sigaction",
"rt_sigprocmask",
"rt_sigreturn",
"rt_sigpending",
"sigaltstack",
// get current work dir
"getcwd",
// process exit
"exit",
"exit_group",
// others
"arch_prctl",
"gettimeofday",
"getrlimit",
"getrusage",
"times",
"time",
"clock_gettime",
"restart_syscall",
```
Default Allowed File Reads:
``` go
"/etc/ld.so.nohwcap",
"/etc/ld.so.preload",
"/etc/ld.so.cache",
"/lib/x86_64-linux-gnu/",
"/usr/lib/x86_64-linux-gnu/",
"/usr/lib/locale/locale-archive",
"/proc/self/exe",
"/etc/timezone",
"/usr/share/zoneinfo/",
"/dev/random",
"/dev/urandom",
"/proc/meminfo",
"/etc/localtime",
``

73
secutil/secutil.go Normal file
View File

@ -0,0 +1,73 @@
// Package secutil provides utility functions to manipulate seccomp filters
// provided by libseccomp
package secutil
import (
"io/ioutil"
"os"
"syscall"
"unsafe"
libseccomp "github.com/seccomp/libseccomp-golang"
)
// FilterToBPF convert libseccomp filter to kernel readable BPF style
func FilterToBPF(filter *libseccomp.ScmpFilter) (*syscall.SockFprog, error) {
r, w, err := os.Pipe()
if err != nil {
return nil, err
}
// export to pipe
go func() {
filter.ExportBPF(w)
w.Close()
}()
// get BPF binary
bin, err := ioutil.ReadAll(r)
if err != nil {
return nil, err
}
// directly convert pointer
return &syscall.SockFprog{
Len: uint16(len(bin) / 8),
Filter: (*syscall.SockFilter)(unsafe.Pointer(&bin[0])),
}, nil
}
// BuildFilter builds libseccomp filter by defining the default action, trace action
// allow and trace syscall names
func BuildFilter(defaultAct, traceAct libseccomp.ScmpAction, allow, trace []string) (*libseccomp.ScmpFilter, error) {
filter, err := libseccomp.NewFilter(defaultAct)
if err != nil {
return nil, err
}
for _, s := range allow {
err := addFilterAction(filter, s, libseccomp.ActAllow)
if err != nil {
return nil, err
}
}
for _, s := range trace {
err := addFilterAction(filter, s, traceAct)
if err != nil {
return nil, err
}
}
return filter, nil
}
func addFilterAction(filter *libseccomp.ScmpFilter, name string, action libseccomp.ScmpAction) error {
syscallID, err := libseccomp.GetSyscallFromName(name)
if err != nil {
return err
}
err = filter.AddRule(syscallID, action)
if err != nil {
return err
}
return nil
}

View File

@ -1,51 +1,38 @@
// Package tracee provides interface to run a seccomp filtered, rlimited
// executable and ptraced
package tracee
import (
libseccomp "github.com/seccomp/libseccomp-golang"
)
import "syscall"
// Runner is the RunProgramConfig including the exec path, argv
// and resource limits. It creates tracee for ptraced tracer
// and resource limits. It creates tracee for ptrace-based tracer.
type Runner struct {
// Resource limit set by set rlimit
TimeLimit uint64 // second
RealTimeLimit uint64 // second
MemoryLimit uint64 // mb
OutputLimit uint64 // mb
StackLimit uint64 // mb
// stdin, stdout, stderr file name. nil for default
InputFileName string
OutputFileName string
ErrorFileName string
// work path
WorkPath string
// argv and env for the child process
Args []string
Env []string
// libseccomp Filter applied to child
Filter *libseccomp.ScmpFilter
// Resource limit set by set rlimit
RLimits []RLimit
// file disriptors for new process, from 0 to len - 1
Files []uintptr
// work path set by setcwd (current working directory for child)
WorkDir string
// BPF syscall filter applied to child
BPF *syscall.SockFprog
}
// NewRunner creates program config with default setting
// RLimit is the resource limits defined by Linux setrlimit
type RLimit struct {
// Res is the resource type (e.g. syscall.RLIMIT_CPU)
Res int
// Rlim is the limit applied to that resource
Rlim syscall.Rlimit
}
// NewRunner creates new runner struct
func NewRunner() Runner {
return Runner{
TimeLimit: 1,
RealTimeLimit: 0,
MemoryLimit: 256,
OutputLimit: 64,
StackLimit: 1024,
}
}
func (r *Runner) verify() {
if r.RealTimeLimit < r.TimeLimit {
r.RealTimeLimit = r.TimeLimit + 2
}
if r.StackLimit > r.MemoryLimit {
r.StackLimit = r.MemoryLimit
}
return Runner{}
}

View File

@ -1,7 +1,6 @@
package tracee
import (
"os"
"syscall"
"unsafe" // required for go:linkname.
@ -25,17 +24,17 @@ func afterForkInChild()
//go:norace
func (r *Runner) Start() (int, error) {
var (
err1 syscall.Errno
bpf *syscall.SockFprog
err1 syscall.Errno
workdir *byte
nextfd int
)
// verify
r.verify()
// make exec args
// make exec args0
argv0, err := syscall.BytePtrFromString(r.Args[0])
if err != nil {
return 0, err
}
// make exec args
argv, err := syscall.SlicePtrFromStrings(r.Args)
if err != nil {
return 0, err
@ -46,39 +45,24 @@ func (r *Runner) Start() (int, error) {
return 0, err
}
// make bpf using libseccomp
if r.Filter != nil {
bpf, err = FilterToBPF(r.Filter)
// make work dir
if r.WorkDir != "" {
workdir, err = syscall.BytePtrFromString(r.WorkDir)
if err != nil {
return 0, err
}
}
// rlimit
rlimits := r.prepareRLimit()
// work dir
var dir *byte
if r.WorkPath != "" {
dir, err = syscall.BytePtrFromString(r.WorkPath)
if err != nil {
return 0, err
}
}
// stdin, stdout, stderr
files, err := r.prepareFile()
if err != nil {
return 0, nil
}
defer closeFiles(files)
fds := make([]uintptr, 3)
for i, f := range files {
if f != nil {
fds[i] = f.Fd()
// similar to exec_linux, avoid side effect by shuffling around
fd := make([]int, len(r.Files))
nextfd = len(r.Files)
for i, ufd := range r.Files {
if nextfd < int(ufd) {
nextfd = int(ufd)
}
fd[i] = int(ufd)
}
nextfd++
// Acquire the fork lock so that no other threads
// create new fds that are not yet close-on-exec
@ -106,29 +90,52 @@ func (r *Runner) Start() (int, error) {
// Notice: cannot call any GO functions beyond this point
// Set limit
for _, rlim := range rlimits {
_, _, err1 = syscall.RawSyscall(syscall.SYS_SETRLIMIT, uintptr(rlim.resource), uintptr(unsafe.Pointer(&rlim.rlim)), 0)
for _, rlim := range r.RLimits {
_, _, err1 = syscall.RawSyscall(syscall.SYS_SETRLIMIT, uintptr(rlim.Res), uintptr(unsafe.Pointer(&rlim.Rlim)), 0)
if err1 != 0 {
goto childerror
}
}
// Chdir if needed
if dir != nil {
_, _, err1 = syscall.RawSyscall(syscall.SYS_CHDIR, uintptr(unsafe.Pointer(dir)), 0, 0)
if workdir != nil {
_, _, err1 = syscall.RawSyscall(syscall.SYS_CHDIR, uintptr(unsafe.Pointer(workdir)), 0, 0)
if err1 != 0 {
goto childerror
}
}
// setup stdin, stdout, stderr
// the other file already marked as close on exec
for i, fd := range fds {
if fd != 0 {
_, _, err1 = syscall.RawSyscall(syscall.SYS_DUP2, fd, uintptr(i), 0)
// Pass 1: fd[i] < i => nextfd
for i := 0; i < len(fd); i++ {
if fd[i] >= 0 && fd[i] < int(i) {
_, _, err1 = syscall.RawSyscall(syscall.SYS_DUP2, uintptr(fd[i]), uintptr(nextfd), 0)
if err1 != 0 {
goto childerror
}
// Set up close on exec
syscall.RawSyscall(syscall.SYS_FCNTL, uintptr(nextfd), syscall.F_SETFD, syscall.FD_CLOEXEC)
fd[i] = nextfd
nextfd++
}
}
// Pass 2: fd[i] => i
for i := 0; i < len(fd); i++ {
if fd[i] == -1 {
syscall.RawSyscall(syscall.SYS_CLOSE, uintptr(i), 0, 0)
continue
}
if fd[i] == int(i) {
// dup2(i, i) will not clear close on exec flag, need to reset the flag
_, _, err1 = syscall.RawSyscall(syscall.SYS_FCNTL, uintptr(fd[i]), syscall.F_SETFD, 0)
if err1 != 0 {
goto childerror
}
continue
}
_, _, err1 = syscall.RawSyscall(syscall.SYS_DUP2, uintptr(fd[i]), uintptr(i), 0)
if err1 != 0 {
goto childerror
}
}
@ -139,7 +146,7 @@ func (r *Runner) Start() (int, error) {
}
// Load seccomp, stop and wait for tracer
if r.Filter != nil {
if r.BPF != nil {
// Check if support
// SECCOMP_SET_MODE_STRICT = 0, args = 1 for invalid operation
_, _, err1 = syscall.Syscall(unix.SYS_SECCOMP, 0, 1, 0)
@ -148,7 +155,7 @@ func (r *Runner) Start() (int, error) {
}
// Load the filter manually
// No new priv
// No new privs
_, _, err1 = syscall.Syscall6(syscall.SYS_PRCTL, unix.PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0, 0)
if err1 != 0 {
goto childerror
@ -173,7 +180,7 @@ func (r *Runner) Start() (int, error) {
// Load seccomp filter
// SECCOMP_SET_MODE_FILTER = 1
// SECCOMP_FILTER_FLAG_TSYNC = 1
_, _, err1 = syscall.Syscall(unix.SYS_SECCOMP, 1, 1, uintptr(unsafe.Pointer(bpf)))
_, _, err1 = syscall.Syscall(unix.SYS_SECCOMP, 1, 1, uintptr(unsafe.Pointer(r.BPF)))
if err1 != 0 {
goto childerror
}
@ -192,75 +199,3 @@ childerror:
// cannot reach this point
panic("cannot reach")
}
type rlimit struct {
resource int
rlim syscall.Rlimit
}
// prepareRLimit creates rlimit structures for tracee
func (r *Runner) prepareRLimit() []rlimit {
return []rlimit{
// CPU limit
{
resource: syscall.RLIMIT_CPU,
rlim: syscall.Rlimit{
Cur: r.TimeLimit,
Max: r.RealTimeLimit,
},
},
// File limit
{
resource: syscall.RLIMIT_FSIZE,
rlim: syscall.Rlimit{
Cur: r.OutputLimit << 20,
Max: r.OutputLimit << 20,
},
},
// Stack limit
{
resource: syscall.RLIMIT_STACK,
rlim: syscall.Rlimit{
Cur: r.StackLimit << 20,
Max: r.StackLimit << 20,
},
},
}
}
// prepareFile opens file for new process
func (r *Runner) prepareFile() ([]*os.File, error) {
var err error
files := make([]*os.File, 3)
if r.InputFileName != "" {
files[0], err = os.OpenFile(r.InputFileName, os.O_RDONLY, 0755)
if err != nil {
goto openerror
}
}
if r.OutputFileName != "" {
files[1], err = os.OpenFile(r.OutputFileName, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0755)
if err != nil {
goto openerror
}
}
if r.ErrorFileName != "" {
files[2], err = os.OpenFile(r.ErrorFileName, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0755)
if err != nil {
goto openerror
}
}
return files, nil
openerror:
closeFiles(files)
return nil, err
}
// closeFiles close all file in the list
func closeFiles(files []*os.File) {
for _, f := range files {
if f != nil {
f.Close()
}
}
}

View File

@ -1,36 +0,0 @@
package tracee
import (
"io/ioutil"
"os"
"syscall"
"unsafe"
libseccomp "github.com/seccomp/libseccomp-golang"
)
// FilterToBPF convert libseccomp filter to kernel readable BPF style
func FilterToBPF(filter *libseccomp.ScmpFilter) (*syscall.SockFprog, error) {
r, w, err := os.Pipe()
if err != nil {
return nil, err
}
// export to pipe
go func() {
filter.ExportBPF(w)
w.Close()
}()
// get BPF binary
bin, err := ioutil.ReadAll(r)
filter.Release()
if err != nil {
return nil, err
}
// directly convert pointer
return &syscall.SockFprog{
Len: uint16(len(bin) / 8),
Filter: (*syscall.SockFilter)(unsafe.Pointer(&bin[0])),
}, nil
}

View File

@ -1,50 +0,0 @@
package tracer
import libseccomp "github.com/seccomp/libseccomp-golang"
const (
msgDisallow int16 = iota + 1
msgHandle
)
func addFilterAction(filter *libseccomp.ScmpFilter, name string, action libseccomp.ScmpAction) error {
syscallID, err := libseccomp.GetSyscallFromName(name)
if err != nil {
return err
}
err = filter.AddRule(syscallID, action)
if err != nil {
return err
}
return nil
}
func (r *Tracer) buildFilter() (*libseccomp.ScmpFilter, error) {
// make filter
var defaultAction libseccomp.ScmpAction
// if debug, allow all syscalls and output what was blocked
if r.Unsafe || r.ShowDetails {
defaultAction = libseccomp.ActTrace.SetReturnCode(msgDisallow)
} else {
defaultAction = libseccomp.ActKill
}
filter, err := libseccomp.NewFilter(defaultAction)
if err != nil {
return nil, err
}
for _, s := range r.Allow {
err := addFilterAction(filter, s, libseccomp.ActAllow)
if err != nil {
return nil, err
}
}
for _, s := range r.Trace {
err := addFilterAction(filter, s, libseccomp.ActTrace.SetReturnCode(msgHandle))
if err != nil {
return nil, err
}
}
return filter, nil
}

View File

@ -6,7 +6,7 @@ import (
"runtime"
"time"
tracee "github.com/criyle/go-judger/tracee"
secutil "github.com/criyle/go-judger/secutil"
libseccomp "github.com/seccomp/libseccomp-golang"
unix "golang.org/x/sys/unix"
)
@ -33,8 +33,31 @@ func (r *Tracer) StartTrace() (result *TraceResult, err error) {
if err != nil {
return
}
defer filter.Release()
tr := r.getTraceeRunner(filter)
bpf, err := secutil.FilterToBPF(filter)
if err != nil {
return
}
// open input / output / err files
files, err := r.prepareFiles()
if err != nil {
return
}
defer closeFiles(files)
// if not defined, then use the original value
fds := make([]uintptr, len(files))
for i, f := range files {
if f != nil {
fds[i] = f.Fd()
} else {
fds[i] = uintptr(i)
}
}
// get tracee
tr := r.getTraceeRunner(bpf, fds)
// run in restricted mode
pid, err := tr.Start()
@ -273,24 +296,3 @@ func setPtraceOption(pid int) error {
return unix.PtraceSetOptions(pid, unix.PTRACE_O_TRACESECCOMP|unix.PTRACE_O_EXITKILL|
unix.PTRACE_O_TRACEFORK|unix.PTRACE_O_TRACECLONE|unix.PTRACE_O_TRACEEXEC|unix.PTRACE_O_TRACEVFORK)
}
func (r *Tracer) getTraceeRunner(filter *libseccomp.ScmpFilter) tracee.Runner {
tr := tracee.NewRunner()
tr.TimeLimit = r.TimeLimit
tr.RealTimeLimit = r.RealTimeLimit
tr.MemoryLimit = r.MemoryLimit
tr.OutputLimit = r.OutputLimit
tr.StackLimit = r.StackLimit
tr.Args = r.Args
tr.Env = r.Env
tr.InputFileName = r.InputFileName
tr.OutputFileName = r.OutputFileName
tr.ErrorFileName = r.ErrorFileName
tr.WorkPath = r.WorkPath
tr.Filter = filter
return tr
}

105
tracer/tracer_util.go Normal file
View File

@ -0,0 +1,105 @@
package tracer
import (
"os"
"syscall"
secutil "github.com/criyle/go-judger/secutil"
tracee "github.com/criyle/go-judger/tracee"
libseccomp "github.com/seccomp/libseccomp-golang"
)
const (
msgDisallow int16 = iota + 1
msgHandle
)
func (r *Tracer) buildFilter() (*libseccomp.ScmpFilter, error) {
// make filter
var defaultAction libseccomp.ScmpAction
// if debug, allow all syscalls and output what was blocked
if r.Unsafe || r.ShowDetails {
defaultAction = libseccomp.ActTrace.SetReturnCode(msgDisallow)
} else {
defaultAction = libseccomp.ActKill
}
return secutil.BuildFilter(defaultAction, libseccomp.ActTrace.SetReturnCode(msgHandle), r.Allow, r.Trace)
}
// prepareFile opens file for new process
func (r *Tracer) prepareFiles() ([]*os.File, error) {
var err error
files := make([]*os.File, 3)
if r.InputFileName != "" {
files[0], err = os.OpenFile(r.InputFileName, os.O_RDONLY, 0755)
if err != nil {
goto openerror
}
}
if r.OutputFileName != "" {
files[1], err = os.OpenFile(r.OutputFileName, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0755)
if err != nil {
goto openerror
}
}
if r.ErrorFileName != "" {
files[2], err = os.OpenFile(r.ErrorFileName, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0755)
if err != nil {
goto openerror
}
}
return files, nil
openerror:
closeFiles(files)
return nil, err
}
// closeFiles close all file in the list
func closeFiles(files []*os.File) {
for _, f := range files {
if f != nil {
f.Close()
}
}
}
func (r *Tracer) getTraceeRunner(bpf *syscall.SockFprog, fds []uintptr) tracee.Runner {
tr := tracee.NewRunner()
tr.RLimits = r.prepareRLimit()
tr.WorkDir = r.WorkPath
tr.BPF = bpf
tr.Args = r.Args
tr.Env = r.Env
tr.Files = fds
return tr
}
// prepareRLimit creates rlimit structures for tracee
func (r *Tracer) prepareRLimit() []tracee.RLimit {
return []tracee.RLimit{
// CPU limit
{
Res: syscall.RLIMIT_CPU,
Rlim: syscall.Rlimit{
Cur: r.TimeLimit,
Max: r.RealTimeLimit,
},
},
// File limit
{
Res: syscall.RLIMIT_FSIZE,
Rlim: syscall.Rlimit{
Cur: r.OutputLimit << 20,
Max: r.OutputLimit << 20,
},
},
// Stack limit
{
Res: syscall.RLIMIT_STACK,
Rlim: syscall.Rlimit{
Cur: r.StackLimit << 20,
Max: r.StackLimit << 20,
},
},
}
}