Commandline Cloaking and Sysmon for Linux
tl;dr
I investigated running Sysmon for Linux against a number of techniques to fake a program’s commandline. Code and Sysmon config available on GitHub.
Sysmon for Linux
Earlier this year, Microsoft released Sysmon for Linux, which is an awesome new addition to free Linux monitoring tools. Typically, sysadmins have used AuditD (or tools that use AuditD under the hood) to monitor Linux systems. However, using AuditD can be tricky to configure, and can require a level of knowledge of Linux internals that might put off administrators that just want basic process monitoring.
Sysmon for Linux conversely uses the same event schema as Sysmon on Windows, which abstracts away the internals in how it generates events, instead groping them into “ProcessCreate”, “NetworkConnect “, “FileDelete”, etc. Using the same schema as Windows is quite useful for people already familiar with the Windows version of Sysmon, although documentation could be improved to help those who aren’t familiar, or only know AuditD.
I was interested in exploring what it’s like to set up and run Sysmon for Linux, and how it might fare against some common techniques malware uses to hide from system administrators.
Attack Scenario
A lot of common malware detections on Linux are based upon on pattern matching commandlines and program filenames. I wanted to explore the various ways a program can alter or hide commandlines from Audit tools, and see what is recorded by Sysmon for Linux.
It was also an excuse to learn more about writing low-level programs in Go, which I’ve always wanted to learn more about, instead of defaulting to C all the time.
Lab setup
To set things up, I used both Roberto Rodriguez’s dope Sysmon Overview and the official instructions.
As I was only interested in process events, I used this simple config to just get process creations:
<Sysmon schemaversion="4.70">
<EventFiltering>
<!-- Event ID 1 == ProcessCreate. Log all newly created processes -->
<RuleGroup name="" groupRelation="or">
<ProcessCreate onmatch="exclude"/>
</RuleGroup>
</EventFiltering>
</Sysmon>
Once Sysmon was installed and running, to make things simple I didn’t forward the events to a SIEM, I just used the sysmonLogView
helper tool to print events out to the console in a human-readable format:
sudo bash -c '
tail -f /var/log/syslog |
/opt/sysmon/sysmonLogView -e 1
'
Tests
Test #0 - No cloaking
To start, I made a simple program named basic
that just prints out the arguments passed into it:
func main() {
pid := os.Getpid()
ppid := os.Getppid()
argc := len(os.Args)
fmt.Printf(" PID %d\n", pid)
fmt.Printf(" PPID %d\n", ppid)
fmt.Printf(" argc %d\n", argc)
for i := 0; i < argc; i++ {
fmt.Printf(" argv[%d] %s\n", i, os.Args[i])
}
fmt.Print(" Sleeping for 60 seconds so you can lookup the PID")
time.Sleep(60 * time.Second)
}
When run with a single argument, basic
prints out:
$> /path/to/basic AAAA
PID 984173
PPID 1054354
argc 2
argv[0] /path/to/basic
argv[1] AAAA
Sleeping for 60 seconds so you can lookup the PID
This generates the following Sysmon Event as expected:
Event SYSMONEVENT_CREATE_PROCESS
RuleName: -
UtcTime: 2021-11-30 23:51:08.592
ProcessGuid: {35dd0383-b8ec-61a6-b8d0-480000000000}
ProcessId: 984173
Image: /path/to/basic
FileVersion: -
Description: -
Product: -
Company: -
OriginalFileName: -
CommandLine: /path/to/basic AAAA
CurrentDirectory: /path/to
User: path
LogonGuid: {35dd0383-9d97-61a4-e803-000002000000}
LogonId: 1000
TerminalSessionId: 347
IntegrityLevel: no level
Hashes: -
ParentProcessGuid: {35dd0383-9a98-61a4-fddb-e43082550000}
ParentProcessId: 1054354
ParentImage: /usr/bin/bash
ParentCommandLine: -bash
ParentUser: path
We can see basic
was launched by bash
, the process IDs match up, and the commandline is the same.
All as expected.
A really neat aspect of Sysmon for Linux is how it extracts the commandline arguments from a process. Other tools use the arguments passed into the execve syscall, but these arguments can be vulnerable to a race condition, such as the one detailed by Rex Guo and Junyuan Zeng at DEF CON 29, due to the fact they exist in the parent process’ userspace memory. Sysmon for Linux however extracts the arguments from the task struct within the kernel’s memory, which (at the time Sysmon reads it) is inaccessible to the parent process, and will always be the real arguments used to start the process.
We can also compare Sysmon’s output to other sysadmin tools, proving the commandline, process ID, and filename match up:
# Use ps to look for 'basic' process
$> ps u -C "basic"
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
path 1066830 0.0 0.1 702896 8968 pts/2 Sl+ 11:23 0:00 /path/to/basic AAAA
Under the hood, ps
parses data from the /proc/
pseudo-filesystem, so we can also do that:
# Look up the commandline using the PID and the proc psudo filesystem
$> xxd -g 1 -c 6 /proc/1066830/cmdline
00000000: 2e 2f 62 69 6e 2f /path/to/
00000006: 62 61 73 69 63 00 basic.
0000000c: 41 41 41 41 00 AAAA.
By reading /proc/<pid>/cmdline
we can see the strings in argv
, each ending in a 00
NULL character.
This data exists within basic
’s memory, which means that a dodgy program could alter this data.
Test #1 - Post-execution cloaking
Next, I wrote a program named dodgy
to alter the contents of it’s argv after it has started to run.
While this is easy to do in either C or Go, the Go code is harder to read, so I created dodgy
in C, which looks like this:
int main(int argc, char* argv[]) {
// Overwrite address of argv to fool 'ps'
memset(argv[0], 'F', strlen(argv[0]));
if (argc > 1) {
memset(argv[1], 'B', strlen(argv[1]));
}
// Print arguments just like 'basic' did
printf(" PID %d\n", getpid());
printf(" PPID %d\n", getppid());
printf(" argc %d\n", argc);
for (int i = 0; i < argc; i++) {
printf(" argv[%d] %s\n", i, argv[i]);
}
printf("Sleeping for 60 seconds so you can lookup the PID\n");
sleep(60);
return 0;
}
Now when we run the program we get:
$> /path/to/dodgy AAAA
PID 1067696
PPID 1054354
argc 2
argv[0] FFFFFFFFFFF
argv[1] BBBB
Sleeping for 60 seconds so you can lookup the PID
# Then in another tab look for all 'dodgy' processes
$> ps u -C "dodgy"
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
path 1067696 0.0 0.0 2304 556 pts/2 S+ 12:06 0:00 FFFFFFFFFFF BBBB
# Look up the commandline using the PID and the proc psudo filesystem
$> xxd -g 1 -c 6 /proc/1067696/cmdline
00000000: 46 46 46 46 46 46 FFFFFF
00000006: 46 46 46 46 46 00 FFFFF.
0000000c: 42 42 42 42 00 BBBB.
There are similar techniques programs can use to fake information once it has executed, including:
- Using prctl and
PR_SET_NAME
to change the process name in/proc/pid/comm
- Fork twice and kill parents, which sets the process’ Parent PID to PID 1
But to Sysmon all these efforts are in vain - As it logs the process information before it runs, it sees the original commandline that the process was started with:
Event SYSMONEVENT_CREATE_PROCESS
RuleName: -
UtcTime: 2021-11-30 23:51:08.592
ProcessGuid: {35dd0383-b8ec-61a6-b8d0-480000000000}
ProcessId: 1067696
Image: /path/to/dodgy
FileVersion: -
Description: -
Product: -
Company: -
OriginalFileName: -
CommandLine: /path/to/dodgy AAAA
CurrentDirectory: /path/to
User: path
LogonGuid: {35dd0383-9d97-61a4-e803-000002000000}
LogonId: 1000
TerminalSessionId: 347
IntegrityLevel: no level
Hashes: -
ParentProcessGuid: {35dd0383-9a98-61a4-fddb-e43082550000}
ParentProcessId: 1054354
ParentImage: /usr/bin/bash
ParentCommandLine: -bash
ParentUser: path
Test #2 - Piping input
Some programs support reading data in from standard input, which means it can either be |
piped in
or read in using <
file redirection. One example is Python
, where the following examples are all equivalent:
# Run the file
python3 quietquokka.py
python3 $(bash -c "echo cXVpZXRxdW9ra2EucHkK | base64 -d")
# Pipe file in
cat quietquokka.py | python3
# File redirection
python3 < quietquokka.py
The first two generate Syslog events matching what you expect (the $(bash -c ...
part gets evaluated prior to the syscall):
Event SYSMONEVENT_CREATE_PROCESS
RuleName: -
UtcTime: 2021-12-01 02:57:11.135
ProcessGuid: {35dd0383-e487-61a6-fdbb-b3691d560000}
ProcessId: 1068450
Image: /usr/bin/bash
FileVersion: -
Description: -
Product: -
Company: -
OriginalFileName: -
CommandLine: bash -c echo cXVpZXRxdW9ra2EucHkK | base64 -d
CurrentDirectory: /path/to
User: path
LogonGuid: {35dd0383-9d97-61a4-e803-000002000000}
LogonId: 1000
TerminalSessionId: 347
IntegrityLevel: no level
Hashes: -
ParentProcessGuid: {35dd0383-9a98-61a4-fddb-e43082550000}
ParentProcessId: 1054354
ParentImage: /usr/bin/bash
ParentCommandLine: -bash
ParentUser: path
Event SYSMONEVENT_CREATE_PROCESS
RuleName: -
UtcTime: 2021-12-01 02:57:11.137
ProcessGuid: {35dd0383-e487-61a6-69ee-60001a560000}
ProcessId: 1068452
Image: /usr/bin/base64
FileVersion: -
Description: -
Product: -
Company: -
OriginalFileName: -
CommandLine: base64 -d
CurrentDirectory: /path/to
User: path
LogonGuid: {35dd0383-9d97-61a4-e803-000002000000}
LogonId: 1000
TerminalSessionId: 347
IntegrityLevel: no level
Hashes: -
ParentProcessGuid: {35dd0383-e487-61a6-fdbb-b3691d560000}
ParentProcessId: 1068450
ParentImage: /usr/bin/bash
ParentCommandLine: bash
ParentUser: path
Event SYSMONEVENT_CREATE_PROCESS
RuleName: -
UtcTime: 2021-12-01 02:57:11.137
ProcessGuid: {35dd0383-e487-61a6-ed63-6a0000000000}
ProcessId: 1068453
Image: /usr/bin/python3.9
FileVersion: -
Description: -
Product: -
Company: -
OriginalFileName: -
CommandLine: python3 quietquokka.py
CurrentDirectory: /path/to
User: path
LogonGuid: {35dd0383-9d97-61a4-e803-000002000000}
LogonId: 1000
TerminalSessionId: 347
IntegrityLevel: no level
Hashes: -
ParentProcessGuid: {35dd0383-9a98-61a4-fddb-e43082550000}
ParentProcessId: 1054354
ParentImage: /usr/bin/bash
ParentCommandLine: -bash
ParentUser: path
However, in the other two examples (piping in data and file redirection), the Python commandline is empty:
Event SYSMONEVENT_CREATE_PROCESS
RuleName: -
UtcTime: 2021-12-01 02:58:58.656
ProcessGuid: {35dd0383-e4f2-61a6-ed63-6a0000000000}
ProcessId: 1068470
Image: /usr/bin/python3.9
FileVersion: -
Description: -
Product: -
Company: -
OriginalFileName: -
CommandLine: python3
CurrentDirectory: /path/to
User: path
LogonGuid: {35dd0383-9d97-61a4-e803-000002000000}
LogonId: 1000
TerminalSessionId: 347
IntegrityLevel: no level
Hashes: -
ParentProcessGuid: {35dd0383-9a98-61a4-fddb-e43082550000}
ParentProcessId: 1054354
ParentImage: /usr/bin/bash
ParentCommandLine: -bash
ParentUser: path
I’ve covered this technique before, including thoughts on how to make use of eBPF to detect it. And to be clear Sysmon isn’t wrong - the Python was started with just the commandline “python3”.
You can’t pipe in all arguments to a program, however, only in specific cases where the program supports it.
But this does include a number of tools like python
, curl
, and bash
, and is important to think about when
looking to make commandline-focussed detection.
Test #3 - In-memory loader
Another common technique used by malware is to execute a program from memory. This can be easily done by using memfd_create:
func main() {
// Print PID of loader program
fmt.Printf(" Ldr PID %d\n", os.Getpid())
// Use memfd to create in-memory file
fd, _ := unix.MemfdCreate("", 0)
// open created in-memory file
fp := fmt.Sprintf("/proc/self/fd/%d", fd)
memfd_file := os.NewFile(uintptr(fd), fp)
defer memfd_file.Close()
// Read ELF from disk and write into in-memory file
// ELF could also be read from a C2 server, encrypted
// on disk, hardcoded, piped in from stdin, etc.
data, _ := ioutil.ReadFile("/path/to/basic")
memfd_file.Write(data)
// Execute in-memory binary, fake argv[0] filename
argv := []string{"from_loader", "AAAA"}
unix.Exec(fp, argv, os.Environ())
}
This loader
program can be then be run to launch basic
from in-memory:
$> /path/to/loader
Ldr PID 17436
PID 17436
PPID 15924
argc 2
argv[0] from_loader
argv[1] AAAA
Sleeping for 60 seconds so you can lookup the PID
# Look up pid
$> ps u | grep 17436
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
path 17436 0.0 0.0 703064 2872 pts/0 Sl+ 15:13 0:00 from_loader AAAA
Due to how the execve syscall works, basic
’s PID is the same as loader’s. This is because execve
doesn’t really create a new process per sey,
but instead replaces the program running from loader
to basic
.
But to Sysmon, both programs are recorded:
Event SYSMONEVENT_CREATE_PROCESS
RuleName: -
UtcTime: 2022-01-02 04:19:04.901
ProcessGuid: {35dd0383-27b8-61d1-8cdf-480000000000}
ProcessId: 17436
Image: /path/to//bin/loader
FileVersion: -
Description: -
Product: -
Company: -
OriginalFileName: -
CommandLine: /path/to/loader /path/to/basic AAAA
CurrentDirectory: /path/to/
User: path
LogonGuid: {35dd0383-21ff-61d1-e803-000000000000}
LogonId: 1000
TerminalSessionId: 177
IntegrityLevel: no level
Hashes: -
ParentProcessGuid: {00000000-0000-0000-0000-000000000000}
ParentProcessId: 15924
ParentImage: -
ParentCommandLine: -
ParentUser: -
Event SYSMONEVENT_CREATE_PROCESS
RuleName: -
UtcTime: 2022-01-02 04:19:04.901
ProcessGuid: {35dd0383-27b8-61d1-1000-480000000000}
ProcessId: 17436
Image: /memfd:
FileVersion: -
Description: -
Product: -
Company: -
OriginalFileName: -
CommandLine: from_loader AAAA
CurrentDirectory: /path/to/
User: path
LogonGuid: {35dd0383-21ff-61d1-e803-000000000000}
LogonId: 1000
TerminalSessionId: 177
IntegrityLevel: no level
Hashes: -
ParentProcessGuid: {00000000-0000-0000-0000-000000000000}
ParentProcessId: 15924
ParentImage: -
ParentCommandLine: -
ParentUser: -
Sysmon can only tell the second ELF executed was from in-memory (marking it as /memfd
),
and it records the ‘fake’ from_loader
commandline. It cannot tell that the binary run was actually /path/to/basic
.
But depending on the machine, user, or parent process, it is probably highly suspicious to see a /memfd
in-memory ELF being run.
So while you cannot tell what binary it was, this alone might be enough to investigate.
Test #4 - LD_PRELOAD
One common technique used by malware to hide is to make use of LD_PRELOAD and similar
environment variables. By setting LD_PRELOAD
to the path to a library on disk, the Linux dynamic linker will inject this library into
all new programs, and will look in that library first for any exported functions that the program may have instead wanted to use
from other libraries. This means a malicious library and can hook and alter the program, and is commonly used as a backdoor to alter
functions that relate to file reading or network connections, so the malware can hide data on disk, or allow a normally blocked connection through.
But another thing a malicious LD_PRELOAD
library can do is hook the program just as it starts, and alter the argv
arguments before they
are seen and used. For programs written in C using libc, this means hooking the __libc_start_main
function.
In C, this type of hooking is simple:
int __libc_start_main(
int (*main)(int, char **, char **),
int argc,
char **argv,
int (*init)(int, char **, char **),
void (*fini)(void),
void (*rtld_fini)(void),
void *stack_end)
{
// Overwrite args like in 'dodgy'
memset(argv[0], 'F', strlen(argv[0]));
if (argc > 1) {
memset(argv[1], 'B', strlen(argv[1]));
}
// Lookup the real libc function
typeof(&__libc_start_main) orig_start_main = dlsym(RTLD_NEXT, "__libc_start_main");
// Call real __libc_start_main
return orig_start_main(main, argc, argv, init, fini, rtld_fini, stack_end);
}
After writing a C version of the basic
program to be hooked, I ran with and set the LD_PRELOAD
variable to be my malicious library:
# Set the LD_PRELOAD variable to path to preload
# NOTE: this won't work on Go programs, so use the C version of Basic
LD_PRELOAD=/path/to/preload.so /path/to/basic_c AAAA
PID 26687
PPID 16961
argc 2
argv[0] FFFFFFFFFFFFF
argv[1] BBBB
Sleeping for 60 seconds so you can lookup the PID
# Check ps output for pid
$> ps aux | grep 26687
path 26687 0.0 0.0 2360 532 pts/1 S+ 15:47 0:00 FFFFFFFFFFFFF BBBB
# Extract LD_PRELOAD env variable from ps output
$> ps ue | grep 26687 | grep -o "LD_PRELOAD=.*" | awk '{print $1}'
LD_PRELOAD=/path/to/preload.so
As preload’s shenanigans run after the process starts, Sysmon only see’s the original arguments, and
not what the effective arguments to basic_c
are:
Event SYSMONEVENT_CREATE_PROCESS
RuleName: -
UtcTime: 2021-12-02 03:29:01.320
ProcessGuid: {35dd0383-3d7d-61a8-9d12-65fb78550000}
ProcessId: 26687
Image: /path/to/basic_c
FileVersion: -
Description: -
Product: -
Company: -
OriginalFileName: -
CommandLine: /path/to/basic_c AAAA
CurrentDirectory: /path/to
User: path
LogonGuid: {35dd0383-4d8c-61a4-e803-000000000000}
LogonId: 1000
TerminalSessionId: 334
IntegrityLevel: no level
Hashes: -
ParentProcessGuid: {35dd0383-4d8c-61a4-fd3b-d4af46560000}
ParentProcessId: 1052430
ParentImage: /usr/bin/bash
ParentCommandLine: -bash
ParentUser: path
This is accurate, technically basic_c
was launched with AAAA
, and preload runs after the process
has launched. But the basic_c
program will act as if the fake BBBB
argument was passed into it.
To add to the confusion, instead of overwriting argv
, the preload library could instead
point __libc_start_main
to a new argv array, which will fool ps
as well:
// Create our own argv and argc
static char* new_argv[] = {
"from_preload",
"BBBB"
};
static int new_argc = 2;
int __libc_start_main(
int (*main)(int, char **, char **),
int argc,
char **argv,
int (*init)(int, char **, char **),
void (*fini)(void),
void (*rtld_fini)(void),
void *stack_end)
{
// Lookup the real libc function
typeof(&__libc_start_main) orig_start_main = dlsym(RTLD_NEXT, "__libc_start_main");
// Call real __libc_start_main, note we instead pass it our new argc and argv
// instead of the original ones
return orig_start_main(main, new_argc, new_argv, init, fini, rtld_fini, stack_end);
}
When run this looks like:
# Set the LD_PRELOAD variable to path to preload
# NOTE: this won't work on Go programs, so use the C version of Basic
LD_PRELOAD=/path/to/preload.so /path/to/basic_c AAAA
PID 28152
PPID 16961
argc 2
argv[0] from_preload
argv[1] BBBB
Sleeping for 60 seconds so you can lookup the PID
# Check ps output for pid, note the original arguments and not what basic effectivly ran with
$> ps aux | grep 28152
path 28152 0.0 0.0 2360 536 pts/1 S+ 15:57 0:00 /path/to/basic_c AAAA
This blog was also meant to be about how to do things in Go and not C, so thanks to Awgh on the Bloodhound Slack’s Go channel, I was able to create a similar library in Go:
//export __libc_start_main
func __libc_start_main(main *C.void, argc C.int, argv **C.char, init *C.void, fini *C.void, rtld_fini *C.void,) C.int {
// Create and use new argv[0] and argv[1]
offset := unsafe.Sizeof(uintptr(0))
argv_copy := argv
new_argv_0 := C.CString("from_preload")
*argv_copy = new_argv_0
if argc > 1 {
new_argv_1 := C.CString("BBBB")
argv_copy = (**C.char)(unsafe.Pointer(uintptr(unsafe.Pointer(argv_copy)) + offset))
*argv_copy = new_argv_1
}
// Find and call real __libc_start_main function
fp := C.dlopen(C.CString("libc.so.6"), C.RTLD_LAZY)
orig_func := C.dlsym(fp, C.CString("__libc_start_main"))
val, err := cdecl.Call(
uintptr(orig_func),
uintptr(unsafe.Pointer(main)),
uintptr(argc),
uintptr(unsafe.Pointer(argv)),
uintptr(unsafe.Pointer(init)),
uintptr(unsafe.Pointer(fini)),
uintptr(unsafe.Pointer(rtld_fini)),
)
return (C.int)(val)
}
(see the full code here)
Unfortunately, Sysmon doesn’t log the environment variables a program was launched with, most likely
due to size constraints, but if it did so you should be able to see the LD_PRELOAD
, which is not commonly used in production
environments, and will be pointing to the malicious library.
Test #5 - In-memory patching
The final technique to discuss is making use of in-memory patching, to achieve the same goal as LD_PRELOAD
, but without
using the environment variable.
Back in 1998, Silvio Cesare wrote a paper on
Unix ELF parasites and viruses.
In the paper he wrote about how to patch ELF binaries to inject code at the program’s entrypoint, running custom
shellcode in between when the program has been execve
‘d, and the program’s “main” function.
The awesome Binjection library has implemented Silvio’s technique in Go, so I was able to use their library and my in-memory loader code to:
- Read a binary to run from disk
- Patch the entrypoint with custom shellcode that alters
argv
before returning to the real entrypoint - Run the patched binary from memory using
memfd
.
The full code I called injector
, and is available here.
The shellcode to alter the arguments at the ELF entrypoint is actually very simple, as at the start of the ELF execution the kernel
will place the memory addresses to the arguments on the top of the stack, so as long as you don’t care about memory corruption, to overwrite the first
argument from AAAA
to BBBB
the shellcode simply looks like:
; Get address of to argv[1] string
mov rax, [rsp+0x10]
; overwrite argv[1] string to "BBBB\0"
; which in hex is: 42 42 42 42 00
mov [rax], dword 0x42424242
mov [rax+4], byte 0x00
; Reset rax to 0
xor rax, rax
The end result looks like this:
$> /path/to/injector /path/to/shellcode.bin /path/to/basic AAAA
PID 28638
PPID 16961
argc 2
argv[0] from_injector
argv[1] BBBB
Sleeping for 60 seconds so you can lookup the PID
# Check ps output for pid, note the original arguments and not what `basic` saw
$> ps aux | grep 28152
path 28638 0.0 0.0 703064 2876 pts/1 Sl+ 16:34 0:00 from_injector BBBB
Sysmon for Linux will log the loader fully, but again will have some trouble with the loaded binary, only recording /memfd
:
Event SYSMONEVENT_CREATE_PROCESS
RuleName: -
UtcTime: 2022-01-02 05:36:48.387
ProcessGuid: {35dd0383-39f0-61d1-a01b-4b0000000000}
ProcessId: 28638
Image: /path/to/injector
FileVersion: -
Description: -
Product: -
Company: -
OriginalFileName: -
CommandLine: /path/to/injector /path/to/shellcode.bin /path/to/basic AAAA
CurrentDirectory: /path/to/
User: path
LogonGuid: {35dd0383-25d1-61d1-e803-000001000000}
LogonId: 1000
TerminalSessionId: 178
IntegrityLevel: no level
Hashes: -
ParentProcessGuid: {00000000-0000-0000-0000-000000000000}
ParentProcessId: 16961
ParentImage: -
ParentCommandLine: -
ParentUser: -
Event SYSMONEVENT_CREATE_PROCESS
RuleName: -
UtcTime: 2022-01-02 05:36:48.387
ProcessGuid: {35dd0383-39f0-61d1-2700-480000000000}
ProcessId: 28638
Image: /memfd:
FileVersion: -
Description: -
Product: -
Company: -
OriginalFileName: -
CommandLine: from_injector AAAA
CurrentDirectory: /path/to/
User: path
LogonGuid: {35dd0383-25d1-61d1-e803-000001000000}
LogonId: 1000
TerminalSessionId: 178
IntegrityLevel: no level
Hashes: -
ParentProcessGuid: {00000000-0000-0000-0000-000000000000}
ParentProcessId: 16961
ParentImage: -
ParentCommandLine: -
ParentUser: -
Sysmon for Linux doesn’t currently log the hash of the image being executed. If it did, the Sha256 hash of the binary launched would be the hash for the original binary+shellcode:
$> sha256sum /path/to/basic
9cadc2121a28ffe4c9c30afe093794206fdc429b8d56b4b0ddae70ac9dc993cd /path/to/basic
# the patched binary can be read from the proc filesystem:
$> sha256sum /proc/28152/exe
0a21c7e1bb346db045e2d182ec2678f13d431689ff2d0a8ab83e641bd94a5b4f /proc/28152/exe
The patched binary hash would be extremely uncommon, possibly unique, and doesn’t match the original binary on disk. Depending on the environment, the uncommonness of the hash may be enough to investigate.
Conclusions and final thoughts
Sysmon for Linux
Sysmon for Linux is looking like a great free monitoring tool for Linux. For people familiar with the Windows version of Sysmon, it provides a common configuration language that will definitely be useful for admin to maintain, and it’s process creation events would provide excellent insight into a lot of standard Linux malware attacks.
It is missing a few features that might catch more advanced attacks, such a file hashing and environment variable logging, and I didn’t explore its performance on a production-sized machine, but it’s a solid monitor to rival using AuditD, or other free monitors such as Tracee.
One thing not covered in this blog is the ability for an attacker to tamper with Sysmon’s configuration. Sysmon uses eBPF under the hood, and uses eBPF Maps to store the configuration (What event types to record, processes to ignore, etc.), which as I’ve written about in the past can be susceptible to tampering by an attacker with admin/root level privileges. While it is difficult for a program to protect itself for such a highly privileged attacker, it’s worth noting that the attack I described in my previous blog does work on Sysmon without generating any tamper-detection events. This is something the Sysmon team might consider detecting in the future, but it’s not an easy thing to prevent, and they may rightfully consider it out of scope for a free monitoring tool.
Writing low-level Go
Thanks to the Awgh, the Bloodhound Slack, and the
Binjection project, I was able to pretty easily get up to speed and write Go code
to do raw syscalls, LD_PRELOAD
ing, and other low-level tricks pretty easily. Raw pointer manipulation looks pretty ugly in Go compared
to C, but doing things like argument parsing and safe string manipulation was much nicer in Go.
Example Code
I’ve uploaded the source for all the different programs mentioned in this blog here: https://github.com/pathtofile/commandline_cloaking. With examples in both C and Go, it has these programs:
- Basic’s argument printing
- Dodgy’s
argv
manipulation LD_PRELOAD
libraries- In-memory loading to alter
argv
after execution - ELF Patching to alter
argv
after execution - Sample Sysmon for Linux configuration and setup