Skip to content

Commit cd423a9

Browse files
Added support to read TLS traffic in go binaries (#234)
Co-authored-by: Ori Shavit <[email protected]>
1 parent a10cd0b commit cd423a9

File tree

21 files changed

+1388
-25
lines changed

21 files changed

+1388
-25
lines changed

build/bpfman.Dockerfile

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
FROM --platform=$TARGETPLATFORM golang:1.22.1 AS ebpf-buildenv
2+
3+
WORKDIR /src
4+
COPY go.mod go.sum ./
5+
6+
RUN apt-get update
7+
RUN apt-get install -y clang llvm libelf-dev libbpf-dev linux-headers-generic
8+
RUN ln -sf /usr/include/$(uname -m)-linux-gnu/asm /usr/include/asm
9+
RUN go mod download
10+
11+
COPY ebpf/ ./ebpf/
12+
13+
# Generate ebpf code
14+
ARG TARGETARCH
15+
RUN GOARCH=$TARGETARCH go generate -tags ebpf ./ebpf/...
16+
17+
FROM quay.io/bpfman/bpfman AS bpfman
18+
COPY --from=ebpf-buildenv /src/ebpf/ /otterize/ebpf/
19+
20+
ENTRYPOINT ["./bpfman-rpc", "--timeout=0"]

src/bintools/bininfo/arch.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package bininfo
2+
3+
import (
4+
"debug/elf"
5+
"errors"
6+
)
7+
8+
// ErrUnsupportedArch is returned when an architecture given as a parameter is not supported.
9+
var ErrUnsupportedArch = errors.New("got unsupported arch")
10+
11+
// Arch only includes go architectures that we support in the ebpf code.
12+
type Arch string
13+
14+
const (
15+
ArchX86_64 Arch = "amd64"
16+
ArchARM64 Arch = "arm64"
17+
)
18+
19+
// GetArchitecture returns supported Go architecture for the given ELF file.
20+
func GetArchitecture(elfFile *elf.File) (Arch, error) {
21+
switch elfFile.FileHeader.Machine {
22+
case elf.EM_X86_64:
23+
return ArchX86_64, nil
24+
case elf.EM_AARCH64:
25+
return ArchARM64, nil
26+
}
27+
28+
return "", ErrUnsupportedArch
29+
}

src/bintools/bininfo/lang.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package bininfo
2+
3+
import (
4+
"debug/buildinfo"
5+
"os"
6+
"path/filepath"
7+
"strings"
8+
)
9+
10+
type SourceLanguage string
11+
12+
const (
13+
GoLang SourceLanguage = "golang"
14+
Python SourceLanguage = "python"
15+
Binary SourceLanguage = "binary"
16+
)
17+
18+
func GetSourceLanguage(path string, f *os.File) SourceLanguage {
19+
if isGoBinary(f) {
20+
return GoLang
21+
}
22+
if isPythonCommand(path) {
23+
return Python
24+
25+
}
26+
27+
return Binary
28+
}
29+
30+
func isGoBinary(f *os.File) bool {
31+
_, err := buildinfo.Read(f)
32+
return err == nil
33+
}
34+
35+
func isPythonCommand(path string) bool {
36+
// Resolve the symlink to the actual executable path
37+
actualPath, err := os.Readlink(path)
38+
if err != nil {
39+
return false
40+
}
41+
42+
// Check if the actual path is a python binary - path will be something along /usr/bin/python3
43+
_, file := filepath.Split(actualPath)
44+
return strings.Contains(file, "python")
45+
}

src/bintools/elf.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Some functionality was copied from the Datadog-agent repository.
2+
// Source: https://github.com/DataDog/datadog-agent
3+
// Original license applies: Apache License Version 2.0.
4+
5+
package bintools
6+
7+
import (
8+
"debug/elf"
9+
"errors"
10+
"fmt"
11+
"github.com/otterize/network-mapper/src/bintools/bininfo"
12+
)
13+
14+
// GetELFSymbolsByName retrieves ELF symbols by name and returns them in a map.
15+
func GetELFSymbolsByName(elfFile *elf.File) (map[string]elf.Symbol, error) {
16+
symbolMap := make(map[string]elf.Symbol)
17+
18+
// Retrieve symbols from the .symtab section (static symbols)
19+
if symbols, err := elfFile.Symbols(); err == nil {
20+
for _, sym := range symbols {
21+
symbolMap[sym.Name] = sym
22+
}
23+
}
24+
25+
// Retrieve symbols from the .dynsym section (dynamic symbols)
26+
if dynSymbols, err := elfFile.DynamicSymbols(); err == nil {
27+
for _, sym := range dynSymbols {
28+
symbolMap[sym.Name] = sym
29+
}
30+
}
31+
32+
return symbolMap, nil
33+
}
34+
35+
// SymbolToOffset returns the offset of the given symbol name in the given elf file.
36+
func SymbolToOffset(f *elf.File, symbol elf.Symbol) (uint32, error) {
37+
if f == nil {
38+
return 0, errors.New("got nil elf file")
39+
}
40+
41+
var sectionsToSearchForSymbol []*elf.Section
42+
43+
for i := range f.Sections {
44+
if f.Sections[i].Flags == elf.SHF_ALLOC+elf.SHF_EXECINSTR {
45+
sectionsToSearchForSymbol = append(sectionsToSearchForSymbol, f.Sections[i])
46+
}
47+
}
48+
49+
if len(sectionsToSearchForSymbol) == 0 {
50+
return 0, fmt.Errorf("symbol %q not found in file - no sections to search", symbol)
51+
}
52+
53+
var executableSection *elf.Section
54+
55+
// Find what section the symbol is in by checking the executable section's addr space.
56+
for m := range sectionsToSearchForSymbol {
57+
sectionStart := sectionsToSearchForSymbol[m].Addr
58+
sectionEnd := sectionStart + sectionsToSearchForSymbol[m].Size
59+
if symbol.Value >= sectionStart && symbol.Value < sectionEnd {
60+
executableSection = sectionsToSearchForSymbol[m]
61+
break
62+
}
63+
}
64+
65+
if executableSection == nil {
66+
return 0, errors.New("could not find symbol in executable sections of binary")
67+
}
68+
69+
// Calculate the Address of the symbol in the executable section
70+
return uint32(symbol.Value - executableSection.Addr + executableSection.Offset), nil
71+
}
72+
73+
// FindReturnLocations returns the offsets of all the returns of the given func (sym) with the given offset.
74+
func FindReturnLocations(elfFile *elf.File, arch bininfo.Arch, sym elf.Symbol, functionOffset uint64) ([]uint64, error) {
75+
textSection := elfFile.Section(".text")
76+
if textSection == nil {
77+
return nil, fmt.Errorf("no %q section found in binary file", ".text")
78+
}
79+
80+
switch arch {
81+
case bininfo.ArchX86_64:
82+
return ScanFunction(textSection, sym, functionOffset, FindX86_64ReturnInstructions)
83+
case bininfo.ArchARM64:
84+
return ScanFunction(textSection, sym, functionOffset, FindARM64ReturnInstructions)
85+
default:
86+
return nil, bininfo.ErrUnsupportedArch
87+
}
88+
}

src/bintools/gobin/gobin.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package gobin
2+
3+
import (
4+
"debug/buildinfo"
5+
"fmt"
6+
delve "github.com/go-delve/delve/pkg/goversion"
7+
"log"
8+
"os"
9+
)
10+
11+
// FindGoVersion attempts to determine the Go version
12+
func FindGoVersion(f *os.File) (gover delve.GoVersion, raw string, err error) {
13+
info, err := buildinfo.Read(f)
14+
if err != nil {
15+
log.Fatalf("error reading build info: %v", err)
16+
}
17+
18+
versionString := info.GoVersion
19+
gover, ok := delve.Parse(versionString)
20+
if !ok {
21+
return gover, "", fmt.Errorf("error parsing go version %s", versionString)
22+
}
23+
24+
return gover, versionString, nil
25+
}

src/bintools/gobin/types.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package gobin
2+
3+
import (
4+
"github.com/otterize/network-mapper/src/bintools/bininfo"
5+
)
6+
7+
// FunctionMetadata used to attach a uprobe to a function.
8+
type FunctionMetadata struct {
9+
EntryAddress uint64
10+
ReturnAddresses []uint64
11+
}
12+
13+
type GoBinaryInfo struct {
14+
Arch bininfo.Arch
15+
GoVersion string
16+
Functions map[string]FunctionMetadata
17+
}

src/bintools/process.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package bintools
2+
3+
import (
4+
"debug/elf"
5+
"fmt"
6+
delve "github.com/go-delve/delve/pkg/goversion"
7+
"github.com/otterize/network-mapper/src/bintools/bininfo"
8+
"github.com/otterize/network-mapper/src/bintools/gobin"
9+
"log"
10+
"os"
11+
)
12+
13+
func ProcessGoBinary(binPath string, functions []string) (res *gobin.GoBinaryInfo, err error) {
14+
f, err := os.Open(binPath)
15+
if err != nil {
16+
log.Fatalf("could not open file %s, %w", binPath, err)
17+
}
18+
defer func(f *os.File) {
19+
cErr := f.Close()
20+
if cErr == nil {
21+
err = cErr
22+
}
23+
}(f)
24+
25+
// Parse the ELF file
26+
elfFile, err := elf.NewFile(f)
27+
if err != nil {
28+
log.Fatalf("file %s could not be parsed as an ELF file: %w", binPath, err)
29+
}
30+
31+
// Determine the architecture of the binary
32+
arch, err := bininfo.GetArchitecture(elfFile)
33+
if err != nil {
34+
return nil, err
35+
}
36+
37+
// Determine the Go version
38+
goVersion, rawVersion, err := gobin.FindGoVersion(f)
39+
if err != nil {
40+
return nil, err
41+
}
42+
43+
// We only want to support Go versions 1.18 and above - where the ABI is register based
44+
if !goVersion.AfterOrEqual(delve.GoVersion{Major: 1, Minor: 18}) {
45+
return nil, fmt.Errorf("unsupported go version %s, only versions 1.18 and above are supported", rawVersion)
46+
}
47+
48+
// Get symbols
49+
symbols, err := GetELFSymbolsByName(elfFile)
50+
if err != nil {
51+
return nil, fmt.Errorf("could not get symbols from ELF file: %w", err)
52+
}
53+
54+
functionMetadata := make(map[string]gobin.FunctionMetadata, len(functions))
55+
56+
for _, funcName := range functions {
57+
offset, err := SymbolToOffset(elfFile, symbols[funcName])
58+
if err != nil {
59+
return nil, fmt.Errorf("could not find location for function %q: %w", funcName, err)
60+
}
61+
62+
// Find return locations
63+
var returnLocations []uint64
64+
symbol, ok := symbols[funcName]
65+
if !ok {
66+
return nil, fmt.Errorf("could not find function %q in symbols", funcName)
67+
}
68+
69+
locations, err := FindReturnLocations(elfFile, arch, symbol, uint64(offset))
70+
if err != nil {
71+
return nil, fmt.Errorf("could not find return locations for function %q: %w", funcName, err)
72+
}
73+
74+
returnLocations = locations
75+
76+
functionMetadata[funcName] = gobin.FunctionMetadata{
77+
EntryAddress: uint64(offset),
78+
ReturnAddresses: returnLocations,
79+
}
80+
}
81+
82+
return &gobin.GoBinaryInfo{
83+
Arch: arch,
84+
GoVersion: rawVersion,
85+
Functions: functionMetadata,
86+
}, nil
87+
}

0 commit comments

Comments
 (0)