├── .gitignore ├── Makefile ├── README.md ├── go.mod ├── go.sum ├── gobox.go ├── gobox_test.go └── pass.txt /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | gobox 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: install 2 | install: go.sum 3 | go get -v -t 4 | 5 | .PHONY: build 6 | build: install 7 | go build -o gobox 8 | 9 | .PHONY: test 10 | test: build 11 | go test 12 | 13 | .PHONY: dist 14 | dist: 15 | env GOOS=linux GOARCH=amd64 go build -o ./dist/gobox_linux_amd64 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # goBox 2 | 3 | > GO sandbox to run untrusted code. 4 | 5 | goBox uses Ptrace to hook into READ syscalls, giving you the option to accept or deny syscalls before they are executed. 6 | 7 | ## Usage 8 | 9 | ``` 10 | Usage of ./gobox: 11 | 12 | gobox [FLAGS] command 13 | 14 | flags: 15 | -h Print Usage. 16 | -n value 17 | A glob pattern for automatically blocking file reads. 18 | -y value 19 | A glob pattern for automatically allowing file reads. 20 | ``` 21 | 22 | ## Use cases 23 | 24 | ### You want to install anything 25 | 26 | ```shell 27 | > gobox -n "/etc/password.txt" npm install sketchy-module 28 | 29 | BLOCKED READ on /etc/password.txt 30 | ``` 31 | 32 | ```shell 33 | > gobox -n "/etc/password.txt" bash <(curl https://danger.zone/install.sh) 34 | 35 | BLOCKED READ on /etc/password.txt 36 | ``` 37 | 38 | ### You are interested in what file reads you favourite program makes. 39 | 40 | Sure you could use strace, but it references file descriptors goBox makes the this much easier at a glance by printing the absolute path of the fd. 41 | 42 | ``` 43 | > gobox ls 44 | Wanting to READ /usr/lib/x86_64-linux-gnu/libselinux.so.1 [y/n] 45 | ``` 46 | 47 | **NOTE**: It's definitely a better idea to encrypt all your sensitive data, goBox should probably only be used when that is inconvenient or impractical. 48 | 49 | **NOTE**: I haven't made any effort for cross-x compatibility so it currently only works on linux. I'd happily accept patches to improve portability. 50 | 51 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nishitm/goBox 2 | 3 | go 1.13 4 | 5 | require github.com/gobwas/glob v0.2.3 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= 2 | github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= 3 | -------------------------------------------------------------------------------- /gobox.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "os" 9 | "os/exec" 10 | "strings" 11 | "syscall" 12 | 13 | "github.com/gobwas/glob" 14 | ) 15 | 16 | type Request struct { 17 | path string 18 | syscall string 19 | allowed bool 20 | } 21 | 22 | func requestPermission(path string) (Request, error) { 23 | scanner := bufio.NewScanner(os.Stdin) 24 | fmt.Println(fmt.Sprintf("Wanting to READ %s [y/n]", path)) 25 | for scanner.Scan() { 26 | input := strings.ToLower(scanner.Text()) 27 | if input == "y" { 28 | break 29 | } 30 | if scanner.Text() == "n" { 31 | return Request{path, "READ", false}, nil 32 | } 33 | 34 | // Make a sounds 35 | fmt.Printf("\a") 36 | } 37 | return Request{path, "READ", true}, nil 38 | } 39 | 40 | func Exec(bin string, args, allowedPatterns, blockedPatterns []string) (map[string]Request, error) { 41 | var regs syscall.PtraceRegs 42 | reqs := make(map[string]Request) 43 | cmd := exec.Command(bin, args...) 44 | 45 | cmd.Stderr = nil 46 | cmd.Stdin = nil 47 | cmd.Stdout = nil 48 | cmd.SysProcAttr = &syscall.SysProcAttr{ 49 | Ptrace: true, 50 | // TODO Pdeathsig a linux only 51 | Pdeathsig: syscall.SIGKILL, 52 | } 53 | cmd.Stdout = os.Stdout 54 | cmd.Stderr = os.Stderr 55 | err := cmd.Start() 56 | if err != nil { 57 | return nil, fmt.Errorf("error while starting: %w", err) 58 | } 59 | _ = cmd.Wait() 60 | 61 | pid := cmd.Process.Pid 62 | 63 | for { 64 | err := syscall.PtraceGetRegs(pid, ®s) 65 | if err != nil { 66 | break 67 | } 68 | 69 | // https://stackoverflow.com/questions/33431994/extracting-system-call-name-and-arguments-using-ptrace 70 | if regs.Orig_rax == 0 { 71 | // TODO this is a cross-x barrier 72 | path, err := os.Readlink(fmt.Sprintf("/proc/%d/fd/%d", pid, regs.Rdi)) 73 | 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | for _, pattern := range allowedPatterns { 79 | g := glob.MustCompile(pattern) 80 | matched := g.Match(path) 81 | 82 | if matched { 83 | matchedReq := Request{path, "READ", true} 84 | reqs[path] = matchedReq 85 | } 86 | } 87 | 88 | for _, pattern := range blockedPatterns { 89 | g := glob.MustCompile(pattern) 90 | matched := g.Match(path) 91 | 92 | if matched { 93 | matchedReq := Request{path, "READ", false} 94 | reqs[path] = matchedReq 95 | } 96 | } 97 | 98 | req, ok := reqs[path] 99 | 100 | if !ok { 101 | req, err := requestPermission(path) 102 | if err != nil { 103 | return nil, err 104 | } 105 | reqs[req.path] = req 106 | 107 | // Throw and exit the command 108 | if !req.allowed { 109 | return nil, errors.New(fmt.Sprintf("Blocked %s on %s", req.syscall, req.path)) 110 | } 111 | 112 | } else { 113 | // Throw and exit the command 114 | if !req.allowed { 115 | return nil, errors.New(fmt.Sprintf("Blocked %s on %s", req.syscall, req.path)) 116 | } 117 | } 118 | } 119 | 120 | err = syscall.PtraceSyscall(pid, 0) 121 | if err != nil { 122 | return nil, err 123 | } 124 | 125 | _, err = syscall.Wait4(pid, nil, 0, nil) 126 | if err != nil { 127 | return nil, err 128 | } 129 | } 130 | return reqs, nil 131 | } 132 | 133 | type arrayFlags []string 134 | 135 | func (i *arrayFlags) String() string { 136 | return "" 137 | } 138 | 139 | func (i *arrayFlags) Set(value string) error { 140 | *i = append(*i, value) 141 | return nil 142 | } 143 | 144 | func main() { 145 | var allowedPattern arrayFlags 146 | var blockedPattern arrayFlags 147 | 148 | // TODO add sane defaults like libc etc 149 | allowedPattern = append(allowedPattern, "") 150 | 151 | flag.Var(&allowedPattern, "y", "A glob pattern for automatically allowing file reads.") 152 | flag.Var(&blockedPattern, "n", "A glob pattern for automatically blocking file reads.") 153 | help := flag.Bool("h", false, "Print Usage.") 154 | 155 | flag.Parse() 156 | 157 | if *help == true { 158 | flag.Usage() 159 | return 160 | } 161 | 162 | args := flag.Args() 163 | 164 | _, err := Exec(args[0], args[1:], allowedPattern, blockedPattern) 165 | if err != nil { 166 | fmt.Println(err) 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /gobox_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "os/exec" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestExec(t *testing.T) { 11 | s := []string{"pass.txt"} 12 | patterns := []string{""} 13 | reqs, err := Exec("cat", s, patterns, patterns) 14 | 15 | if err != nil { 16 | t.Errorf("Something went wrong: %v", err) 17 | } 18 | 19 | if len(reqs) != 2 { 20 | t.Errorf("reqs count was incorrect, got: %d, want: %d.", len(reqs), 2) 21 | } 22 | } 23 | 24 | func TestInput(t *testing.T) { 25 | cmd := exec.Command("./gobox", "cat", "./pass.txt") 26 | var out bytes.Buffer 27 | var in bytes.Buffer 28 | cmd.Stdout = &out 29 | cmd.Stdout = &in 30 | in.Write([]byte("n\n\r")) 31 | 32 | err := cmd.Run() 33 | if err != nil { 34 | t.Errorf("Something went wrong: %v", err) 35 | } 36 | if strings.Contains(out.String(), "Blocked READ on ...") { 37 | t.Errorf("Expected %s output got %s", "Blocked READ on ...", out.String()) 38 | } 39 | } 40 | 41 | // TODO we probably should instead just pass a mock reader for stdin into the Exec function and then call the fn 42 | // directly rather that full bin tests 43 | func TestAllowList(t *testing.T) { 44 | cmd := exec.Command("./gobox", "--y", "*.so", "--y", "*.txt", "cat", "./pass.txt") 45 | var out bytes.Buffer 46 | cmd.Stdout = &out 47 | err := cmd.Run() 48 | if err != nil { 49 | t.Errorf("Something went wrong: %v", err) 50 | } 51 | if out.String() != "123\n" { 52 | t.Errorf("Expected %s output got %s", "123", out.String()) 53 | } 54 | } 55 | 56 | func TestBlockList(t *testing.T) { 57 | cmd := exec.Command("./gobox", "--y", "*.so", "--n", "*.txt", "cat", "./password.txt") 58 | var out bytes.Buffer 59 | cmd.Stdout = &out 60 | err := cmd.Run() 61 | if err != nil { 62 | t.Errorf("Something went wrong: %v", err) 63 | } 64 | if !strings.Contains(out.String(), "Blocked READ on ") { 65 | t.Errorf("Expected %s output got %s", "Blocked READ on", out.String()) 66 | } 67 | } 68 | 69 | func TestHelp(t *testing.T) { 70 | cmd := exec.Command("./gobox", "-h") 71 | var out bytes.Buffer 72 | cmd.Stdout = &out 73 | err := cmd.Run() 74 | 75 | if err != nil { 76 | t.Errorf("Something went wrong: %v", err) 77 | } 78 | 79 | if strings.Contains(out.String(), "Usage of ./gobox:") { 80 | t.Errorf("Expected %s output got %s", "123", out.String()) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /pass.txt: -------------------------------------------------------------------------------- 1 | test123 2 | --------------------------------------------------------------------------------