Detecting insiders on GNU/Linux servers

Yet another auditd ruleset

Intrusion detection on Linux systems is an information security subject that does not get as much light as intrusion detection on other operating systems such as Microsoft Windows. One theory I heard of that may explain this situation is that attackers usually target more Windows workstations of employees inside organizations to gain a foothold rather than Linux servers. I found that theory to be false in multiple incident response cases over the years. Attackers often target web servers or other internet-facing services, which are mostly running under the GNU/Linux operating system (sometimes under BSD) as a starting point in conducting attacks against organizations.

An attacker, once he gains initial foothold on a GNU/Linux system, usually follow one or more of the below purposes:

  • performing reconnaissance
  • evading restricted environments
  • elevating his privileges
  • obtaining secrets
  • persisting

Meanwhile he pursue these, he also wants to avoid detection, so he will try to cover his traces. In order to achieve these goals though, his methods almost always enter in one or more of the following categories of actions:

  • read a file
  • write a file
  • execute a binary
  • load a kernel module

This is partly inherent to the “everything is a file” philosophy of GNU/Linux systems and it makes detection rely on these actions.

The present article aims at detecting some malicious actions with auditd rules. This is not the only method to achieve it. However, auditd is a mature, kernel-implemented way of detection and is available in most, if not all, GNU/Linux distributions. It seemed to me a good choice to start detecting at a low cost. As detection is much of a cat-and-mouse game, the hereby presented methods will not be enough to detect all ways of elevating one’s privileges or persist on a system, nor will they address future ways of performing above listed actions.

Moreover, the mantra for these kind of detection rules should be “if this rule match, the behaviour is suspicious”, as much as possible I am trying not to raise alerts when a legit user is typing command lines on a GNU/Linux machine that a system administrator could type. These are usually time-consuming alerts for a SOC that are impossible to distinguish from legit behaviour. Here are some example of such commands that I have seen detected in existing signature rulesets:

whoami
ss -ltpn
ls /etc/cron.d
cat /etc/passwd

The above are, not enough to raise alerts. It is however good-to-have material if some other action triggered a signature for a given user but cannot be considered a malicious behaviour by itself.

I hope the presented rules will give you ideas that you may not find in other sets available online, they are mostly based on incident response and penetration testing experience.

The ruleset is also made for a GNU/Linux server, not a user workstation, keep it in mind. A server has less way tools installed and less user activity while the number of daemons it is running may be greater than a user workstation.

All the rules presented here are available on a github repository.

Reconnaissance

Sniffing the network

Sniffing the network is a typical reconnaissance action that you want to detect on an internal network. While a network administrator may run a tcpdump command from time to time, it should be considered a suspicious activity as this is a common way for attackers to gather network information such as IP addresses, protocols used at link-layer etc.

There are two actions you want to spot:

  • installing a network sniffer
  • using a network sniffer

tcpdump, a well-known sniffer, is usually is installed on /usr/bin, so a write to that location can detect its installation.

Associated auditd rules:

# tcpdump install/modification
-w /usr/bin/tcpdump -p wa -k detect_tcpdump_install

# tcpdump use
-a always,exit -F path=/usr/bin/tcpdump -F perm=x -k detect_tcpdump_exec

Attackers also make intensive use of another well-known network sniffer: wireshark. While unusual to execute on the target server (for better opsec and safety tcpdump will be used and the resulting pcap will be downloaded and analyzed offline) , you still want to detect it.

Associated auditd rules:

# wireshark install/modification
-w /usr/bin/wireshark -p wa -k detect_wireshark_install
-w /usr/bin/tshark -p wa -k detect_tshark_install

# tshark/wireshark use
-a always,exit -F path=/usr/bin/wireshark -F perm=x -k detect_wireshark_exec
-a always,exit -F path=/usr/bin/tshark -F perm=x -k detect_tshark_exec

Scanning the network

Apart from passive actions an attacker could conduct on the network, he usually will also perform active actions such as scanning targets. The go-to tool for that is the Network Mapper: nmap.

Associated auditd rules:

# nmap install/modification
-w /usr/bin/nmap -p wa -k detect_nmap_install

# nmap use
-w /usr/bin/nmap -p x -k detect_nmap

Privilege escalation

Local user or group addition

Adding a user to the system or adding an existing user inside a privileged group could constitute a privilege escalation due to poor file permissions on key Linux files. Exploiting a privileged binary having write permissions onto one of these key files could also be a way of achieving the same thing. But this could also be categorized as persistence, I chose to put it in this section arbitrarily.

On a GNU/Linux system this translates by modifications of /etc/passwd, /etc/shadow, /etc/group or /etc/gshadow files.

Associated auditd rules:

# User database modification
-w /etc/passwd -p wa -k detect_modify_passwd
-w /etc/shadow -p wa -k detect_modify_shadow

# Group database modification
-w /etc/group -p wa -k detect_modify_group
-w /etc/gshadow -p wa -k detect_modify_gshadow

sudoers addition

Same goes for the users listed in the sudoers file, an attacker having write permissions on /etc/sudoers could add himself or a group he is in into the file. For the same reason, it could be used as a persistence mean as well.

Associated auditd rules:

# Sudoers addition
-w /etc/sudoers -p wa -k detect_modify_sudoers
-w /etc/sudoers.d/ -p wa -k detect_modify_sudoers_d

set*id() syscalls

System calls to setuid() or setgid() is a common privilege escalation technique used in numerous exploits. You want to detect when this is done towards a privileged UID such as root’s but from an supposedly unprivileged one.

Associated auditd rules:

# Set*id() function family calls to privileged UID (root) 
-a always,exit -S setuid -S setgid -S setreuid -S setregid -F a0=0 -F uid!=0 -k detect_root_setuid_setgid

Note that this rule could (and should) be extended to any UID on your perimeter that is a privileged user.

This rule can be tested with a simple C program:

#include <stdio.h>
#include <unistd.h>

int main()
{
        if (setuid(0) == -1)
                printf("setuid failed\n");
        printf("%d\n", getuid());
}

Unprivileged user shell:

$ gcc -o setuid setuid.c
$ ./setuid 
setuid failed
1000

Auditor:

# ausearch --start this-week -k detect_setuid
time->Mon Apr 21 05:07:02 2025
type=PROCTITLE msg=audit(1745226422.803:4276): proctitle="./setuid"
type=SYSCALL msg=audit(1745226422.803:4276): arch=c000003e syscall=105 success=no exit=-1 a0=0 a1=7ffc11084808 a2=7ffc11084818 a3=7f87c451f758 items=0 ppid=8444 pid=611300 auid=1000 uid=1000 gid=1000 euid=1000 suid=1000 fsuid=1000 egid=1000 sgid=1000 fsgid=1000 tty=pts2 ses=2 comm="setuid" exe="/tmp/setuid" subj=unconfined key="detect_setuid_setgid"

Code injection

Code injection in a running process is a classic way for an attacker to escalate privileges through vulnerable binaries with the help of shellcodes. Under GNU/Linux systems, a well-known system call serve the purpose of writing another process memory: ptrace(). But this is not the only method, both the procfs pseudo filesystem and the process_vm_writev() system call can also be used.

Whatever the technique used, what you want to detect are memory writes to another process.

Associated auditd rules:

# Process injection
# Memory write with ptrace(PTRACE_POKETEXT, ...)
-a always,exit -S ptrace -F a0=4 -k detect_injection_ptrace_poketext

# Memory write with process_vm_writev()
-a always,exit -S process_vm_writev -k detect_injection_vm_writev

Note that it I did not add a rule for procfs writes, this is because the path to the memory map of a process is unpredictable as it contains the process pid. Such as: /proc/<pid>/map. Unfortunately auditd does not allow for rules with globbing in paths.

A few programs to test:

  1. ptrace() (from another article of this blog)

Unprivileged user shell:

$ gcc -o ptrace ptrace.c
$ ./ptrace /usr/bin/yes
Writing PID: 617889 Addr: 0x69274810 Buf: 0x096aff31
Writing PID: 617889 Addr: 0x69274818 Buf: 0x4dd68948
Writing PID: 617889 Addr: 0x69274820 Buf: 0x076a5a41
Writing PID: 617889 Addr: 0x69274828 Buf: 0x5178c085
Writing PID: 617889 Addr: 0x69274830 Buf: 0x58296a50
Writing PID: 617889 Addr: 0x69274838 Buf: 0x0f5e016a
Writing PID: 617889 Addr: 0x69274840 Buf: 0x97483b78
Writing PID: 617889 Addr: 0x69274848 Buf: 0x007fbb01
Writing PID: 617889 Addr: 0x69274850 Buf: 0x106ae689
Writing PID: 617889 Addr: 0x69274858 Buf: 0x4859050f
Writing PID: 617889 Addr: 0x69274860 Buf: 0x74c9ff49
Writing PID: 617889 Addr: 0x69274868 Buf: 0x6a006a58
Writing PID: 617889 Addr: 0x69274870 Buf: 0x0ff63148
Writing PID: 617889 Addr: 0x69274878 Buf: 0x79c08548
Writing PID: 617889 Addr: 0x69274880 Buf: 0x0f5f016a
Writing PID: 617889 Addr: 0x69274888 Buf: 0x48050f5a
Writing PID: 617889 Addr: 0x69274890 Buf: 0x0000e6ff

Auditor:

# ausearch --start this-week -k detect_injection_ptrace_poketext
time->Mon Apr 21 05:20:08 2025
type=PROCTITLE msg=audit(1745227208.397:4420): proctitle=2E2F707472616365002F7573722F62696E2F796573
type=SYSCALL msg=audit(1745227208.397:4420): arch=c000003e syscall=101 success=yes exit=0 a0=4 a1=96da1 a2=7f6669274890 a3=e6ff items=0 ppid=8444 pid=617888 auid=1000 uid=1000 gid=1000 euid=1000 suid=1000 fsuid=1000 egid=1000 sgid=1000 fsgid=1000 tty=pts2 ses=2 comm="ptrace" exe="/tmp/ptrace" subj=unconfined key="detect_injection_ptrace_poketext"
  1. process_vm_writev() (stolen from that good article)
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <sys/uio.h>

int main(int argc, char *argv[])
{
        int pid = atoi(argv[1]);

        // Initialize local and remote iovec structs used to perform the syscall
        struct iovec local[1];
        struct iovec remote[1];

        // Place our data in the local iovec 
        local[0].iov_base = "aaa";
        local[0].iov_len = 3;

        // Point the remote iovec to the address in the remote process
        remote[0].iov_base = (void *)0x12345678;
        remote[0].iov_len = 3;

        // Write the local data to the remote address
        process_vm_writev(pid, local, 1, remote, 1, 0);
}

Unprivileged user shell (note that trying to inject init is a bad idea and will fail):

$ gcc -o process_vm_writev process_vm_writev.c
$ ./process_vm_writev 1

Auditor:

ausearch --start this-week -k detect_injection_vm_writev
time->Mon Apr 21 05:13:54 2025
type=PROCTITLE msg=audit(1745226834.016:4290): proctitle=2E2F70726F636573735F766D5F7772697465760031
type=SYSCALL msg=audit(1745226834.016:4290): arch=c000003e syscall=311 success=no exit=-1 a0=1 a1=7fff5691eaf0 a2=1 a3=7fff5691eae0 items=0 ppid=8444 pid=614707 auid=1000 uid=1000 gid=1000 euid=1000 suid=1000 fsuid=1000 egid=1000 sgid=1000 fsgid=1000 tty=pts2 ses=2 comm="process_vm_writ" exe="/tmp/process_vm_writev" subj=unconfined key="detect_injection_vm_writev"

Code compilation

In order to use exploits, attacker often compile it on the system it will be executed on. Installing compilers on servers is a bad idea as it will serve that purpose while having no legit use (distributions packages binaries, not code). What we want is detecting both installation of compilers and use of these.

Associated auditd rules:

# Compiler install
-w /usr/bin/gcc -p wa -k detect_compiler_gcc_install
-w /usr/bin/g++ -p wa -k detect_compiler_gcc_plusplus_install
-w /usr/bin/clang -p wa -k detect_compiler_clang_install
-w /usr/bin/clang++ -p wa -k detect_compiler_clang_plusplus_install

# Compiler usage
-a always,exit -F path=/usr/bin/gcc -F perm=x -k detect_compiler_gcc
-a always,exit -F path=/usr/bin/g++ -F perm=x -k detect_compiler_gcc_plusplus
-a always,exit -F path=/usr/bin/clang -F perm=x -k detect_compiler_clang
-a always,exit -F path=/usr/bin/clang++ -F perm=x -k detect_compiler_clang_plusplus

Restricted environments evasion

Container escape

There are numerous documented container evasions out there and it is difficult to detect them all. A well-known one to escape a privileged container is to mount the host’s filesystem. One usually achieve this by locating the host root filesystem and executing the mount system call with the corresponding device in /dev.

As an example, we can take the setup listed in that article. Basically it is a docker container on a host with LVM. The host filesystem is on the /dev/dm-0 block, which is predictable as it is the first exposed device by LVM through the device mapper. There are no reasonable reason I can see for re-mounts of the host root partition from such a container, a specific rule can be written.

Associated auditd rules:

# Container evasion via host root FS mount: mount("/dev/dm-0", ...)
-a always,exit -S mount -F a0=/dev/dm-0 -F auid!=0 -k detect_container_evasion_privileged_mount

Once again, the absence of support for globbing in the rules prevents the writing of a more generic rule (for all LVM-style devices for example). And the device the root partition might be on is not necessarily dm-0.

Lateralization

RDP

In the Microsoft Windows world, RDP is a well-known living-off-the-land protocol widely used by attackers to jump from a machine to another. It is not rare during incident response to find a GNU/Linux machine accessing another one in the Active Directory domain this way.

Associated auditd rules:

# RDP tools install
-w /usr/bin/xfreerdp -p wa -k detect_rdp_xfreerdp_install_1
-w /usr/local/bin/xfreerdp -p wa -k detect_rdp_xfreerdp_install_2

# RDP use
-w /usr/bin/xfreerdp -p x -k detect_rdp_xfreerdp_1
-w /usr/local/bin/xfreerdp -p x -k detect_rdp_xfreerdp_2

As xfreerdp is not the only tool one can use to RDP from a GNU/Linux system, you may want to investigate if there are others on your perimeter.

Persistence

SSH

Adding an SSH key in order to come back on a system is a common way of achieving persistence. If the user adding the key is also different from the SSH configuration folder owner, this sounds suspicious. root could decide to add keys for other users but once again, you want to know if a sysadmin is doing that.

Associated auditd rules:

# Add root SSH keys as non-root user
-w /root/.ssh/authorized_keys -p wa -F auid!=0 -k detect_add_SSH_root_key 

Cron task

Adding a scheduled task is a way an attacker can ensure its binary or script, if it dies, will be executed again, either on host reboot or periodically while the host is up. The historic way of doing so on GNU/Linux systems is via a cron job.

Associated auditd rules:

# Non-root users adding privileged cron jobs
-w /etc/crontab -p wa -F auid!=0 -k detect_add_cron_job_crontab
-w /etc/anacrontab -p wa -F auid!=0 -k detect_add_cron_job_anacrontab
-w /etc/cron.d/ -p war -F auid!=0 -k detect_add_cron_job_d
-w /etc/cron.hourly/ -p war -F auid!=0 -k detect_add_cron_job_hourly
-w /etc/cron.daily/ -p war -F auid!=0 -k detect_add_cron_job_daily
-w /etc/cron.weekly/ -p war -F auid!=0 -k detect_add_cron_job_weekly
-w /etc/cron.monthly/ -p war -F auid!=0 -k detect_add_cron_job_monthly
-w /etc/cron.yearly/ -p war -F auid!=0 -k detect_add_cron_job_yearly

Systemd task

A more modern approach is to create a systemd service (a.k.a. task). The unit files for these services have different locations, some of which cannot be found in the official documentation. A useful tool I found to know what paths should be checked is systemd-analyze:

# uname -a
Linux kali 6.8.11-amd64 #1 SMP PREEMPT_DYNAMIC Kali 6.8.11-1kali2 (2024-05-30) x86_64 GNU/Linux
# systemd-analyze --system unit-paths
/etc/systemd/system.control
/run/systemd/system.control
/run/systemd/transient
/run/systemd/generator.early
/etc/systemd/system
/etc/systemd/system.attached
/run/systemd/system
/run/systemd/system.attached
/run/systemd/generator
/usr/local/lib/systemd/system
/usr/lib/systemd/system
/run/systemd/generator.late

Associated auditd rules:

# Non-root users adding "system" systemd unit files
-w /etc/systemd/system/ -p wa -F auid!=0 -k detect_add_systemd_etc_system
-w /lib/systemd/system/ -p wa -F auid!=0 -k detect_add_systemd_lib
-w /usr/lib/systemd/system/ -p wa -F auid!=0 -k detect_add_systemd_usr_lib
-w /usr/local/lib/systemd/system/ -p wa -F auid!=0 -k detect_add_systemd_usr_local_lib

udev rules

Adding udev rules is a nice persistence mean and is often overlooked by analysts writing detection signatures. Non-root users adding such rules is highly suspicious while root adding some does not happen so often either so you want to catch all added udev rules.

Associated auditd rules:

# Udev rules
-w /etc/udev/rules.d/ -p wa -k detect_udev_etc 
-w /usr/lib/udev/rules.d/  -p wa -k detect_udev_usr_lib

Detection evasion

Shared memory (/dev/shm)

To avoid writing to disks on a host, attackers try to keep malwares in RAM. One way of doing this on a GNU/Linux system is to use shared memory, exposed to userland in /dev/shm. The shm_open()function is implemented to write to that path on Linux. Unfortunately, this is a standard library function, not a system call, the auditd rule I found relevant to detect it while not raising too many false positives was on the target /dev/shm path.

Associated auditd rules:

# Shared memory use
-w /dev/shm -p wa -k detect_shm_dev_write

A simple way to test is to use the shm_open() function to write data in a shared memory space:

#include <sys/mman.h>
#include <sys/stat.h>        /* For mode constants */
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>

int main()
{
        char *shm_name = "/shm_test";
        char *shm_data = "ciao";
        size_t shm_len = strlen(shm_data);

        int fd = shm_open(shm_name, O_RDWR | O_CREAT, S_IRUSR | S_IWRITE);

        if (fd == -1)
        {
                printf("shm_open failed: %s\n", strerror(errno));
                return 1;
        }

        if (ftruncate(fd, shm_len) == -1)
        {
                printf("ftruncate failed: %s\n", strerror(errno));
                return 1;
        }

        void *addr = mmap(NULL, shm_len, PROT_WRITE, MAP_SHARED, fd, 0);

        if (addr == MAP_FAILED)
        {
                printf("mmap failed: %s\n", strerror(errno));
                return 1;
        }

        memcpy(addr, shm_data, shm_len);

        if (munmap(addr, shm_len) == -1)
        {
                printf("munmap failed: %s\n", strerror(errno));
                return 1;
        }

        if (shm_unlink(shm_name) == -1)
        {
                printf("shm_unlink failed: %s\n", strerror(errno));
                return 1;
        }

        return 0;
}

Unprivileged user shell:

$ gcc -o shm_open shm_open.c
$ ./shm_open

Auditor


# ausearch --start this-week -k detect_shm
time->Mon Apr 21 05:01:38 2025
type=PROCTITLE msg=audit(1745226098.316:4259): proctitle="./shm_open"
type=PATH msg=audit(1745226098.316:4259): item=1 name="/dev/shm/shm_test" inode=34 dev=00:19 mode=0100600 ouid=1000 ogid=1000 rdev=00:00 nametype=DELETE cap_fp=0 cap_fi=0 cap_fe=0 cap_fver=0 cap_frootid=0
type=PATH msg=audit(1745226098.316:4259): item=0 name="/dev/shm/" inode=1 dev=00:19 mode=041777 ouid=0 ogid=0 rdev=00:00 nametype=PARENT cap_fp=0 cap_fi=0 cap_fe=0 cap_fver=0 cap_frootid=0
type=CWD msg=audit(1745226098.316:4259): cwd="/tmp"
type=SYSCALL msg=audit(1745226098.316:4259): arch=c000003e syscall=87 success=yes exit=0 a0=7ffde153fa60 a1=2f a2=fffffffffffffff8 a3=7faa26833980 items=2 ppid=8444 pid=608617 auid=1000 uid=1000 gid=1000 euid=1000 suid=1000 fsuid=1000 egid=1000 sgid=1000 fsgid=1000 tty=pts2 ses=2 comm="shm_open" exe="/tmp/shm_open" subj=unconfined key="detect_shm_dev_write"

Conclusion

auditd is a low-cost solution to begin host detection on GNU/Linux systems. Rules are simple to write and can be deployed easily thanks to how well-spread the auditd daemon is on Linux distributions. However it has practical limitations:

  • Rules over standard library functions cannot be written as it is a kernel mechanism and only system calls can be watched
  • It does not support globbing in paths or system call arguments, making writing of generic signatures impossible
  • It does not detect all kind of changes made to files. For example, echo <whatever> >> /etc/sudoers will not trigger the sudoers rule of this article (see here for why)

I hope you found new ideas in these lines, make attackers' lives harder, use auditd and implement detection.

References