Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[agent-smith] Reduce cpu and memory consumption #10356

Merged
merged 1 commit into from
May 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions components/ee/agent-smith/cmd/signature-matches.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ var signatureMatchesCmd = &cobra.Command{
}
defer f.Close()

sfc := classifier.SignatureReadCache{
Reader: f,
}

if cfgFile == "" {
log.Info("no config present - reading signature from STDIN")
var sig classifier.Signature
Expand All @@ -36,7 +40,7 @@ var signatureMatchesCmd = &cobra.Command{
log.Fatal(err)
}

match, err := sig.Matches(f)
match, err := sig.Matches(&sfc)
if err != nil {
log.Fatal(err)
}
Expand All @@ -60,7 +64,7 @@ var signatureMatchesCmd = &cobra.Command{
var res []*classifier.Signature
for _, bl := range cfg.Blocklists.Levels() {
for _, s := range bl.Signatures {
m, err := s.Matches(f)
m, err := s.Matches(&sfc)
if err != nil {
log.WithError(err).WithField("signature", s.Name).Warn("cannot match signature")
continue
Expand Down
14 changes: 13 additions & 1 deletion components/ee/agent-smith/pkg/classifier/classifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package classifier
import (
"errors"
"fmt"
"io"
"os"
"regexp"
"strings"
Expand Down Expand Up @@ -196,8 +197,12 @@ func (sigcl *SignatureMatchClassifier) Matches(executable string, cmdline []stri
defer r.Close()

var serr error

src := SignatureReadCache{
Reader: r,
}
for _, sig := range sigcl.Signatures {
match, err := sig.Matches(r)
match, err := sig.Matches(&src)
if match {
sigcl.signatureHitTotal.Inc()
return &Classification{
Expand All @@ -217,6 +222,13 @@ func (sigcl *SignatureMatchClassifier) Matches(executable string, cmdline []stri
return sigNoMatch, nil
}

type SignatureReadCache struct {
Reader io.ReaderAt
header []byte
symbols []string
rodata []byte
}

func (sigcl *SignatureMatchClassifier) Describe(d chan<- *prometheus.Desc) {
sigcl.processMissTotal.Describe(d)
sigcl.signatureHitTotal.Describe(d)
Expand Down
12 changes: 10 additions & 2 deletions components/ee/agent-smith/pkg/classifier/signature_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ func TestMatchELF(t *testing.T) {
}
defer input.Close()

sfc := SignatureReadCache{
Reader: input,
}

sig := Signature{
Kind: ObjectELFSymbols,
Pattern: []byte("bash_groupname_completion_function"),
Expand All @@ -32,7 +36,7 @@ func TestMatchELF(t *testing.T) {
return
}

matches, err := sig.Matches(input)
matches, err := sig.Matches(&sfc)
if err != nil {
t.Errorf("cannot match signature: %v", err)
return
Expand Down Expand Up @@ -60,7 +64,11 @@ func TestMatchAny(t *testing.T) {
return
}

matches, err := test.Signature.matchAny(bytes.NewReader(test.Input))
sfc := SignatureReadCache{
Reader: bytes.NewReader(test.Input),
}

matches, err := test.Signature.matchAny(&sfc)
if err != nil {
t.Errorf("[%03d] cannot match signature: %v", i, err)
return
Expand Down
96 changes: 61 additions & 35 deletions components/ee/agent-smith/pkg/classifier/sinature.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,16 +103,16 @@ func (s *Signature) Validate() error {
}

// Matches checks if the signature applies to the stream
func (s *Signature) Matches(in io.ReaderAt) (bool, error) {
func (s *Signature) Matches(in *SignatureReadCache) (bool, error) {
if s.Slice.Start > 0 {
_, err := in.ReadAt([]byte{}, s.Slice.Start)
_, err := in.Reader.ReadAt([]byte{}, s.Slice.Start)
// slice start exceeds what we can read - this signature cannot match
if err != nil {
return false, nil
}
}
if s.Slice.End > 0 {
_, err := in.ReadAt([]byte{}, s.Slice.End)
_, err := in.Reader.ReadAt([]byte{}, s.Slice.End)
// slice start exceeds what we can read - this signature cannot match
if err != nil {
return false, nil
Expand All @@ -121,21 +121,27 @@ func (s *Signature) Matches(in io.ReaderAt) (bool, error) {

// check the object kind
if s.Kind != ObjectAny {
head := make([]byte, 261)
_, err := in.ReadAt(head, 0)
if err == io.EOF {
// cannot read header which means that only Any rules would apply
return false, nil
}
if err != nil {
return false, xerrors.Errorf("cannot read stream head: %w", err)
var head []byte
if len(in.header) > 0 {
head = in.header
Comment on lines +125 to +126
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a cache

} else {
head = make([]byte, 261)
_, err := in.Reader.ReadAt(head, 0)
if err == io.EOF {
// cannot read header which means that only Any rules would apply
return false, nil
}
if err != nil {
return false, xerrors.Errorf("cannot read stream head: %w", err)
}
in.header = head
Copy link
Contributor

@utam0k utam0k May 30, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cache at first.

}

matches := false
switch s.Kind {
case ObjectELFSymbols, ObjectELFRodata:
matches = isELF(head)
case ObjectAny:
default:
matches = true
}
if !matches {
Expand Down Expand Up @@ -172,16 +178,23 @@ func isELF(head []byte) bool {
}

// matchELF matches a signature against an ELF file
func (s *Signature) matchELFRodata(in io.ReaderAt) (bool, error) {
executable, err := elf.NewFile(in)
if err != nil {
return false, xerrors.Errorf("cannot anaylse ELF file: %w", err)
}
func (s *Signature) matchELFRodata(in *SignatureReadCache) (bool, error) {
var rodata []byte
if len(in.rodata) > 0 {
rodata = in.rodata
Comment on lines +183 to +184
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a cache 👍

} else {
executable, err := elf.NewFile(in.Reader)
if err != nil {
return false, xerrors.Errorf("cannot anaylse ELF file: %w", err)
}

rodata, err := ExtractELFRodata(executable)
if err != nil {
return false, err
rodata, err = ExtractELFRodata(executable)
if err != nil {
return false, err
}
in.rodata = rodata
Copy link
Contributor

@utam0k utam0k May 30, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cache at first.

}

matches, err := s.matches(rodata)
if matches || err != nil {
return matches, err
Expand All @@ -191,16 +204,23 @@ func (s *Signature) matchELFRodata(in io.ReaderAt) (bool, error) {
}

// matchELF matches a signature against an ELF file
func (s *Signature) matchELF(in io.ReaderAt) (bool, error) {
executable, err := elf.NewFile(in)
if err != nil {
return false, xerrors.Errorf("cannot anaylse ELF file: %w", err)
}
func (s *Signature) matchELF(in *SignatureReadCache) (bool, error) {
var symbols []string
if len(in.symbols) > 0 {
symbols = in.symbols
} else {
executable, err := elf.NewFile(in.Reader)
if err != nil {
return false, xerrors.Errorf("cannot anaylse ELF file: %w", err)
}

symbols, err := ExtractELFSymbols(executable)
if err != nil {
return false, err
symbols, err = ExtractELFSymbols(executable)
if err != nil {
return false, err
}
in.symbols = symbols
Comment on lines +208 to +221
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

}

for _, sym := range symbols {
matches, err := s.matches([]byte(sym))
if matches || err != nil {
Expand All @@ -213,22 +233,28 @@ func (s *Signature) matchELF(in io.ReaderAt) (bool, error) {

// ExtractELFSymbols extracts all ELF symbol names from an ELF binary
func ExtractELFSymbols(executable *elf.File) ([]string, error) {
var symbols []string
syms, err := executable.Symbols()
if err != nil && err != elf.ErrNoSymbols {
return nil, xerrors.Errorf("cannot get dynsym section: %w", err)
}
for _, s := range syms {
symbols = append(symbols, s.Name)
}

dynsyms, err := executable.DynamicSymbols()
if err != nil && err != elf.ErrNoSymbols {
return nil, xerrors.Errorf("cannot get dynsym section: %w", err)
}

symbols := make([]string, len(syms)+len(dynsyms))
i := 0
for _, s := range syms {
symbols[i] = s.Name
i += 1
}

for _, s := range dynsyms {
symbols = append(symbols, s.Name)
symbols[i] = s.Name
i += 1
Comment on lines +246 to +255
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these refactoring?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We know the number of elements, so I allocate the underlying array once instead of appending to the slice which includes resizing i.e. reallocation of the array.

}

return symbols, nil
}

Expand All @@ -247,11 +273,11 @@ func ExtractELFRodata(executable *elf.File) ([]byte, error) {
}

// matchAny matches a signature against a binary file
func (s *Signature) matchAny(in io.ReaderAt) (bool, error) {
func (s *Signature) matchAny(in *SignatureReadCache) (bool, error) {
buffer := make([]byte, 8096)
pos := s.Slice.Start
for {
n, err := in.ReadAt(buffer, pos)
n, err := in.Reader.ReadAt(buffer, pos)
sub := buffer[0:n]
pos += int64(n)

Expand Down