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

Add support for UPX files #731

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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
55 changes: 55 additions & 0 deletions pkg/archive/upx.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
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
}

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

return nil
}
50 changes: 49 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,35 @@ func GetExt(path string) string {
return ext
}

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

func ValidateUPX(path string) (bool, error) {
egibs marked this conversation as resolved.
Show resolved Hide resolved
if err := UpxInstalled(); err != nil {
return false, err
}
cmd := exec.Command("upx", "-l", path)
egibs marked this conversation as resolved.
Show resolved Hide resolved
output, err := cmd.CombinedOutput()
if err != nil {
if bytes.Contains(output, []byte("NotPackedException")) ||
bytes.Contains(output, []byte("not packed by UPX")) {
return false, nil
}
return false, nil
}
return true, nil
}

func UpxInstalled() error {
egibs marked this conversation as resolved.
Show resolved Hide resolved
_, 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
}

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

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

// final strategy: DIY matching where mimetype is too strict.
s := string(hdr[:])
if bytes.Contains(hdr[:], []byte{'\x55', '\x50', '\x58', '\x21'}) {
egibs marked this conversation as resolved.
Show resolved Hide resolved
if isValid, err := ValidateUPX(path); err == nil && isValid {
return Path(".upx"), nil
}
}

switch {
case hdr[0] == '\x7f' && hdr[1] == 'E' || hdr[2] == 'L' || hdr[3] == 'F':
return Path(".elf"), nil
Expand Down
74 changes: 66 additions & 8 deletions tests/linux/2024.vncjew/__min__c.simple
Original file line number Diff line number Diff line change
@@ -1,13 +1,71 @@
# linux/2024.vncjew/__min__c: critical
anti-static/elf/entropy: high
anti-static/elf/header: high
egibs marked this conversation as resolved.
Show resolved Hide resolved
anti-static/elf/multiple: medium
anti-static/packer/upx: high
c2/addr/ip: high
# linux/2024.vncjew/__min__c ∴ /__min__c: critical
c2/addr/ip: medium
c2/addr/url: low
egibs marked this conversation as resolved.
Show resolved Hide resolved
c2/tool_transfer/arch: low
fs/proc/self_exe: medium
credential/password: low
credential/ssl/private_key: low
crypto/aes: low
crypto/cipher: medium
crypto/decrypt: low
crypto/ecdsa: low
crypto/ed25519: low
crypto/encrypt: medium
crypto/public_key: low
crypto/rc4: low
crypto/tls: low
data/compression/gzip: low
data/encoding/base64: low
data/encoding/json: low
data/encoding/json_decode: low
data/hash/md5: low
discover/system/cpu: low
discover/system/hostname: low
discover/system/platform: low
discover/user/HOME: low
discover/user/USER: low
evasion/bypass_security/linux/iptables: medium
evasion/bypass_security/linux/iptables_append: medium
exec/plugin: low
exec/program: medium
fs/directory/list: low
fs/file/open: low
fs/file/read: low
fs/link_read: low
fs/path/etc: low
fs/path/etc_hosts: medium
fs/path/etc_resolv.conf: low
fs/path/home: low
fs/permission/chown: medium
fs/permission/modify: medium
fs/tempfile: low
malware/family/vncjew: critical
net/dns: low
net/dns/servers: low
net/dns/txt: low
net/http/accept: low
net/http/accept_encoding: low
net/http/auth: low
net/http/cookies: medium
net/http/post: medium
net/http/proxy: low
net/http/request: low
net/remote_control/vnc: medium
net/http/websocket: medium
egibs marked this conversation as resolved.
Show resolved Hide resolved
net/ip/addr: medium
net/ip/host_port: medium
net/ip/multicast_send: low
net/ip/parse: medium
net/resolve/hostname: low
net/socket/listen: medium
net/socket/local_addr: low
net/socket/peer_address: low
net/socket/receive: low
net/socket/send: low
net/tcp/connect: medium
net/udp/receive: low
net/udp/send: low
net/url/embedded: low
net/url/parse: low
net/url/request: medium
os/fd/sendfile: low
os/kernel/netlink: low
sec-tool/net/masscan: high
60 changes: 53 additions & 7 deletions tests/linux/clean/trino.linux-amd64.launcher.simple
Original file line number Diff line number Diff line change
@@ -1,14 +1,60 @@
# linux/clean/trino.linux-amd64.launcher: medium
anti-static/elf/content: medium
anti-static/elf/entropy: medium
anti-static/elf/header: medium
anti-static/elf/multiple: medium
anti-static/packer/upx: medium
# linux/clean/trino.linux-amd64.launcher ∴ /trino.linux-amd64: medium
c2/addr/ip: medium
c2/addr/url: low
c2/tool_transfer/arch: low
collect/archives/zip: medium
credential/password: low
credential/ssl/private_key: low
crypto/aes: low
crypto/ecdsa: low
crypto/public_key: low
crypto/rc4: low
crypto/tls: low
data/compression/gzip: low
fs/proc/self_exe: medium
data/encoding/base64: low
discover/system/cpu: low
discover/system/hostname: low
discover/system/platform: low
exec/plugin: low
exec/program: medium
fs/directory/create: low
fs/directory/remove: low
fs/file/delete: low
fs/file/open: low
fs/file/read: low
fs/link_read: low
fs/lock_update: low
fs/path/etc: low
fs/path/etc_hosts: medium
fs/path/etc_resolv.conf: low
fs/path/users: medium
fs/path/var: low
fs/permission/chown: medium
fs/permission/modify: medium
net/dns: low
net/dns/servers: low
net/dns/txt: low
net/http/auth: low
net/http/post: medium
net/http/proxy: low
net/http/request: low
net/ip/addr: medium
net/ip/host_port: medium
net/ip/parse: medium
net/resolve/hostname: low
net/socket/listen: medium
net/socket/local_addr: low
net/socket/peer_address: low
net/socket/receive: low
net/socket/send: low
net/tcp/connect: medium
net/udp/receive: low
net/udp/send: low
net/url/embedded: low
net/url/parse: low
net/url/request: medium
os/fd/sendfile: low
os/kernel/netlink: low
persist/daemon: medium
persist/pid_file: medium
process/groups_set: low
Loading
Loading