Skip to content

Commit

Permalink
feat: create command hunt to trace multiple functions from different …
Browse files Browse the repository at this point in the history
…test binaries (#18)

feat: create command hunt to trace multiple functions from different test binaries

---------

Signed-off-by: Alessio Greggi <[email protected]>
  • Loading branch information
alegrey91 authored Jul 27, 2024
1 parent 04f4d6c commit 4c1050d
Show file tree
Hide file tree
Showing 12 changed files with 122,923 additions and 137,166 deletions.
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ build-go: create-bin-dir
export CURRENT_DIR=$(shell pwd); \
CC=gcc \
CGO_CFLAGS="-I $$CURRENT_DIR/libbpfgo/output" \
CGO_LDFLAGS="-lelf -lz $$CURRENT_DIR/libbpfgo/output/libbpf.a" \
CGO_LDFLAGS="-lelf -lz $$CURRENT_DIR/libbpfgo/output/libbpf/libbpf.a" \
go build \
-tags core,ebpf \
-v \
Expand All @@ -31,7 +31,7 @@ build: create-bin-dir vmlinux.h build-static-libbpfgo build-bpf
export CURRENT_DIR=$(shell pwd); \
CC=gcc \
CGO_CFLAGS="-I $$CURRENT_DIR/libbpfgo/output" \
CGO_LDFLAGS="-lelf -lz $$CURRENT_DIR/libbpfgo/output/libbpf.a" \
CGO_LDFLAGS="-lelf -lz $$CURRENT_DIR/libbpfgo/output/libbpf/libbpf.a" \
go build \
-tags core,ebpf \
-v \
Expand All @@ -46,7 +46,7 @@ endif
export CURRENT_DIR=$(shell pwd); \
CC=gcc \
CGO_CFLAGS="-I $$CURRENT_DIR/libbpfgo/output" \
CGO_LDFLAGS="-lelf -lz $$CURRENT_DIR/libbpfgo/output/libbpf.a" \
CGO_LDFLAGS="-lelf -lz $$CURRENT_DIR/libbpfgo/output/libbpf/libbpf.a" \
go build \
-tags core,ebpf \
-v \
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ getrlimit

These are the syscalls that have been executed by the traced function!

**N.B.** For a complete list of available command, take a look [here](docs/commands.md).

## Installation

To install `harpoon` you currently have 2 options:
Expand Down
4 changes: 2 additions & 2 deletions cmd/analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ var excludedPaths []string
var exclude string
var saveAnalysis bool

// captureCmd represents the create args
// analyzeCmd represents the create args
var analyzeCmd = &cobra.Command{
Use: "analyze",
Short: "Analyze infers the symbols of functions that are tested by unit-tests",
Expand Down Expand Up @@ -142,7 +142,7 @@ func init() {
rootCmd.AddCommand(analyzeCmd)

analyzeCmd.Flags().StringVarP(&exclude, "exclude", "e", "", "Skip directories specified in the comma separated list")
analyzeCmd.Flags().BoolVarP(&saveAnalysis, "save", "s", false, "Save analysis in a file")
analyzeCmd.Flags().BoolVarP(&saveAnalysis, "save", "s", false, "Save the result of analysis into a file")
}

func shouldSkipPath(path string) bool {
Expand Down
198 changes: 7 additions & 191 deletions cmd/capture.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,44 +16,16 @@ limitations under the License.
package cmd

import (
"bytes"
"encoding/binary"
"fmt"
"os"
"path"
"path/filepath"
"strings"
"sync"
"unsafe"

"github.com/alegrey91/harpoon/internal/archiver"
"github.com/alegrey91/harpoon/internal/elfreader"
embedded "github.com/alegrey91/harpoon/internal/embeddable"
"github.com/alegrey91/harpoon/internal/executor"
syscallsw "github.com/alegrey91/harpoon/internal/syscallswriter"
bpf "github.com/aquasecurity/libbpfgo"
"github.com/aquasecurity/libbpfgo/helpers"
"github.com/alegrey91/harpoon/internal/captor"
"github.com/spf13/cobra"
)

type event struct {
SyscallID uint32
}

var functionSymbols string
var commandOutput bool
var libbpfOutput bool
var save bool
var directory string

var bpfConfigMap = "config_map"
var bpfEventsMap = "events"
var uprobeEnterFunc = "enter_function"
var uprobeExitFunc = "exit_function"
var tracepointFunc = "trace_syscall"
var tracepointCategory = "raw_syscalls"
var tracepointName = "sys_enter"

// captureCmd represents the create args
var captureCmd = &cobra.Command{
Use: "capture",
Expand All @@ -63,169 +35,13 @@ by passing the function name symbol and the binary args.
`,
Example: " harpoon -f main.doSomething ./command arg1 arg2 ...",
Run: func(cmd *cobra.Command, args []string) {
functionSymbolList := strings.Split(functionSymbols, ",")

if !libbpfOutput {
// suppress libbpf log ouput
bpf.SetLoggerCbs(
bpf.Callbacks{
Log: func(level int, msg string) {
return
},
},
)
}

objectFile, err := embedded.BPFObject.ReadFile("output/ebpf.o")
bpfModule, err := bpf.NewModuleFromBuffer(objectFile, "ebpf.o")
if err != nil {
fmt.Printf("error loading BPF object file: %v\n", err)
os.Exit(-1)
}
defer bpfModule.Close()

/*
HashMap used for passing various configuration
from user-space to kernel-space.
*/
config, err := bpfModule.GetMap(bpfConfigMap)
if err != nil {
fmt.Printf("error retrieving map (%s) from BPF program: %v\n", bpfConfigMap, err)
os.Exit(-1)
}
enterFuncProbe, err := bpfModule.GetProgram(uprobeEnterFunc)
if err != nil {
fmt.Printf("error loading program (%s): %v\n", uprobeEnterFunc, err)
os.Exit(-1)
}
exitFuncProbe, err := bpfModule.GetProgram(uprobeExitFunc)
if err != nil {
fmt.Printf("error loading program (%s): %v\n", uprobeExitFunc, err)
os.Exit(-1)
}
traceFunction, err := bpfModule.GetProgram(tracepointFunc)
if err != nil {
fmt.Printf("error loading program (%s): %v\n", tracepointFunc, err)
os.Exit(-1)
}

bpfModule.BPFLoadObject()
enterLinks := make([]*bpf.BPFLink, len(functionSymbolList))
exitLinks := make([][]*bpf.BPFLink, len(functionSymbolList))
for id, functionSymbol := range functionSymbolList {
offset, err := helpers.SymbolToOffset(args[0], functionSymbol)
if err != nil {
fmt.Printf("error finding function (%s) offset: %v\n", functionSymbol, err)
os.Exit(-1)
}
enterLink, err := enterFuncProbe.AttachUprobe(-1, args[0], offset)
if err != nil {
fmt.Printf("error attaching uprobe at function (%s) offset: %d, error: %v\n", functionSymbol, offset, err)
os.Exit(-1)
}
enterLinks = append(enterLinks, enterLink)

/*
Since the uretprobes doesn't work well with Go binaries,
we are going to attach a uprobe ∀ RET instruction withing the
traced function.
*/
functionRetOffsets, err := elfreader.GetFunctionRetOffsets(args[0], functionSymbol)
for _, offsetRet := range functionRetOffsets {
exitLink, err := exitFuncProbe.AttachUprobe(-1, args[0], offset+uint32(offsetRet))
if err != nil {
fmt.Printf("error attaching uprobe at function (%s) RET: %d, error: %v\n", functionSymbol, offset+uint32(offsetRet), err)
os.Exit(-1)
}
exitLinks[id] = append(exitLinks[id], exitLink)
}
}

traceLink, err := traceFunction.AttachTracepoint(tracepointCategory, tracepointName)
if err != nil {
fmt.Printf("error attaching tracepoint at event (%s:%s): %v\n", tracepointCategory, tracepointName, err)
os.Exit(-1)
}
defer traceLink.Destroy()

/*
Sending input argument to BPF program
to instruct tracing specific args taken from cli.
*/
config_key_args := 0
baseargs := filepath.Base(args[0])
baseCmd := append([]byte(baseargs), 0)
err = config.Update(unsafe.Pointer(&config_key_args), unsafe.Pointer(&baseCmd[0]))
if err != nil {
fmt.Printf("error updating map (%s) with values %d / %s: %v\n", bpfConfigMap, config_key_args, baseargs, err)
os.Exit(-1)
}

// init perf buffer
eventsChannel := make(chan []byte)
lostChannel := make(chan uint64)
rb, err := bpfModule.InitPerfBuf(bpfEventsMap, eventsChannel, lostChannel, 1)
if err != nil {
fmt.Println("error initializing map (%s) with PerfBuffer: %v\n", bpfEventsMap, err)
os.Exit(-1)
}

// run args that we want to trace
var wg sync.WaitGroup
wg.Add(1)
go executor.Run(args, commandOutput, &wg)

var syscalls []uint32
go func() {
for {
select {
case data := <-eventsChannel:
var e event
err := binary.Read(bytes.NewBuffer(data), binary.LittleEndian, &e)
if err != nil {
return
}
syscalls = append(syscalls, e.SyscallID)
case lost := <-lostChannel:
fmt.Fprintf(os.Stderr, "lost %d data\n", lost)
return
}
}
}()

rb.Poll(300)
// wait for args completion
wg.Wait()
rb.Stop()

var errOut error
if save {
fileName := archiver.Convert(functionSymbolList[0])
err := os.Mkdir(directory, 0766)
if err != nil {
fmt.Printf("error creating directory: %v\n", err)
os.Exit(-1)
}
file, err := os.Create(path.Join(directory, fileName))
if err != nil {
fmt.Printf("error creating file %s: %v\n", file, err)
os.Exit(-1)
}
defer file.Close()

if err := file.Chmod(0744); err != nil {
fmt.Printf("error setting permissions to %s: %v\n", file, err)
}
// write to file
errOut = syscallsw.Print(file, syscalls)
} else {
// write to stdout
errOut = syscallsw.Print(os.Stdout, syscalls)
}
if errOut != nil {
fmt.Printf("error printing out system calls: %v\n", errOut)
os.Exit(-1)
opts := captor.CaptureOptions{
CommandOutput: commandOutput,
LibbpfOutput: libbpfOutput,
Save: save,
Directory: directory,
}
captor.Capture(functionSymbols, args, opts)
},
}

Expand Down
96 changes: 96 additions & 0 deletions cmd/hunt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
Copyright © 2024 Alessio Greggi
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd

import (
"fmt"
"io"
"os"
"strings"

"github.com/alegrey91/harpoon/internal/captor"
meta "github.com/alegrey91/harpoon/internal/metadata"
"github.com/spf13/cobra"
yaml "gopkg.in/yaml.v2"
)

var (
harpoonFile string
)

// huntCmd represents the create args
var huntCmd = &cobra.Command{
Use: "hunt",
Short: "Hunt is like capture but gets a list of functions to be traced",
Long: `
`,
Example: " harpoon hunt --file .harpoon.yaml",
Run: func(cmd *cobra.Command, args []string) {
file, err := os.Open(harpoonFile)
if err != nil {
fmt.Printf("failed to open %s: %v\n", harpoonFile, err)
return
}
defer file.Close()

byteValue, err := io.ReadAll(file)
if err != nil {
fmt.Printf("Failed to read file: %s", err)
}

// Unmarshal the JSON data into the struct
var analysisReport meta.SymbolsList
if err := yaml.Unmarshal(byteValue, &analysisReport); err != nil {
fmt.Printf("Failed to unmarshal YAML: %v", err)
}
//fmt.Println(analysisReport)

for _, symbolsOrigins := range analysisReport.SymbolsOrigins {
fmt.Println("test binary:", symbolsOrigins.TestBinaryPath)
fmt.Println("symbols:", symbolsOrigins.Symbols)

// command builder
var captureArgs []string
captureArgs = append(captureArgs, symbolsOrigins.TestBinaryPath)
functionSymbols := strings.Join(symbolsOrigins.Symbols, ",")
opts := captor.CaptureOptions{
CommandOutput: commandOutput,
LibbpfOutput: libbpfOutput,
Save: save,
Directory: directory,
}

captor.Capture(functionSymbols, captureArgs, opts)
}
},
}

func init() {
rootCmd.AddCommand(huntCmd)

huntCmd.Flags().StringVarP(&harpoonFile, "file", "F", ".harpoon.yaml", "File with the result of analysis")
huntCmd.MarkFlagRequired("file")

huntCmd.Flags().StringVarP(&functionSymbols, "functions", "f", "", "Name of the function symbols to be traced")

huntCmd.Flags().BoolVarP(&commandOutput, "include-cmd-output", "c", false, "Include the executed command output")

huntCmd.Flags().BoolVarP(&libbpfOutput, "include-libbpf-output", "l", false, "Include the libbpf output")

huntCmd.Flags().BoolVarP(&save, "save", "S", false, "Save output to a file")
huntCmd.Flags().StringVarP(&directory, "directory", "D", "", "Directory to use to store saved files")
huntCmd.MarkFlagsRequiredTogether("save", "directory")
}
Loading

0 comments on commit 4c1050d

Please sign in to comment.