Repository: liamg/siphon Branch: main Commit: 6a6e402ef506 Files: 15 Total size: 11.8 KB Directory structure: gitextract_i7w6ueu6/ ├── .github/ │ └── workflows/ │ ├── release.yml │ └── test.yml ├── .gitignore ├── .goreleaser.yml ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── intercept.go ├── main.go ├── registers_386.go ├── registers_amd64.go ├── registers_arm.go ├── registers_arm64.go └── watch_test.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/release.yml ================================================ name: release on: push: tags: - v* jobs: build: name: releasing runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 with: fetch-depth: 0 - uses: actions/setup-go@v3 with: go-version: "1.19" - uses: goreleaser/goreleaser-action@v3 with: version: latest args: release --rm-dist env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/test.yml ================================================ name: tests on: pull_request: jobs: test: name: tests runs-on: ${{ matrix.os }} strategy: matrix: os: [ ubuntu-latest ] steps: - uses: actions/checkout@v3 - uses: actions/setup-go@v3 with: go-version: '1.19' cache: true - name: Run tests run: sudo -E $(which go) test ./... ================================================ FILE: .gitignore ================================================ .idea ================================================ FILE: .goreleaser.yml ================================================ builds: - id: siphon main: . binary: siphon ldflags: - "-s -w -extldflags '-fno-PIC -static'" env: - CGO_ENABLED=0 goos: - linux goarch: - "amd64" - "arm64" - "386" - "arm" changelog: sort: asc filters: exclude: - "^docs:" - "^test:" archives: - format: binary name_template: "{{ .Binary}}-{{ .Os }}-{{ .Arch }}" release: prerelease: auto github: owner: liamg name: siphon ================================================ FILE: LICENSE ================================================ This is free and unencumbered software released into the public domain. Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. For more information, please refer to ================================================ FILE: README.md ================================================ # Siphon Intercept input/output (_stdin/stdout/stderr_) for any process, even where said output is sent to _/dev/null_ or elsewhere. ![demo gif](demo.gif) It can also be used to spy on another users shell: ![demo gif 2](demo2.gif) Currently Siphon works on Linux, with `amd64`, `arm64`, `arm`, and `386`. Adding support for more architectures is pretty simple, feel free to raise an issue. It uses `ptrace` which means you'll likely need to run it as `root` for the ptrace privilege. ## Installation Grab a binary from the [latest release](https://github.com/liamg/siphon/releases/latest). ================================================ FILE: go.mod ================================================ module github.com/liamg/siphon go 1.19 require ( github.com/spf13/cobra v1.6.0 github.com/stretchr/testify v1.7.4 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/pflag v1.0.5 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: go.sum ================================================ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.6.0 h1:42a0n6jwCot1pUmomAp4T7DeMD+20LFv4Q54pxLf2LI= github.com/spf13/cobra v1.6.0/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.4 h1:wZRexSlwd7ZXfKINDLsO4r7WBt3gTKONc6K/VesHvHM= github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: intercept.go ================================================ package main import ( "fmt" "os" "runtime" "syscall" ) func watchProcess(pid int, stdout, stderr, stdin bool) error { // ensure tracing all comes from same thread runtime.LockOSThread() if _, err := os.FindProcess(pid); err != nil { return fmt.Errorf("could not find process with pid %d: %w", pid, err) } if err := syscall.PtraceAttach(pid); err == syscall.EPERM { return fmt.Errorf("could not attach to process with pid %d: %w - check your permissions", pid, err) } else if err != nil { return err } status := syscall.WaitStatus(0) if _, err := syscall.Wait4(pid, &status, 0, nil); err != nil { return err } defer func() { _ = syscall.PtraceDetach(pid) _, _ = syscall.Wait4(pid, &status, 0, nil) }() // deliver SIGTRAP|0x80 if err := syscall.PtraceSetOptions(pid, syscall.PTRACE_O_TRACESYSGOOD); err != nil { return err } for { fd, data, err := interceptReadsAndWrites(pid) if err != nil { return err } if stdout && fd == uint64(syscall.Stdout) || stderr && fd == uint64(syscall.Stderr) || stdin && fd == uint64(syscall.Stdin) { if fd == uint64(syscall.Stdin) { fd = uint64(os.Stdin.Fd()) } _, _ = fmt.Fprintf(os.NewFile(uintptr(fd), "pipe"), "%s", string(data)) } } } func interceptReadsAndWrites(pid int) (fd uint64, data []byte, err error) { // intercept syscall err = syscall.PtraceSyscall(pid, 0) if err != nil { return 0, nil, fmt.Errorf("could not intercept syscall: %w", err) } // wait for a syscall status := syscall.WaitStatus(0) _, err = syscall.Wait4(pid, &status, 0, nil) if err != nil { return 0, nil, err } // if interrupted, stop tracing if status.StopSignal().String() == "interrupt" { _ = syscall.PtraceSyscall(pid, int(status.StopSignal())) return 0, nil, fmt.Errorf("process interrupted") } var exited bool waitForExit := func() error { if exited { return nil } exited = true // continue the syscall we intercepted err = syscall.PtraceSyscall(pid, 0) if err != nil { return fmt.Errorf("could not continue process: %w", err) } // and wait for it to finish status = syscall.WaitStatus(0) _, err = syscall.Wait4(pid, &status, 0, nil) if err != nil { return err } return nil } defer func() { err = waitForExit() if err == nil { // process exited if status.Exited() { err = fmt.Errorf("process exited") return } // if interrupted, stop tracing if status.StopSignal().String() == "interrupt" { _ = syscall.PtraceSyscall(pid, int(status.StopSignal())) err = fmt.Errorf("process interrupted") return } } }() // if we have a syscall, examine it... if status.TrapCause()&int(syscall.SIGTRAP|0x80) > 0 { // wait for syscall exit if err := waitForExit(); err != nil { return 0, nil, err } // read registers regs := &syscall.PtraceRegs{} if err := syscall.PtraceGetRegs(pid, regs); err != nil { return 0, nil, err } // find the syscall number for the host architecture syscallNo := grabSyscallNo(regs) // if it's a read/write syscall, grab the args switch syscallNo { case syscall.SYS_READ, syscall.SYS_WRITE: // grab the args to WRITE for the host architecture // fd == file descriptor (generally 1 for stdout, 2 for stderr) // ptr == pointer to the buffer // lng == length of the buffer fd, ptr, lng := grabArgsFromRegs(regs) // if we want to see this output, read it from memory if lng > 0 { data := make([]byte, lng) if _, err := syscall.PtracePeekData(pid, uintptr(ptr), data); err != nil { return 0, nil, err } return fd, data, nil } } } return 0, nil, err } ================================================ FILE: main.go ================================================ package main import ( "fmt" "strconv" "github.com/spf13/cobra" ) var cmd = &cobra.Command{ Use: "siphon [pid]", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { cmd.SilenceUsage = true cmd.SilenceErrors = true pid, err := strconv.ParseInt(args[0], 10, 64) if err != nil { return fmt.Errorf("invalid pid: %w", err) } return watchProcess(int(pid), flagStdOut, flagStdErr, flagStdIn) }, } var ( flagStdOut = true flagStdErr = true flagStdIn = false ) func main() { cmd.Flags().BoolVarP(&flagStdOut, "stdout", "o", flagStdOut, "Show stdout") cmd.Flags().BoolVarP(&flagStdErr, "stderr", "e", flagStdErr, "Show stderr") cmd.Flags().BoolVarP(&flagStdIn, "stdin", "i", flagStdIn, "Show stdin") if err := cmd.Execute(); err != nil { _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Error: %v", err) } } ================================================ FILE: registers_386.go ================================================ //go:build 386 package main import ( "syscall" ) func grabSyscallNo(regs *syscall.PtraceRegs) uint64 { return uint64(regs.Orig_eax) } func grabArgsFromRegs(regs *syscall.PtraceRegs) (fd, ptr, lng uint64) { return uint64(regs.Ebx), uint64(regs.Ecx), uint64(regs.Edx) } ================================================ FILE: registers_amd64.go ================================================ //go:build amd64 package main import ( "syscall" ) func grabSyscallNo(regs *syscall.PtraceRegs) uint64 { return regs.Orig_rax } func grabArgsFromRegs(regs *syscall.PtraceRegs) (fd, ptr, lng uint64) { return regs.Rdi, regs.Rsi, regs.Rdx } ================================================ FILE: registers_arm.go ================================================ //go:build arm package main import ( "syscall" ) func grabSyscallNo(regs *syscall.PtraceRegs) uint64 { return uint64(regs.Uregs[7]) } func grabArgsFromRegs(regs *syscall.PtraceRegs) (fd, ptr, lng uint64) { return uint64(regs.Uregs[0]), uint64(regs.Uregs[1]), uint64(regs.Uregs[2]) } ================================================ FILE: registers_arm64.go ================================================ //go:build arm64 package main import ( "syscall" ) func grabSyscallNo(regs *syscall.PtraceRegs) uint64 { return regs.Regs[8] } func grabArgsFromRegs(regs *syscall.PtraceRegs) (fd, ptr, lng uint64) { return regs.Regs[0], regs.Regs[1], regs.Regs[2] } ================================================ FILE: watch_test.go ================================================ package main import ( "bytes" "os" "os/exec" "strconv" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func Test_Watch(t *testing.T) { require.Equal(t, 0, os.Getuid(), "test must be run as root") pingBuffer := bytes.NewBuffer([]byte{}) pingCmd := exec.Command("ping", "127.0.0.1") pingCmd.Stdout = pingBuffer require.NoError(t, pingCmd.Start()) time.Sleep(time.Second) watchBuffer := bytes.NewBuffer([]byte{}) watchCmd := exec.Command("go", "run", ".", strconv.Itoa(pingCmd.Process.Pid)) watchCmd.Stdout = watchBuffer require.NoError(t, watchCmd.Start()) time.Sleep(5 * time.Second) require.NoError(t, pingCmd.Process.Kill()) require.NoError(t, pingCmd.Process.Release()) _ = watchCmd.Wait() watchOutput := watchBuffer.String() assert.True(t, strings.HasSuffix(pingBuffer.String(), watchOutput)) assert.Greater(t, len(watchOutput), 0) }