Skip to content
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
26 changes: 19 additions & 7 deletions pkg/archive/archive.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,15 @@ func extractNestedArchive(
if err != nil {
return fmt.Errorf("failed to determine file type: %w", err)
}
if ft != nil && ft.MIME == "application/zlib" {
switch {
case ft != nil && ft.MIME == "application/x-upx":
isArchive = true
}
if _, ok := programkind.ArchiveMap[programkind.GetExt(f)]; ok {
case ft != nil && ft.MIME == "application/zlib":
isArchive = true
case programkind.ArchiveMap[programkind.GetExt(f)]:
isArchive = true
}

//nolint:nestif // ignore complexity of 8
if isArchive {
// Ensure the file was extracted and exists
Expand All @@ -52,11 +55,15 @@ func extractNestedArchive(
if err != nil {
return fmt.Errorf("failed to determine file type: %w", err)
}
if ft != nil && ft.MIME == "application/zlib" {
switch {
case ft != nil && ft.MIME == "application/x-upx":
extract = ExtractUPX
case ft != nil && ft.MIME == "application/zlib":
extract = ExtractZlib
} else {
default:
extract = ExtractionMethod(programkind.GetExt(fullPath))
}

err = extract(ctx, d, fullPath)
if err != nil {
return fmt.Errorf("extract nested archive: %w", err)
Expand Down Expand Up @@ -103,11 +110,16 @@ func ExtractArchiveToTempDir(ctx context.Context, path string) (string, error) {
if err != nil {
return "", fmt.Errorf("failed to determine file type: %w", err)
}
if ft != nil && ft.MIME == "application/zlib" {

switch {
case ft != nil && ft.MIME == "application/zlib":
extract = ExtractZlib
} else {
case ft != nil && ft.MIME == "application/x-upx":
extract = ExtractUPX
default:
extract = ExtractionMethod(programkind.GetExt(path))
}

if extract == nil {
return "", fmt.Errorf("unsupported archive type: %s", path)
}
Expand Down
56 changes: 56 additions & 0 deletions pkg/archive/upx.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package archive

import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"

"github.com/chainguard-dev/clog"
"github.com/chainguard-dev/malcontent/pkg/programkind"
)

func ExtractUPX(ctx context.Context, d, f string) error {
// Check if UPX is installed
if err := programkind.UPXInstalled(); err != nil {
return err
}

logger := clog.FromContext(ctx).With("dir", d, "file", f)
logger.Debug("extracting upx")

// Check if the file is valid
_, err := os.Stat(f)
if err != nil {
return fmt.Errorf("failed to stat file: %w", err)
}

gf, err := os.Open(f)
if err != nil {
return fmt.Errorf("failed to open file: %w", err)
}
defer gf.Close()

base := filepath.Base(f)
target := filepath.Join(d, base[:len(base)-len(filepath.Ext(base))])

// copy the file to the temporary directory before decompressing
tf, err := os.ReadFile(f)
if err != nil {
return err
}

err = os.WriteFile(target, tf, 0o600)
if err != nil {
return err
}

// Preserve the original file to scan both variants
cmd := exec.Command("upx", "-d", "-k", target)
if _, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to decompress upx file: %w", err)
}

return nil
}
53 changes: 52 additions & 1 deletion pkg/programkind/programkind.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
package programkind

import (
"bytes"
"errors"
"fmt"
"io"
"io/fs"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
Expand All @@ -30,6 +32,7 @@ var ArchiveMap = map[string]bool{
".tar.gz": true,
".tar.xz": true,
".tgz": true,
".upx": true,
".whl": true,
".xz": true,
".zip": true,
Expand Down Expand Up @@ -86,6 +89,7 @@ var supportedKind = map[string]string{
"sh": "application/x-sh",
"so": "application/x-sharedlib",
"ts": "application/typescript",
"upx": "application/x-upx",
"whl": "application/x-wheel+zip",
"yaml": "",
"yara": "",
Expand All @@ -99,8 +103,17 @@ type FileType struct {
}

// IsSupportedArchive returns whether a path can be processed by our archive extractor.
// UPX files are an edge case since they may or may not even have an extension that can be referenced.
func IsSupportedArchive(path string) bool {
return ArchiveMap[GetExt(path)]
if _, isValidArchive := ArchiveMap[GetExt(path)]; isValidArchive {
return true
}
if ft, err := File(path); err == nil && ft != nil {
if ft.MIME == "application/x-upx" {
return true
}
}
return false
}

// getExt returns the extension of a file path
Expand Down Expand Up @@ -131,6 +144,40 @@ func GetExt(path string) string {
return ext
}

var ErrUPXNotFound = errors.New("UPX executable not found in PATH")

func UPXInstalled() error {
_, err := exec.LookPath("upx")
if err != nil {
if errors.Is(err, exec.ErrNotFound) {
return ErrUPXNotFound
}
return fmt.Errorf("failed to check for UPX executable: %w", err)
}
return nil
}

// IsValidUPX checks whether a suspected UPX-compressed file can be decompressed with UPX.
func IsValidUPX(header []byte, path string) (bool, error) {
if !bytes.Contains(header, []byte("UPX!")) {
return false, nil
}

if err := UPXInstalled(); err != nil {
return false, err
}

cmd := exec.Command("upx", "-l", "-f", path)
output, err := cmd.CombinedOutput()

if err != nil && (bytes.Contains(output, []byte("NotPackedException")) ||
bytes.Contains(output, []byte("not packed by UPX"))) {
return false, nil
}

return true, nil
}

func makeFileType(path string, ext string, mime string) *FileType {
ext = strings.TrimPrefix(ext, ".")

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

// final strategy: DIY matching where mimetype is too strict.
s := string(hdr[:])
if isUPX, err := IsValidUPX(hdr[:], path); err == nil && isUPX {
return Path(".upx"), nil
}

switch {
case hdr[0] == '\x7f' && hdr[1] == 'E' || hdr[2] == 'L' || hdr[3] == 'F':
return Path(".elf"), nil
Expand Down
2 changes: 1 addition & 1 deletion rules/net/remote_control/vnc.yara
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,5 @@ rule vnc_elf_subtle: medium {
$VNC = "VNC"

condition:
filesize < 3MB and uint32(0) == 1179403647 and all of them
filesize < 5MB and uint32(0) == 1179403647 and all of them
}
Loading
Loading