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