Projects STRLCPY dismember Commits e25c39aa
🤬
  • ■ ■ ■ ■ ■
    .github/FUNDING.yml
     1 +github: [liamg]
  • ■ ■ ■ ■ ■ ■
    .github/workflows/release.yml
     1 +name: release
     2 + 
     3 +on:
     4 + push:
     5 + tags:
     6 + - v*
     7 + 
     8 +jobs:
     9 + build:
     10 + name: releasing
     11 + runs-on: ubuntu-latest
     12 + 
     13 + steps:
     14 + - uses: actions/checkout@v2
     15 + with:
     16 + fetch-depth: 0
     17 + 
     18 + - uses: actions/setup-go@v2
     19 + with:
     20 + go-version: '1.18'
     21 + 
     22 + - name: Release
     23 + uses: goreleaser/goreleaser-action@v2
     24 + with:
     25 + version: latest
     26 + args: release --rm-dist
     27 + env:
     28 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
     29 + 
  • ■ ■ ■ ■ ■ ■
    .github/workflows/test.yml
     1 +name: test
     2 + 
     3 +on:
     4 + pull_request:
     5 + 
     6 +jobs:
     7 + build:
     8 + runs-on: ${{ matrix.os }}
     9 + strategy:
     10 + matrix:
     11 + os: [ ubuntu-latest ] # optionally add macos-latest, windows-latest
     12 + name: build and test
     13 + 
     14 + steps:
     15 + - uses: actions/checkout@v2
     16 + - uses: actions/setup-go@v2
     17 + with:
     18 + go-version: '1.18'
     19 + - uses: actions/cache@v3
     20 + with:
     21 + path: |
     22 + ~/.cache/go-build
     23 + ~/go/pkg/mod
     24 + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
     25 + restore-keys: |
     26 + ${{ runner.os }}-go-
     27 + - name: Run test
     28 + run: make test
     29 + 
  • ■ ■ ■ ■ ■ ■
    .goreleaser.yml
     1 +builds:
     2 + - id: dismember
     3 + main: .
     4 + binary: dismember
     5 + ldflags:
     6 + - "-s -w"
     7 + flags:
     8 + - "--trimpath"
     9 + env:
     10 + - CGO_ENABLED=0
     11 + goos:
     12 + - linux
     13 + goarch:
     14 + - "386"
     15 + - amd64
     16 + - arm64
     17 +archives:
     18 + -
     19 + format: binary
     20 + name_template: "{{ .Binary}}-{{ .Os }}-{{ .Arch }}"
     21 +release:
     22 + prerelease: auto
     23 + github:
     24 + owner: liamg
     25 + name: dismember
     26 + 
  • ■ ■ ■ ■ ■ ■
    LICENSE
     1 +MIT License
     2 + 
     3 +Copyright (c) 2022 Liam Galvin
     4 + 
     5 +Permission is hereby granted, free of charge, to any person obtaining a copy
     6 +of this software and associated documentation files (the "Software"), to deal
     7 +in the Software without restriction, including without limitation the rights
     8 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     9 +copies of the Software, and to permit persons to whom the Software is
     10 +furnished to do so, subject to the following conditions:
     11 + 
     12 +The above copyright notice and this permission notice shall be included in all
     13 +copies or substantial portions of the Software.
     14 + 
     15 +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
     16 +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
     17 +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
     18 +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
     19 +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
     20 +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
     21 +SOFTWARE.
  • ■ ■ ■ ■ ■ ■
    Makefile
     1 +default: test
     2 + 
     3 +.PHONY: test
     4 +test:
     5 + go test -race ./...
     6 + 
  • ■ ■ ■ ■ ■ ■
    README.md
     1 +# dismember
     2 + 
     3 +Dismember is a command-line tool for Linux used to grep for patterns across the entire memory used by a process (or processes).
     4 + 
     5 +![A gif showing dismember finding a password from a Slack message](demo.gif)
     6 + 
     7 +Dismember can be used to search memory of all processes it has access to, so running it as root is the most effective method.
     8 + 
     9 +## Installation
     10 + 
     11 +Grab a binary from the [latest release](https://github.com/liamg/dismember/releases/latest) and add it to your path.
     12 + 
     13 +## Examples
     14 + 
     15 +### Search for a pattern in a process by PID
     16 +```bash
     17 +dismember grep -p 1234 'the password is .*'
     18 +```
     19 + 
     20 +### Search for a pattern in a process by name
     21 +```bash
     22 +dismember grep -n apache 'username=liamg&password=.*'
     23 +```
     24 + 
     25 +### Search for a pattern across all processes
     26 +```bash
     27 +# find a github api token
     28 +dismember grep 'gh[pousr]_[0-9a-zA-Z]{36}'
     29 +```
     30 + 
  • demo.gif
  • ■ ■ ■ ■ ■ ■
    go.mod
     1 +module github.com/liamg/dismember
     2 + 
     3 +go 1.18
     4 + 
     5 +require (
     6 + github.com/spf13/cobra v1.5.0
     7 + github.com/stretchr/testify v1.7.4
     8 +)
     9 + 
     10 +require (
     11 + github.com/davecgh/go-spew v1.1.1 // indirect
     12 + github.com/inconshreveable/mousetrap v1.0.0 // indirect
     13 + github.com/pmezard/go-difflib v1.0.0 // indirect
     14 + github.com/spf13/pflag v1.0.5 // indirect
     15 + gopkg.in/yaml.v3 v3.0.1 // indirect
     16 +)
     17 + 
  • ■ ■ ■ ■ ■ ■
    go.sum
     1 +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
     2 +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
     3 +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
     4 +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
     5 +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
     6 +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
     7 +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
     8 +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
     9 +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
     10 +github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
     11 +github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
     12 +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
     13 +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
     14 +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
     15 +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
     16 +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
     17 +github.com/stretchr/testify v1.7.4 h1:wZRexSlwd7ZXfKINDLsO4r7WBt3gTKONc6K/VesHvHM=
     18 +github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
     19 +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
     20 +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
     21 +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
     22 +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
     23 +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
     24 +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
     25 + 
  • ■ ■ ■ ■ ■ ■
    internal/cmd/grep.go
     1 +package cmd
     2 + 
     3 +import (
     4 + "bytes"
     5 + "fmt"
     6 + "regexp"
     7 + "strings"
     8 + 
     9 + "github.com/liamg/dismember/pkg/proc"
     10 + "github.com/spf13/cobra"
     11 +)
     12 + 
     13 +var flagPID int
     14 +var flagProcessName string
     15 +var flagIncludeSelf bool
     16 +var flagDumpRadius int
     17 + 
     18 +func init() {
     19 + 
     20 + grepCmd := &cobra.Command{
     21 + Use: "grep [keyword]",
     22 + Short: "Search process memory for a given string or regex",
     23 + Long: ``,
     24 + RunE: grepHandler,
     25 + Args: cobra.ExactArgs(1),
     26 + }
     27 + 
     28 + grepCmd.Flags().IntVarP(&flagPID, "pid", "p", 0, "PID of the process whose memory should be grepped. Omitting this option will grep the memory of all available processes on the system.")
     29 + grepCmd.Flags().StringVarP(&flagProcessName, "pname", "n", "", "Grep memory of all processes whose name contains this string.")
     30 + grepCmd.Flags().IntVarP(&flagDumpRadius, "dump-radius", "r", 2, "The number of lines of memory to dump both above and below each match.")
     31 + grepCmd.Flags().BoolVarP(&flagIncludeSelf, "self", "s", false, "Include results that are matched against the current process, or an ancestor of that process.")
     32 + rootCmd.AddCommand(grepCmd)
     33 +}
     34 + 
     35 +func grepHandler(cmd *cobra.Command, args []string) error {
     36 + 
     37 + var processes []proc.Process
     38 + 
     39 + if flagPID == 0 {
     40 + var err error
     41 + processes, err = proc.List(false)
     42 + if err != nil {
     43 + return err
     44 + }
     45 + } else {
     46 + processes = []proc.Process{proc.Process(flagPID)}
     47 + }
     48 + 
     49 + regex, err := regexp.Compile(args[0])
     50 + if err != nil {
     51 + return fmt.Errorf("invalid regex pattern: %w", err)
     52 + }
     53 + 
     54 + stdErr := cmd.ErrOrStderr()
     55 + _ = stdErr
     56 + stdOut := cmd.OutOrStdout()
     57 + 
     58 + var total int
     59 + for _, process := range processes {
     60 + if flagProcessName != "" {
     61 + status, err := process.Status()
     62 + if err != nil {
     63 + continue
     64 + }
     65 + if !strings.Contains(status.Name, flagProcessName) {
     66 + continue
     67 + }
     68 + }
     69 + if !flagIncludeSelf && process.IsInHierarchyOf(proc.Self()) {
     70 + continue
     71 + }
     72 + results, err := grepProcessMemory(process, regex)
     73 + if err != nil {
     74 + // TODO: add to debug log
     75 + //_, _ = fmt.Fprintf(stdErr, "failed to access memory for process %d: %s\n", process.PID(), err)
     76 + continue
     77 + }
     78 + for i, result := range results {
     79 + _, _ = fmt.Fprint(stdOut, summariseResult(total+i+1, result))
     80 + }
     81 + total += len(results)
     82 + }
     83 + if total == 0 {
     84 + _, _ = fmt.Fprintf(stdOut, "%sOperation Complete. No results found.%s\n\n", ansiRed, ansiReset)
     85 + } else {
     86 + _, _ = fmt.Fprintf(stdOut, "%sOperation Complete. %s%d%s%s results found.%s\n\n", ansiGreen, ansiBold, total, ansiReset, ansiGreen, ansiReset)
     87 + }
     88 + 
     89 + return nil
     90 +}
     91 + 
     92 +type GrepResult struct {
     93 + Pattern *regexp.Regexp
     94 + Process proc.Process
     95 + Map proc.Map
     96 + Address uint64
     97 + Match []byte
     98 +}
     99 + 
     100 +const (
     101 + ansiReset = "\x1b[0m"
     102 + ansiBold = "\x1b[1m"
     103 + ansiDim = "\x1b[2m"
     104 + ansiUnderline = "\x1b[4m"
     105 + ansiItalic = "\x1b[3m"
     106 + ansiRed = "\x1b[31m"
     107 + ansiGreen = "\x1b[32m"
     108 +)
     109 + 
     110 +func summariseResult(number int, g GrepResult) string {
     111 + 
     112 + buffer := bytes.NewBuffer(nil)
     113 + 
     114 + _, _ = fmt.Fprintf(buffer, " %sMatch #%d%s\n\n", ansiUnderline, number, ansiReset)
     115 + _, _ = fmt.Fprintf(buffer, " %sMatched%s %s\n", ansiBold, ansiReset, string(g.Match))
     116 + _, _ = fmt.Fprintf(buffer, " %sPattern%s %s\n", ansiBold, ansiReset, g.Pattern.String())
     117 + _, _ = fmt.Fprintf(buffer, " %sProcess%s %s\n", ansiBold, ansiReset, g.Process.String())
     118 + _, _ = fmt.Fprintf(buffer, " %sAddress%s 0x%x %s\n\n", ansiBold, ansiReset, g.Address, g.Map.Path)
     119 + _, _ = fmt.Fprintf(buffer, " %sMemory Dump%s\n\n%s\n\n", ansiBold, ansiReset, hexDump(g))
     120 + 
     121 + return buffer.String()
     122 +}
     123 + 
     124 +func hexDump(g GrepResult) string {
     125 + 
     126 + buffer := bytes.NewBuffer(nil)
     127 + 
     128 + offset := g.Address - g.Map.Address
     129 + 
     130 + linesEitherSide := uint64(flagDumpRadius)
     131 + if linesEitherSide < 0 {
     132 + linesEitherSide = 0
     133 + }
     134 + 
     135 + start := ((offset / 16) * 16) - (16 * linesEitherSide)
     136 + size := (((uint64(len(g.Match)) + (16 * (2 * linesEitherSide))) / 16) + 1) * 16
     137 + 
     138 + data, err := g.Process.ReadMemory(g.Map, start, size)
     139 + if err != nil {
     140 + return fmt.Sprintf("dump not available: %s", err)
     141 + }
     142 + 
     143 + literalStartAddr := g.Map.Address + start
     144 + 
     145 + _, _ = fmt.Fprintf(buffer, " %s", ansiDim)
     146 + for i := 0; i < 0x10; i++ {
     147 + _, _ = fmt.Fprintf(buffer, "%02X ", i)
     148 + }
     149 + _, _ = fmt.Fprintln(buffer, ansiReset)
     150 + 
     151 + var ascii string
     152 + for index, b := range data {
     153 + 
     154 + localIndex := uint64(index) + start
     155 + inSecret := localIndex >= offset && localIndex < offset+uint64(len(g.Match))
     156 + 
     157 + if index%16 == 0 && index > 0 {
     158 + _, _ = fmt.Fprintf(buffer, " %s\n", ascii)
     159 + ascii = ""
     160 + }
     161 + if index%16 == 0 {
     162 + _, _ = fmt.Fprintf(buffer, " %s%016x%s ", ansiDim, literalStartAddr+uint64(index/16), ansiReset)
     163 + }
     164 + 
     165 + if inSecret {
     166 + _, _ = fmt.Fprintf(buffer, "%s%s", ansiBold, ansiRed)
     167 + }
     168 + _, _ = fmt.Fprintf(buffer, "%02x%s ", b, ansiReset)
     169 + ascii += asciify(b, inSecret)
     170 + }
     171 + if ascii != "" {
     172 + _, _ = fmt.Fprintf(buffer, " %s\n", ascii)
     173 + }
     174 + 
     175 + return buffer.String()
     176 +}
     177 + 
     178 +func asciify(b byte, hl bool) string {
     179 + if b < ' ' || b > '~' {
     180 + b = '.'
     181 + }
     182 + if !hl {
     183 + return fmt.Sprintf("%s%c%s", ansiDim, b, ansiReset)
     184 + }
     185 + return fmt.Sprintf("%s%s%c%s", ansiBold, ansiRed, b, ansiReset)
     186 +}
     187 + 
     188 +func grepProcessMemory(p proc.Process, regex *regexp.Regexp) ([]GrepResult, error) {
     189 + var results []GrepResult
     190 + maps, err := p.Maps()
     191 + if err != nil {
     192 + return nil, err
     193 + }
     194 + for _, map_ := range maps {
     195 + if !map_.Permissions.Readable {
     196 + continue
     197 + }
     198 + memory, err := p.ReadMemory(map_, 0, 0)
     199 + if err != nil {
     200 + continue
     201 + }
     202 + for _, matches := range regex.FindAllIndex(memory, -1) {
     203 + results = append(results, GrepResult{
     204 + Process: p,
     205 + Map: map_,
     206 + Address: map_.Address + uint64(matches[0]),
     207 + Match: shrinkMatch(memory[matches[0]:matches[1]]),
     208 + Pattern: regex,
     209 + })
     210 + }
     211 + }
     212 + return results, nil
     213 +}
     214 + 
     215 +func shrinkMatch(match []byte) []byte {
     216 + return bytes.Split(match, []byte{0x00})[0]
     217 +}
     218 + 
  • ■ ■ ■ ■ ■ ■
    internal/cmd/list.go
     1 +package cmd
     2 + 
     3 +import (
     4 + "fmt"
     5 + 
     6 + "github.com/liamg/dismember/pkg/proc"
     7 + "github.com/spf13/cobra"
     8 +)
     9 + 
     10 +func init() {
     11 + rootCmd.AddCommand(&cobra.Command{
     12 + Use: "list",
     13 + Short: "List all processes currently available on the system",
     14 + Long: ``,
     15 + RunE: listHandler,
     16 + })
     17 +}
     18 + 
     19 +func listHandler(cmd *cobra.Command, args []string) error {
     20 + 
     21 + processes, err := proc.List(true)
     22 + if err != nil {
     23 + return err
     24 + }
     25 + 
     26 + stdErr := cmd.ErrOrStderr()
     27 + stdOut := cmd.OutOrStdout()
     28 + 
     29 + for _, process := range processes {
     30 + status, err := process.Status()
     31 + if err != nil {
     32 + _, _ = fmt.Fprintf(stdErr, "failed to read status for process %d: %s\n", process.PID(), err)
     33 + continue
     34 + }
     35 + _, _ = fmt.Fprintf(stdOut, "% -10d %s\n", process.PID(), status.Name)
     36 + }
     37 + 
     38 + return nil
     39 +}
     40 + 
  • ■ ■ ■ ■ ■ ■
    internal/cmd/root.go
     1 +package cmd
     2 + 
     3 +import (
     4 + "github.com/spf13/cobra"
     5 +)
     6 + 
     7 +var rootCmd = &cobra.Command{
     8 + Use: "dismember",
     9 + Short: "",
     10 + Long: ``,
     11 + SilenceErrors: true,
     12 + PersistentPreRun: func(cmd *cobra.Command, args []string) {
     13 + cmd.SilenceUsage = true
     14 + },
     15 +}
     16 + 
     17 +func Execute() error {
     18 + return rootCmd.Execute()
     19 +}
     20 + 
  • ■ ■ ■ ■ ■ ■
    internal/cmd/status.go
     1 +package cmd
     2 + 
     3 +import (
     4 + "fmt"
     5 + "io"
     6 + "strconv"
     7 + 
     8 + "github.com/liamg/dismember/pkg/proc"
     9 + "github.com/spf13/cobra"
     10 +)
     11 + 
     12 +func init() {
     13 + rootCmd.AddCommand(&cobra.Command{
     14 + Use: "status [pid]",
     15 + Short: "Show information about the status of a process",
     16 + Long: ``,
     17 + RunE: statusHandler,
     18 + Args: cobra.ExactArgs(1),
     19 + })
     20 +}
     21 + 
     22 +func statusHandler(cmd *cobra.Command, args []string) error {
     23 + 
     24 + pid, err := strconv.Atoi(args[0])
     25 + if err != nil {
     26 + return fmt.Errorf("invalid pid specified: '%s': %w", args[0], err)
     27 + }
     28 + 
     29 + process := proc.Process(pid)
     30 + 
     31 + status, err := process.Status()
     32 + if err != nil {
     33 + return fmt.Errorf("failed to read status for process %d: %w\n", process.PID(), err)
     34 + }
     35 + 
     36 + stdOut := cmd.OutOrStdout()
     37 + 
     38 + printKeyVal(stdOut, "PID", strconv.Itoa(int(process.PID())))
     39 + printKeyVal(stdOut, "Name", status.Name)
     40 + if status.Parent != 0 {
     41 + printKeyVal(stdOut, "Parent", status.Parent.String())
     42 + } else {
     43 + printKeyVal(stdOut, "Parent", "-")
     44 + }
     45 + 
     46 + return nil
     47 +}
     48 + 
     49 +func printKeyVal(w io.Writer, key string, value string) {
     50 + _, _ = fmt.Fprintf(w, "%-20s %s\n", key, value)
     51 +}
     52 + 
  • ■ ■ ■ ■ ■ ■
    main.go
     1 +package main
     2 + 
     3 +import (
     4 + "fmt"
     5 + "os"
     6 + 
     7 + "github.com/liamg/dismember/internal/cmd"
     8 +)
     9 + 
     10 +func main() {
     11 + if err := cmd.Execute(); err != nil {
     12 + _, _ = fmt.Fprintf(os.Stderr, "Error: %s\n", err)
     13 + os.Exit(1)
     14 + }
     15 +}
     16 + 
  • ■ ■ ■ ■ ■ ■
    pkg/proc/map.go
     1 +package proc
     2 + 
     3 +import (
     4 + "fmt"
     5 + "strconv"
     6 + "strings"
     7 +)
     8 + 
     9 +// see https://man7.org/linux/man-pages/man5/proc.5.html
     10 +//
     11 +// Example:
     12 +// address perms offset dev inode pathname
     13 +// 08048000-08056000 r-xp 00000000 03:0c 64593 /usr/sbin/gpm
     14 + 
     15 +type Maps []Map
     16 + 
     17 +type Map struct {
     18 + Address uint64
     19 + Size uint64
     20 + Permissions MemPerms
     21 + Offset uint64
     22 + Device uint64 // see man makedev
     23 + Inode uint64
     24 + Path string
     25 +}
     26 + 
     27 +type MemPerms struct {
     28 + Readable bool
     29 + Writable bool
     30 + Executable bool
     31 + Shared bool
     32 +}
     33 + 
     34 +func (p *Process) Maps() (Maps, error) {
     35 + data, err := p.readFile("maps")
     36 + if err != nil {
     37 + return nil, err
     38 + }
     39 + return parseMaps(data)
     40 +}
     41 + 
     42 +func parseMaps(data []byte) (Maps, error) {
     43 + var maps Maps
     44 + for _, line := range strings.Split(string(data), "\n") {
     45 + fields := strings.Fields(line)
     46 + if len(fields) < 5 {
     47 + continue
     48 + }
     49 + if len(fields) == 5 {
     50 + fields = append(fields, "")
     51 + }
     52 + var m Map
     53 + start, end, err := parseAddressRange(fields[0])
     54 + if err != nil {
     55 + return nil, err
     56 + }
     57 + m.Address = start
     58 + m.Size = end - start
     59 + m.Permissions, err = parsePermissions(fields[1])
     60 + if err != nil {
     61 + return nil, err
     62 + }
     63 + m.Offset, err = parseUint64Hex(fields[2])
     64 + if err != nil {
     65 + return nil, err
     66 + }
     67 + m.Device, err = parseMkdev(fields[3])
     68 + if err != nil {
     69 + return nil, err
     70 + }
     71 + m.Inode, err = parseUint64Dec(fields[4])
     72 + if err != nil {
     73 + return nil, err
     74 + }
     75 + m.Path = fields[5]
     76 + maps = append(maps, m)
     77 + }
     78 + return maps, nil
     79 +}
     80 + 
     81 +func parsePermissions(s string) (MemPerms, error) {
     82 + var perms MemPerms
     83 + if len(s) != 4 {
     84 + return perms, fmt.Errorf("invalid permissions: %s", s)
     85 + }
     86 + perms.Readable = s[0] == 'r'
     87 + perms.Writable = s[1] == 'w'
     88 + perms.Executable = s[2] == 'x'
     89 + perms.Shared = s[3] == 's'
     90 + return perms, nil
     91 +}
     92 + 
     93 +func parseAddressRange(input string) (uint64, uint64, error) {
     94 + fields := strings.Split(input, "-")
     95 + if len(fields) != 2 {
     96 + return 0, 0, fmt.Errorf("invalid address range: %s", input)
     97 + }
     98 + start, err := parseUint64Hex(fields[0])
     99 + if err != nil {
     100 + return 0, 0, fmt.Errorf("invalid start address: %s: %w", fields[0], err)
     101 + }
     102 + end, err := parseUint64Hex(fields[1])
     103 + if err != nil {
     104 + return 0, 0, fmt.Errorf("invalid start address: %s: %w", fields[0], err)
     105 + }
     106 + return start, end, nil
     107 +}
     108 + 
     109 +func parseUint64Hex(input string) (uint64, error) {
     110 + return strconv.ParseUint(input, 16, 64)
     111 +}
     112 + 
     113 +func parseUint64Dec(input string) (uint64, error) {
     114 + return strconv.ParseUint(input, 10, 64)
     115 +}
     116 + 
     117 +func parseMkdev(input string) (uint64, error) {
     118 + major, minor, ok := strings.Cut(input, ":")
     119 + if !ok {
     120 + return parseUint64Hex(input)
     121 + }
     122 + ma, err := parseUint64Hex(major)
     123 + if err != nil {
     124 + return 0, err
     125 + }
     126 + mi, err := parseUint64Hex(minor)
     127 + if err != nil {
     128 + return 0, err
     129 + }
     130 + return (ma << 32) | mi, nil
     131 +}
     132 + 
  • ■ ■ ■ ■ ■ ■
    pkg/proc/map_test.go
     1 +package proc
     2 + 
     3 +import (
     4 + "testing"
     5 + 
     6 + "github.com/stretchr/testify/assert"
     7 + "github.com/stretchr/testify/require"
     8 +)
     9 + 
     10 +func Test_Map(t *testing.T) {
     11 + tests := []struct {
     12 + name string
     13 + input string
     14 + want Maps
     15 + wantErr bool
     16 + }{
     17 + {
     18 + input: `
     19 +55d38ffaf000-55d38ffb1000 r--p 00000000 103:03 16528031 /usr/bin/cat
     20 +55d38ffb1000-55d38ffb5000 r-xp 00002000 103:03 16528031 /usr/bin/cat
     21 +55d391c13000-55d391c34000 rw-p 00000000 00:00 0 [heap]
     22 +7fb79f1ff000-7fb79f22e000 rw-p 00000000 00:00 0
     23 +ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]
     24 +`,
     25 + want: Maps{
     26 + {
     27 + Address: 0x55d38ffaf000,
     28 + Size: 0x55d38ffb1000 - 0x55d38ffaf000,
     29 + Permissions: MemPerms{
     30 + Readable: true,
     31 + Writable: false,
     32 + Executable: false,
     33 + Shared: false,
     34 + },
     35 + Offset: 0,
     36 + Device: (259 << 32) | 3,
     37 + Inode: 16528031,
     38 + Path: "/usr/bin/cat",
     39 + },
     40 + {
     41 + Address: 0x55d38ffb1000,
     42 + Size: 0x55d38ffb5000 - 0x55d38ffb1000,
     43 + Permissions: MemPerms{
     44 + Readable: true,
     45 + Writable: false,
     46 + Executable: true,
     47 + Shared: false,
     48 + },
     49 + Offset: 0x2000,
     50 + Device: (259 << 32) | 3,
     51 + Inode: 16528031,
     52 + Path: "/usr/bin/cat",
     53 + },
     54 + {
     55 + Address: 0x55d391c13000,
     56 + Size: 0x55d391c34000 - 0x55d391c13000,
     57 + Permissions: MemPerms{
     58 + Readable: true,
     59 + Writable: true,
     60 + Executable: false,
     61 + Shared: false,
     62 + },
     63 + Offset: 0,
     64 + Device: 0,
     65 + Inode: 0,
     66 + Path: "[heap]",
     67 + },
     68 + {
     69 + Address: 0x7fb79f1ff000,
     70 + Size: 0x7fb79f22e000 - 0x7fb79f1ff000,
     71 + Permissions: MemPerms{
     72 + Readable: true,
     73 + Writable: true,
     74 + Executable: false,
     75 + Shared: false,
     76 + },
     77 + Offset: 0,
     78 + Device: 0,
     79 + Inode: 0,
     80 + Path: "",
     81 + },
     82 + {
     83 + Address: 0xffffffffff600000,
     84 + Size: 0xffffffffff601000 - 0xffffffffff600000,
     85 + Permissions: MemPerms{
     86 + Readable: false,
     87 + Writable: false,
     88 + Executable: true,
     89 + Shared: false,
     90 + },
     91 + Offset: 0,
     92 + Device: 0,
     93 + Inode: 0,
     94 + Path: "[vsyscall]",
     95 + },
     96 + },
     97 + },
     98 + }
     99 + 
     100 + for _, test := range tests {
     101 + t.Run(test.name, func(t *testing.T) {
     102 + got, err := parseMaps([]byte(test.input))
     103 + if test.wantErr {
     104 + require.Error(t, err)
     105 + return
     106 + }
     107 + require.NoError(t, err)
     108 + assert.Equal(t, test.want, got)
     109 + })
     110 + }
     111 +}
     112 + 
  • ■ ■ ■ ■ ■ ■
    pkg/proc/mem.go
     1 +package proc
     2 + 
     3 +func (p *Process) ReadMemory(m Map, offset uint64, size uint64) ([]byte, error) {
     4 + f, err := p.openFile("mem")
     5 + if err != nil {
     6 + return nil, err
     7 + }
     8 + defer func() { _ = f.Close() }()
     9 + 
     10 + if _, err := f.Seek(int64(m.Address+offset), 0); err != nil {
     11 + return nil, err
     12 + }
     13 + 
     14 + if size == 0 {
     15 + size = m.Size
     16 + }
     17 + 
     18 + data := make([]byte, size)
     19 + if _, err := f.Read(data); err != nil {
     20 + return nil, err
     21 + }
     22 + 
     23 + return data, nil
     24 +}
     25 + 
  • ■ ■ ■ ■ ■ ■
    pkg/proc/process.go
     1 +package proc
     2 + 
     3 +import (
     4 + "fmt"
     5 + "os"
     6 + "path/filepath"
     7 + "regexp"
     8 + "strconv"
     9 +)
     10 + 
     11 +type Process uint64
     12 + 
     13 +const (
     14 + NoProcess Process = 0
     15 +)
     16 + 
     17 +func (p *Process) PID() uint64 {
     18 + return uint64(*p)
     19 +}
     20 + 
     21 +func Self() Process {
     22 + return Process(os.Getpid())
     23 +}
     24 + 
     25 +func (p *Process) Name() string {
     26 + status, err := p.Status()
     27 + if err != nil || status.Name == "" {
     28 + return "unknown"
     29 + }
     30 + return status.Name
     31 +}
     32 + 
     33 +func (p *Process) String() string {
     34 + return fmt.Sprintf("%d (%s)", p.PID(), p.Name())
     35 +}
     36 + 
     37 +var pidRegex = regexp.MustCompile(`^\d+$`)
     38 + 
     39 +func List(includeSelf bool) ([]Process, error) {
     40 + 
     41 + self := os.Getpid()
     42 + 
     43 + var results []Process
     44 + entries, err := os.ReadDir("/proc")
     45 + if err != nil {
     46 + return nil, err
     47 + }
     48 + for _, entry := range entries {
     49 + if !pidRegex.MatchString(entry.Name()) {
     50 + continue
     51 + }
     52 + pid, err := strconv.Atoi(entry.Name())
     53 + if err != nil {
     54 + continue
     55 + }
     56 + if pid == self && !includeSelf {
     57 + continue
     58 + }
     59 + results = append(results, Process(pid))
     60 + }
     61 + return results, nil
     62 +}
     63 + 
     64 +func (p *Process) readFile(path ...string) ([]byte, error) {
     65 + final := filepath.Join(append([]string{"/proc", strconv.Itoa(int(p.PID()))}, path...)...)
     66 + return os.ReadFile(final)
     67 +}
     68 + 
     69 +func (p *Process) openFile(path ...string) (*os.File, error) {
     70 + final := filepath.Join(append([]string{"/proc", strconv.Itoa(int(p.PID()))}, path...)...)
     71 + return os.Open(final)
     72 +}
     73 + 
  • ■ ■ ■ ■ ■ ■
    pkg/proc/status.go
     1 +package proc
     2 + 
     3 +import (
     4 + "fmt"
     5 + "reflect"
     6 + "strconv"
     7 + "strings"
     8 +)
     9 + 
     10 +type Status struct {
     11 + Name string `proc:"Name"`
     12 + Parent Process `proc:"PPid"`
     13 +}
     14 + 
     15 +func (p *Process) Status() (*Status, error) {
     16 + data, err := p.readFile("status")
     17 + if err != nil {
     18 + return nil, err
     19 + }
     20 + return parseStatus(data)
     21 +}
     22 + 
     23 +func (p *Process) IsInHierarchyOf(other Process) bool {
     24 + for other != NoProcess {
     25 + if other == *p {
     26 + return true
     27 + }
     28 + stat, err := other.Status()
     29 + if err != nil {
     30 + return false
     31 + }
     32 + other = stat.Parent
     33 + }
     34 + return false
     35 +}
     36 + 
     37 +func parseStatus(data []byte) (*Status, error) {
     38 + var status Status
     39 + 
     40 + values := make(map[string]string)
     41 + for _, line := range strings.Split(string(data), "\n") {
     42 + key, val, ok := strings.Cut(line, ":")
     43 + if !ok {
     44 + continue
     45 + }
     46 + val = strings.TrimSpace(val)
     47 + values[key] = val
     48 + }
     49 + 
     50 + v := reflect.ValueOf(&status)
     51 + 
     52 + t := v.Elem().Type()
     53 + for i := 0; i < t.NumField(); i++ {
     54 + fv := t.Field(i)
     55 + tags := strings.Split(fv.Tag.Get("proc"), ",")
     56 + tagName := fv.Name
     57 + if len(tags) > 0 {
     58 + tagName = tags[0]
     59 + }
     60 + value, ok := values[tagName]
     61 + if !ok {
     62 + continue
     63 + }
     64 + subject := v.Elem().Field(i)
     65 + 
     66 + if !v.Elem().CanSet() {
     67 + return nil, fmt.Errorf("target is not settable")
     68 + }
     69 + 
     70 + switch subject.Kind() {
     71 + case reflect.String:
     72 + subject.SetString(value)
     73 + case reflect.Uint64:
     74 + u, err := strconv.Atoi(value)
     75 + if err != nil {
     76 + return nil, err
     77 + }
     78 + subject.SetUint(uint64(u))
     79 + default:
     80 + return nil, fmt.Errorf("decoding of kind %s is not supported", subject.Kind())
     81 + }
     82 + }
     83 + 
     84 + return &status, nil
     85 +}
     86 + 
  • ■ ■ ■ ■ ■ ■
    pkg/proc/status_test.go
     1 +package proc
     2 + 
     3 +import (
     4 + "testing"
     5 + 
     6 + "github.com/stretchr/testify/assert"
     7 + "github.com/stretchr/testify/require"
     8 +)
     9 + 
     10 +func Test_Status(t *testing.T) {
     11 + tests := []struct {
     12 + name string
     13 + input string
     14 + want Status
     15 + wantErr bool
     16 + }{
     17 + {
     18 + input: `
     19 +Name: cat
     20 +Umask: 0022
     21 +State: R (running)
     22 +Tgid: 131533
     23 +Ngid: 0
     24 +Pid: 131533
     25 +PPid: 118575
     26 +TracerPid: 0
     27 +Uid: 0 0 0 0
     28 +Gid: 0 0 0 0
     29 +FDSize: 256
     30 +Groups: 0
     31 +NStgid: 131533
     32 +NSpid: 131533
     33 +NSpgid: 131533
     34 +NSsid: 118559
     35 +VmPeak: 5788 kB
     36 +VmSize: 5788 kB
     37 +VmLck: 0 kB
     38 +VmPin: 0 kB
     39 +VmHWM: 976 kB
     40 +VmRSS: 976 kB
     41 +RssAnon: 88 kB
     42 +RssFile: 888 kB
     43 +RssShmem: 0 kB
     44 +VmData: 360 kB
     45 +VmStk: 132 kB
     46 +VmExe: 16 kB
     47 +VmLib: 1668 kB
     48 +VmPTE: 48 kB
     49 +VmSwap: 0 kB
     50 +HugetlbPages: 0 kB
     51 +CoreDumping: 0
     52 +THP_enabled: 1
     53 +Threads: 1
     54 +SigQ: 1/127176
     55 +SigPnd: 0000000000000000
     56 +ShdPnd: 0000000000000000
     57 +SigBlk: 0000000000000000
     58 +SigIgn: 0000000000000000
     59 +SigCgt: 0000000000000000
     60 +CapInh: 0000000000000000
     61 +CapPrm: 000001ffffffffff
     62 +CapEff: 000001ffffffffff
     63 +CapBnd: 000001ffffffffff
     64 +CapAmb: 0000000000000000
     65 +NoNewPrivs: 0
     66 +Seccomp: 0
     67 +Seccomp_filters: 0
     68 +Speculation_Store_Bypass: thread vulnerable
     69 +SpeculationIndirectBranch: conditional enabled
     70 +Cpus_allowed: ff
     71 +Cpus_allowed_list: 0-7
     72 +Mems_allowed: 00000001
     73 +Mems_allowed_list: 0
     74 +voluntary_ctxt_switches: 1
     75 +nonvoluntary_ctxt_switches: 0
     76 +`,
     77 + want: Status{
     78 + Name: "cat",
     79 + Parent: Process(118575),
     80 + },
     81 + },
     82 + }
     83 + 
     84 + for _, test := range tests {
     85 + t.Run(test.name, func(t *testing.T) {
     86 + got, err := parseStatus([]byte(test.input))
     87 + if test.wantErr {
     88 + require.Error(t, err)
     89 + return
     90 + }
     91 + require.NoError(t, err)
     92 + assert.Equal(t, test.want, *got)
     93 + })
     94 + }
     95 +}
     96 + 
Please wait...
Page is in error, reload to recover