Skip to content

Commit

Permalink
Added support to read TLS traffic in go binaries (#234)
Browse files Browse the repository at this point in the history
Co-authored-by: Ori Shavit <[email protected]>
  • Loading branch information
otterobert and orishavit authored Sep 1, 2024
1 parent a10cd0b commit cd423a9
Show file tree
Hide file tree
Showing 21 changed files with 1,388 additions and 25 deletions.
20 changes: 20 additions & 0 deletions build/bpfman.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
FROM --platform=$TARGETPLATFORM golang:1.22.1 AS ebpf-buildenv

WORKDIR /src
COPY go.mod go.sum ./

RUN apt-get update
RUN apt-get install -y clang llvm libelf-dev libbpf-dev linux-headers-generic
RUN ln -sf /usr/include/$(uname -m)-linux-gnu/asm /usr/include/asm
RUN go mod download

COPY ebpf/ ./ebpf/

# Generate ebpf code
ARG TARGETARCH
RUN GOARCH=$TARGETARCH go generate -tags ebpf ./ebpf/...

FROM quay.io/bpfman/bpfman AS bpfman
COPY --from=ebpf-buildenv /src/ebpf/ /otterize/ebpf/

ENTRYPOINT ["./bpfman-rpc", "--timeout=0"]
29 changes: 29 additions & 0 deletions src/bintools/bininfo/arch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package bininfo

import (
"debug/elf"
"errors"
)

// ErrUnsupportedArch is returned when an architecture given as a parameter is not supported.
var ErrUnsupportedArch = errors.New("got unsupported arch")

// Arch only includes go architectures that we support in the ebpf code.
type Arch string

const (
ArchX86_64 Arch = "amd64"
ArchARM64 Arch = "arm64"
)

// GetArchitecture returns supported Go architecture for the given ELF file.
func GetArchitecture(elfFile *elf.File) (Arch, error) {
switch elfFile.FileHeader.Machine {
case elf.EM_X86_64:
return ArchX86_64, nil
case elf.EM_AARCH64:
return ArchARM64, nil
}

return "", ErrUnsupportedArch
}
45 changes: 45 additions & 0 deletions src/bintools/bininfo/lang.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package bininfo

import (
"debug/buildinfo"
"os"
"path/filepath"
"strings"
)

type SourceLanguage string

const (
GoLang SourceLanguage = "golang"
Python SourceLanguage = "python"
Binary SourceLanguage = "binary"
)

func GetSourceLanguage(path string, f *os.File) SourceLanguage {
if isGoBinary(f) {
return GoLang
}
if isPythonCommand(path) {
return Python

}

return Binary
}

func isGoBinary(f *os.File) bool {
_, err := buildinfo.Read(f)
return err == nil
}

func isPythonCommand(path string) bool {
// Resolve the symlink to the actual executable path
actualPath, err := os.Readlink(path)
if err != nil {
return false
}

// Check if the actual path is a python binary - path will be something along /usr/bin/python3
_, file := filepath.Split(actualPath)
return strings.Contains(file, "python")
}
88 changes: 88 additions & 0 deletions src/bintools/elf.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Some functionality was copied from the Datadog-agent repository.
// Source: https://github.com/DataDog/datadog-agent
// Original license applies: Apache License Version 2.0.

package bintools

import (
"debug/elf"
"errors"
"fmt"
"github.com/otterize/network-mapper/src/bintools/bininfo"
)

// GetELFSymbolsByName retrieves ELF symbols by name and returns them in a map.
func GetELFSymbolsByName(elfFile *elf.File) (map[string]elf.Symbol, error) {
symbolMap := make(map[string]elf.Symbol)

// Retrieve symbols from the .symtab section (static symbols)
if symbols, err := elfFile.Symbols(); err == nil {
for _, sym := range symbols {
symbolMap[sym.Name] = sym
}
}

// Retrieve symbols from the .dynsym section (dynamic symbols)
if dynSymbols, err := elfFile.DynamicSymbols(); err == nil {
for _, sym := range dynSymbols {
symbolMap[sym.Name] = sym
}
}

return symbolMap, nil
}

// SymbolToOffset returns the offset of the given symbol name in the given elf file.
func SymbolToOffset(f *elf.File, symbol elf.Symbol) (uint32, error) {
if f == nil {
return 0, errors.New("got nil elf file")
}

var sectionsToSearchForSymbol []*elf.Section

for i := range f.Sections {
if f.Sections[i].Flags == elf.SHF_ALLOC+elf.SHF_EXECINSTR {
sectionsToSearchForSymbol = append(sectionsToSearchForSymbol, f.Sections[i])
}
}

if len(sectionsToSearchForSymbol) == 0 {
return 0, fmt.Errorf("symbol %q not found in file - no sections to search", symbol)
}

var executableSection *elf.Section

// Find what section the symbol is in by checking the executable section's addr space.
for m := range sectionsToSearchForSymbol {
sectionStart := sectionsToSearchForSymbol[m].Addr
sectionEnd := sectionStart + sectionsToSearchForSymbol[m].Size
if symbol.Value >= sectionStart && symbol.Value < sectionEnd {
executableSection = sectionsToSearchForSymbol[m]
break
}
}

if executableSection == nil {
return 0, errors.New("could not find symbol in executable sections of binary")
}

// Calculate the Address of the symbol in the executable section
return uint32(symbol.Value - executableSection.Addr + executableSection.Offset), nil
}

// FindReturnLocations returns the offsets of all the returns of the given func (sym) with the given offset.
func FindReturnLocations(elfFile *elf.File, arch bininfo.Arch, sym elf.Symbol, functionOffset uint64) ([]uint64, error) {
textSection := elfFile.Section(".text")
if textSection == nil {
return nil, fmt.Errorf("no %q section found in binary file", ".text")
}

switch arch {
case bininfo.ArchX86_64:
return ScanFunction(textSection, sym, functionOffset, FindX86_64ReturnInstructions)
case bininfo.ArchARM64:
return ScanFunction(textSection, sym, functionOffset, FindARM64ReturnInstructions)
default:
return nil, bininfo.ErrUnsupportedArch
}
}
25 changes: 25 additions & 0 deletions src/bintools/gobin/gobin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package gobin

import (
"debug/buildinfo"
"fmt"
delve "github.com/go-delve/delve/pkg/goversion"
"log"
"os"
)

// FindGoVersion attempts to determine the Go version
func FindGoVersion(f *os.File) (gover delve.GoVersion, raw string, err error) {
info, err := buildinfo.Read(f)
if err != nil {
log.Fatalf("error reading build info: %v", err)
}

versionString := info.GoVersion
gover, ok := delve.Parse(versionString)
if !ok {
return gover, "", fmt.Errorf("error parsing go version %s", versionString)
}

return gover, versionString, nil
}
17 changes: 17 additions & 0 deletions src/bintools/gobin/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package gobin

import (
"github.com/otterize/network-mapper/src/bintools/bininfo"
)

// FunctionMetadata used to attach a uprobe to a function.
type FunctionMetadata struct {
EntryAddress uint64
ReturnAddresses []uint64
}

type GoBinaryInfo struct {
Arch bininfo.Arch
GoVersion string
Functions map[string]FunctionMetadata
}
87 changes: 87 additions & 0 deletions src/bintools/process.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package bintools

import (
"debug/elf"
"fmt"
delve "github.com/go-delve/delve/pkg/goversion"
"github.com/otterize/network-mapper/src/bintools/bininfo"
"github.com/otterize/network-mapper/src/bintools/gobin"
"log"
"os"
)

func ProcessGoBinary(binPath string, functions []string) (res *gobin.GoBinaryInfo, err error) {
f, err := os.Open(binPath)
if err != nil {
log.Fatalf("could not open file %s, %w", binPath, err)
}
defer func(f *os.File) {
cErr := f.Close()
if cErr == nil {
err = cErr
}
}(f)

// Parse the ELF file
elfFile, err := elf.NewFile(f)
if err != nil {
log.Fatalf("file %s could not be parsed as an ELF file: %w", binPath, err)
}

// Determine the architecture of the binary
arch, err := bininfo.GetArchitecture(elfFile)
if err != nil {
return nil, err
}

// Determine the Go version
goVersion, rawVersion, err := gobin.FindGoVersion(f)
if err != nil {
return nil, err
}

// We only want to support Go versions 1.18 and above - where the ABI is register based
if !goVersion.AfterOrEqual(delve.GoVersion{Major: 1, Minor: 18}) {
return nil, fmt.Errorf("unsupported go version %s, only versions 1.18 and above are supported", rawVersion)
}

// Get symbols
symbols, err := GetELFSymbolsByName(elfFile)
if err != nil {
return nil, fmt.Errorf("could not get symbols from ELF file: %w", err)
}

functionMetadata := make(map[string]gobin.FunctionMetadata, len(functions))

for _, funcName := range functions {
offset, err := SymbolToOffset(elfFile, symbols[funcName])
if err != nil {
return nil, fmt.Errorf("could not find location for function %q: %w", funcName, err)
}

// Find return locations
var returnLocations []uint64
symbol, ok := symbols[funcName]
if !ok {
return nil, fmt.Errorf("could not find function %q in symbols", funcName)
}

locations, err := FindReturnLocations(elfFile, arch, symbol, uint64(offset))
if err != nil {
return nil, fmt.Errorf("could not find return locations for function %q: %w", funcName, err)
}

returnLocations = locations

functionMetadata[funcName] = gobin.FunctionMetadata{
EntryAddress: uint64(offset),
ReturnAddresses: returnLocations,
}
}

return &gobin.GoBinaryInfo{
Arch: arch,
GoVersion: rawVersion,
Functions: functionMetadata,
}, nil
}
Loading

0 comments on commit cd423a9

Please sign in to comment.