Skip to content

Commit 015dcf5

Browse files
authored
Merge branch 'main' into use-yara-x-take-2
Signed-off-by: Evan Gibler <20933572+egibs@users.noreply.github.com>
2 parents c731b9d + 3b49925 commit 015dcf5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+4473
-409
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,16 @@ malcontent has 3 modes of operation:
3535

3636
malcontent is at its best analyzing programs that run on Linux. Still, it also performs admirably for programs designed for other UNIX platforms such as macOS and, to a lesser extent, Windows.
3737

38+
## ⚠️ Malware Disclaimer ⚠️
39+
40+
Due to how malcontent operates, other malware scanners can detect malcontent as malicious.
41+
42+
Programs that leverage Yara rules will often see other programs that also use Yara rules as malicious due to the strings looking for problematic behavior(s).
43+
44+
For example, Elastic's agent has historically detected malcontent because of this: https://github.com/chainguard-dev/malcontent/issues/78*.
45+
46+
> \*Additional scanner findings can be seen in [this](https://www.virustotal.com/gui/file/b6f90aa5b9e7f3a5729a82f3ea35f96439691e150e0558c577a8541d3a187ba4/detection) VirusTotal scan.
47+
3848
## Features
3949

4050
* 14,500+ [YARA](YARA) detection rules

cmd/mal/mal.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -443,7 +443,7 @@ func main() {
443443
return err
444444
}
445445

446-
err = renderer.Full(ctx, res)
446+
err = renderer.Full(ctx, &mc, res)
447447
if err != nil {
448448
returnCode = ExitRenderFailed
449449
return err
@@ -486,7 +486,7 @@ func main() {
486486
return err
487487
}
488488

489-
err = renderer.Full(ctx, res)
489+
err = renderer.Full(ctx, &mc, res)
490490
if err != nil {
491491
returnCode = ExitRenderFailed
492492
return err
@@ -569,7 +569,7 @@ func main() {
569569
return length
570570
}(&res.Files)
571571

572-
err = renderer.Full(ctx, res)
572+
err = renderer.Full(ctx, &mc, res)
573573
if err != nil {
574574
returnCode = ExitRenderFailed
575575
return err

pkg/action/archive_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ func TestScanArchive(t *testing.T) {
243243
if err != nil {
244244
t.Fatal(err)
245245
}
246-
if err := r.Full(ctx, res); err != nil {
246+
if err := r.Full(ctx, nil, res); err != nil {
247247
t.Fatalf("full: %v", err)
248248
}
249249

pkg/action/oci_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ func TestOCI(t *testing.T) {
4646
if err != nil {
4747
t.Fatal(err)
4848
}
49-
if err := r.Full(ctx, res); err != nil {
49+
if err := r.Full(ctx, nil, res); err != nil {
5050
t.Fatalf("full: %v", err)
5151
}
5252

pkg/action/scan.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -601,7 +601,7 @@ func Scan(ctx context.Context, c malcontent.Config) (*malcontent.Report, error)
601601
}
602602
return true
603603
})
604-
if c.Stats {
604+
if c.Stats && c.Renderer.Name() != "JSON" && c.Renderer.Name() != "YAML" {
605605
err = render.Statistics(&c, r)
606606
if err != nil {
607607
return r, fmt.Errorf("stats: %w", err)

pkg/archive/archive.go

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,15 @@ func extractNestedArchive(
3232
if err != nil {
3333
return fmt.Errorf("failed to determine file type: %w", err)
3434
}
35-
if ft != nil && ft.MIME == "application/zlib" {
35+
switch {
36+
case ft != nil && ft.MIME == "application/x-upx":
3637
isArchive = true
37-
}
38-
if _, ok := programkind.ArchiveMap[programkind.GetExt(f)]; ok {
38+
case ft != nil && ft.MIME == "application/zlib":
39+
isArchive = true
40+
case programkind.ArchiveMap[programkind.GetExt(f)]:
3941
isArchive = true
4042
}
43+
4144
//nolint:nestif // ignore complexity of 8
4245
if isArchive {
4346
// Ensure the file was extracted and exists
@@ -52,11 +55,15 @@ func extractNestedArchive(
5255
if err != nil {
5356
return fmt.Errorf("failed to determine file type: %w", err)
5457
}
55-
if ft != nil && ft.MIME == "application/zlib" {
58+
switch {
59+
case ft != nil && ft.MIME == "application/x-upx":
60+
extract = ExtractUPX
61+
case ft != nil && ft.MIME == "application/zlib":
5662
extract = ExtractZlib
57-
} else {
63+
default:
5864
extract = ExtractionMethod(programkind.GetExt(fullPath))
5965
}
66+
6067
err = extract(ctx, d, fullPath)
6168
if err != nil {
6269
return fmt.Errorf("extract nested archive: %w", err)
@@ -103,11 +110,16 @@ func ExtractArchiveToTempDir(ctx context.Context, path string) (string, error) {
103110
if err != nil {
104111
return "", fmt.Errorf("failed to determine file type: %w", err)
105112
}
106-
if ft != nil && ft.MIME == "application/zlib" {
113+
114+
switch {
115+
case ft != nil && ft.MIME == "application/zlib":
107116
extract = ExtractZlib
108-
} else {
117+
case ft != nil && ft.MIME == "application/x-upx":
118+
extract = ExtractUPX
119+
default:
109120
extract = ExtractionMethod(programkind.GetExt(path))
110121
}
122+
111123
if extract == nil {
112124
return "", fmt.Errorf("unsupported archive type: %s", path)
113125
}

pkg/archive/upx.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package archive
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"os/exec"
8+
"path/filepath"
9+
10+
"github.com/chainguard-dev/clog"
11+
"github.com/chainguard-dev/malcontent/pkg/programkind"
12+
)
13+
14+
func ExtractUPX(ctx context.Context, d, f string) error {
15+
// Check if UPX is installed
16+
if err := programkind.UPXInstalled(); err != nil {
17+
return err
18+
}
19+
20+
logger := clog.FromContext(ctx).With("dir", d, "file", f)
21+
logger.Debug("extracting upx")
22+
23+
// Check if the file is valid
24+
_, err := os.Stat(f)
25+
if err != nil {
26+
return fmt.Errorf("failed to stat file: %w", err)
27+
}
28+
29+
gf, err := os.Open(f)
30+
if err != nil {
31+
return fmt.Errorf("failed to open file: %w", err)
32+
}
33+
defer gf.Close()
34+
35+
base := filepath.Base(f)
36+
target := filepath.Join(d, base[:len(base)-len(filepath.Ext(base))])
37+
38+
// copy the file to the temporary directory before decompressing
39+
tf, err := os.ReadFile(f)
40+
if err != nil {
41+
return err
42+
}
43+
44+
err = os.WriteFile(target, tf, 0o600)
45+
if err != nil {
46+
return err
47+
}
48+
49+
// Preserve the original file to scan both variants
50+
cmd := exec.Command("upx", "-d", "-k", target)
51+
if _, err := cmd.CombinedOutput(); err != nil {
52+
return fmt.Errorf("failed to decompress upx file: %w", err)
53+
}
54+
55+
return nil
56+
}

pkg/malcontent/malcontent.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import (
2020
type Renderer interface {
2121
Scanning(context.Context, string)
2222
File(context.Context, *FileReport) error
23-
Full(context.Context, *Report) error
23+
Full(context.Context, *Config, *Report) error
2424
Name() string
2525
}
2626

pkg/programkind/programkind.go

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
package programkind
55

66
import (
7+
"bytes"
78
"errors"
89
"fmt"
910
"io"
1011
"io/fs"
1112
"os"
13+
"os/exec"
1214
"path/filepath"
1315
"regexp"
1416
"strings"
@@ -30,6 +32,7 @@ var ArchiveMap = map[string]bool{
3032
".tar.gz": true,
3133
".tar.xz": true,
3234
".tgz": true,
35+
".upx": true,
3336
".whl": true,
3437
".xz": true,
3538
".zip": true,
@@ -86,6 +89,7 @@ var supportedKind = map[string]string{
8689
"sh": "application/x-sh",
8790
"so": "application/x-sharedlib",
8891
"ts": "application/typescript",
92+
"upx": "application/x-upx",
8993
"whl": "application/x-wheel+zip",
9094
"yaml": "",
9195
"yara": "",
@@ -99,8 +103,17 @@ type FileType struct {
99103
}
100104

101105
// IsSupportedArchive returns whether a path can be processed by our archive extractor.
106+
// UPX files are an edge case since they may or may not even have an extension that can be referenced.
102107
func IsSupportedArchive(path string) bool {
103-
return ArchiveMap[GetExt(path)]
108+
if _, isValidArchive := ArchiveMap[GetExt(path)]; isValidArchive {
109+
return true
110+
}
111+
if ft, err := File(path); err == nil && ft != nil {
112+
if ft.MIME == "application/x-upx" {
113+
return true
114+
}
115+
}
116+
return false
104117
}
105118

106119
// getExt returns the extension of a file path
@@ -131,6 +144,40 @@ func GetExt(path string) string {
131144
return ext
132145
}
133146

147+
var ErrUPXNotFound = errors.New("UPX executable not found in PATH")
148+
149+
func UPXInstalled() error {
150+
_, err := exec.LookPath("upx")
151+
if err != nil {
152+
if errors.Is(err, exec.ErrNotFound) {
153+
return ErrUPXNotFound
154+
}
155+
return fmt.Errorf("failed to check for UPX executable: %w", err)
156+
}
157+
return nil
158+
}
159+
160+
// IsValidUPX checks whether a suspected UPX-compressed file can be decompressed with UPX.
161+
func IsValidUPX(header []byte, path string) (bool, error) {
162+
if !bytes.Contains(header, []byte("UPX!")) {
163+
return false, nil
164+
}
165+
166+
if err := UPXInstalled(); err != nil {
167+
return false, err
168+
}
169+
170+
cmd := exec.Command("upx", "-l", "-f", path)
171+
output, err := cmd.CombinedOutput()
172+
173+
if err != nil && (bytes.Contains(output, []byte("NotPackedException")) ||
174+
bytes.Contains(output, []byte("not packed by UPX"))) {
175+
return false, nil
176+
}
177+
178+
return true, nil
179+
}
180+
134181
func makeFileType(path string, ext string, mime string) *FileType {
135182
ext = strings.TrimPrefix(ext, ".")
136183

@@ -205,6 +252,10 @@ func File(path string) (*FileType, error) {
205252

206253
// final strategy: DIY matching where mimetype is too strict.
207254
s := string(hdr[:])
255+
if isUPX, err := IsValidUPX(hdr[:], path); err == nil && isUPX {
256+
return Path(".upx"), nil
257+
}
258+
208259
switch {
209260
case hdr[0] == '\x7f' && hdr[1] == 'E' || hdr[2] == 'L' || hdr[3] == 'F':
210261
return Path(".elf"), nil

pkg/refresh/refresh.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ func executeRefresh(ctx context.Context, testData []TestData) error {
205205
return fmt.Errorf("refresh sample data for %s: %w", data.OutputPath, err)
206206
}
207207

208-
if err := data.Config.Renderer.Full(ctx, res); err != nil {
208+
if err := data.Config.Renderer.Full(ctx, nil, res); err != nil {
209209
return fmt.Errorf("render results for %s: %w", data.OutputPath, err)
210210
}
211211

0 commit comments

Comments
 (0)