Skip to content

Commit 442daf8

Browse files
authored
Merge branch 'main' into add-stats-to-json
2 parents a7d860e + bbd5349 commit 442daf8

File tree

13 files changed

+4120
-65
lines changed

13 files changed

+4120
-65
lines changed

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/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

rules/net/remote_control/vnc.yara

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,5 @@ rule vnc_elf_subtle: medium {
2222
$VNC = "VNC"
2323
2424
condition:
25-
filesize < 3MB and uint32(0) == 1179403647 and all of them
25+
filesize < 5MB and uint32(0) == 1179403647 and all of them
2626
}

0 commit comments

Comments
 (0)