Hunting for Linux library injection with Osquery

June 20, 2019  |  Jaime Blasco

When analyzing malware and adversary activity in Windows environments, DLL injection techniques are commonly used, and there are plenty of resources on how to detect these activities.

When it comes to Linux, this is less commonly seen in the wild.

I recently came across a great blog from TrustedSec that describes a few techniques and tools that can be used to do library injection in Linux. In this blog post, we are going to review some of those techniques and focus on how we can hunt for them using Osquery.

LD_PRELOAD

LD_PRELOAD is the easiest and most popular way to load a shared library in a process at startup. This environmental variable can be configured with a path to the shared library to be loaded before any other shared object.

For most of the blog, we will be using the examples available in GitHub, listed here.

Let’s use sample-target as the target process and sample-library as the shared library we will be injecting.

We can utilize the ldd tool to inspect the shared libraries that are loaded into a process. If we execute the sample-target binary with ldd we can see that information.

LD Preload to load a shared libary in a process at startup

Linux-vdso.so.1, is a virtual dynamic shared object that the kernel automatically maps into the address space in every process. Depending on the architecture, it can have other names.

Virtual dynamic shared object

Libc.so.6 is one of the dynamic libraries that the sample-target requires to run, and ld-linux.so.2 is in charge of finding and loading the shared libraries. We can see how this is defined in the sample-target ELF file by using readelf.

sample-target ELF file

Now, let’s set the LD_PRELOAD environment variable to load our library by executing.

export LD_PRELOAD=/home/ubuntu/linux-inject/sample-library.so; ldd /home/ubuntu/linux-inject/sample-target

Ubuntu executing LD_PRELOAD

We can see our sample-library being loaded now. We can also get more verbose information by setting the LD_DEBUG environment variable.

export LD_DEBUG=files

More verbose information on LD_PRELOAD

A simple way to hunt for malicious LD_PRELOAD usage with Osquery is by querying the process_envs table and looking for processes with the LD_PRELOAD environment variable set.

SELECT process_envs.pid as source_process_id, process_envs.key as environment_variable_key, process_envs.value as environment_variable_value, processes.name as source_process, processes.path as file_path, processes.cmdline as source_process_commandline, processes.cwd as current_working_directory, 'T1055' as event_attack_id, 'Process Injection' as event_attack_technique, 'Defense Evasion, Privilege Escalation' as event_attack_tactic FROM process_envs join processes USING (pid) WHERE key = 'LD_PRELOAD';

Query process to hunt for malicious LD_Preload usage

Since some monitoring and security software uses LD_PRELOAD for benign purposes, you will have to create a baseline of known processes in your environment using LD_PRELOAD.

A few benign examples we have encountered using LD_PRELOAD include the following.

From an attacker’s perspective, as TrustedSec mentions in their blog, there are some inconveniences on using LD_PRELOAD - mainly that you need to restart the process that you want to inject code into in order for it to work. Below, I’ve given an overview of other techniques that don’t require this.

In addition to LD_PRELOAD, there are other similar techniques an attacker could use to achieve the same results. For example, by setting the LD_LIBRARY_PATH environmental variable, one could specify a directory where the loader will try to find the required libraries first, so an attacker could create a modified version of libc.so, or other required shared libraries, and load malicious code into the process.

Finally, it is recommended to monitor changes to /etc/ld.so.conf and /etc/ld.so.conf.d/*.conf since they can be used for the same purpose. This can be done using Osquery’s FIM functionality by adding "/etc/ld.so.conf" and "/etc/ld.so.conf.d/%%" to the file_paths configuration.

As for particular examples in the wild, a recent one is a version of Winnti targeting Linux systems that was unveiled by Kris McConkey during his presentation “Skeletons in the supply chain,” given at the SAS conference earlier this year. You can find a detailed analysis by Intezer.

Linux Inject

The Linux inject tool can be used to load a shared library into a running process by using ptrace,  similar to the well-known DLL injection technique in Windows.

Let’s take a look at how it works. First, we attach to the target process using ptrace() and inject  the code that will be loading the library. Then, the loader code allocates memory using malloc(), copies the path of the shared library to the buffer and calls __libc_dlopen_mode() to load the shared library. Let’s give it a try.

./inject -n sample-target sample-library.so

library injection failed

It failed! What happened?

The source of this is a Linux security module called Yama that implements discretionary access control (DAC) for specific kernel functions such as ptrace.

You can check the current state by looking at /proc/sys/kernel/yama/ptrace_scope or using systemctl.

check current state using systemctl

As you can see in the docs, when ptrace_scope is set to 1, only a parent process can be debugged (this is the default in Ubuntu 18.04.2). When set to 3, no process can be debugged with ptrace and a reboot is required to change the value.

This is actually great from a defender’s perspective because it means that an attacker would have to modify the value of ptrace_scope before using ptrace.

We can take advantage of this and utilize Osquery’s system_controls table to query the current configuration value of ptrace_scope.

osquery> select  * from system_controls WHERE name == 'kernel.yama.ptrace_scope';

kernel yama

You can also utilize the following scheduled query in Osquery to monitor for changes to ptrace_scope.

"detection_ptrace_scope_changed": {

      "platform": "linux",

      "description": "Detects changes to kernel.yama.ptrace_scope",

      "query": "SELECT name, current_value, config_value from system_controls

  WHERE name == 'kernel.yama.ptrace_scope';",

      "interval": 3600,

      "removed": false

}    

We could also hunt for systems where the current value of ptrace_scope has been modified from the original one in /etc/sysctl.conf.

SELECT name, subsystem, current_value, config_value from system_controls WHERE name == 'kernel.yama.ptrace_scope' AND current_value != config_value;

Or, we could simply check for systems where ptrace is always allowed and flag this as a potential security issue.

SELECT name, subsystem, current_value, config_value from system_controls WHERE name == 'kernel.yama.ptrace_scope' AND current_value = 0;

More importantly, when ptrace is blocked, a Syslog message is recorded that can be used to detect unsuccessful attempts to use ptrace.

Jun 10 20:59:24 ip-172-31-32-145 kernel: [955105.055910] ptrace attach of "./sample-target"[13134] was attempted by "./inject -n sample-target sample-library.so"[13148]

If you are already collecting Syslog messages in your security stack, I recommend you alert on multiple attempts to use ptrace in a system.

In Osquery, the following query can be used to monitor this specific syslog message.

SELECT * from syslog WHERE tag = 'kernel' AND message LIKE '%ptrace attach%';

monitor specific syslog

Ok, so now let’s go back to the linux-inject tool. In order for ptrace to work, the attacker would need to set ptrace_scope to 0.

echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope

linux inject for ptrace to work

ubuntu success

Let’s take a look at the process memory using Osquery’s process_memory_map table.

SELECT process_memory_map.*, pid as mpid from process_memory_map WHERE pid in (select PID from processes where name LIKE '%sample-target');

Osquery select memory map

Among other common memory regions, we can see some of the shared objects we are already familiar with (libc, ld, etc). In addition to that, the injected library, sample-library.so is also there.

We know that we are looking for memory regions that have execute permission, and we can also discard the original image and the regions marked as pseudo.

SELECT count(distinct(process_memory_map.pid)) from process_memory_map LEFT JOIN processes USING (pid) WHERE process_memory_map.path LIKE '/%' and process_memory_map.pseudo != 1 AND process_memory_map.path != processes.path AND process_memory_map.permissions LIKE '%x%';

query still too broad in osquery

That query is still too broad. Let’s check what the most common paths for the shared libraries are.

SELECT split(process_memory_map.path, '/', 0) AS folder, count(*) as cnt from process_memory_map LEFT JOIN processes USING (pid) WHERE process_memory_map.path LIKE '/%' AND process_memory_map.pseudo != 1 AND process_memory_map.path != processes.path AND process_memory_map.permissions LIKE '%x%' GROUP by folder order by cnt desc;

checking common paths

That makes sense; we can create a query that ignores common paths. This would help us hunt for shared libraries that are loaded from non-standard locations.

SELECT process_memory_map.*, pid as mpid from process_memory_map LEFT JOIN processes USING (pid) WHERE process_memory_map.path LIKE '/%' and process_memory_map.pseudo != 1 AND process_memory_map.path NOT LIKE '/lib/%' AND process_memory_map.path NOT LIKE '/usr/lib%' AND process_memory_map.path != processes.path AND process_memory_map.permissions LIKE '%x%';

process memory map

The TrustedSec blog mentions that in many cases attackers will remove the file from disk after loading it to make the analysis more difficult and avoid detection.

If we remove the injected .so file, we can validate with the following query, leveraging it to detect when the shared library has been deleted from disk.

SELECT process_memory_map.pid, process_memory_map.start, process_memory_map.end, process_memory_map.permissions, process_memory_map.offset, process_memory_map.path from process_memory_map LEFT join file USING (path) where pseudo != 1 AND process_memory_map.path NOT LIKE '/lib/%' AND process_memory_map.path NOT LIKE '/usr/lib%' AND process_memory_map.permissions LIKE '%x%' AND filename IS NULL and process_memory_map.inode !=0 AND process_memory_map.permissions = 'r-xp';

injected .so file

In terms of detecting the code that gets injected in the target process, we can use the following Yara rule.

yara table
ReflectiveSOInjectionInterestingly, when I searched for ELF files matching those patterns in Virustotal, I quickly discovered that the popular Pupy RAT actually uses Linux inject in the Linux client.

ReflectiveSOInjection is a tool based on linux-inject. The main difference is in the way the shared object is injected. In linux-inject, the shellcode uses __libc_dlopen_mode to load the shared object. ReflectiveSOInjection maps the shared object into memory and then forces the main program to call the ReflectiveLoader export. The ReflectiveLoader takes care of resolving functions, loading required libraries and map the program segments into memory.

Let’s use ReflectiveSOInjection on the same sample-target that we used with linux-inject.

ReflectiveSOInjection

If we take a look at the memory map, we can see that there is a new memory section marked as rwxp with an empty path.

memory map with new section marked as rwxp

As TrustedSec mentions in their blog, the advantage of this method is that the injected shared object doesn’t have to be on disk.

We can use the following query to hunt for this activity.

SELECT processes.name, process_memory_map.*, pid as mpid from process_memory_map join processes USING (pid) WHERE process_memory_map.permissions = 'rwxp' AND process_memory_map.path = '';

hunting for injected shared file

As a bonus detection, if the attacker is lazy and doesn’t modify the ReflectiveSOInjection code, by default the code is looking for an export with the name “ReflectiveLoader” in the injected shared library. So, we can write a simple Yara signature to detect shared libraries on disk with that export.

import "elf"


rule ReflectiveLoader : LinuxMalware {

                  meta:

                                    author = "AlienVault Labs"

                                    description = "Detects a shared object with the name of an export used by ReflectiveLoader"

                                    reference = "https://github.com/infosecguerrilla/ReflectiveSOInjection"

                  condition:

                                    uint32(0) == 0x464c457f and

                                    elf.type == elf.ET_DYN and

                                    for any i in (0..elf.symtab_entries): (

                                                      elf.symtab[i].name == "ReflectiveLoader"

                                    )     

}

GDB

The last method we are going to explore is using the Gnu Project Debugger, GDB to load a shared object. From an attacker’s perspective GDB may be already installed in the target system and it is less noisy than bringing your own tool. The advantage of using the GDB method is that we can take advantage of this.

Under the covers, this method is almost exactly the same as linux-inject. GDB uses ptrace to attach to a process and then calls the same __libc_dlopen_mode() function that we are familiar with to load the shared object.

gdb using ptrace

The results are the same as with the linux-inject and we can use the same query to hunt for this.

query gdb

In summary, we analyzed multiple ways an attacker can inject a shared object into a running process, and we shared different Osquery queries and detection ideas that blue teams can use to hunt for this behavior in their environments. In addition to that, we shared some examples of how these techniques are being used in the wild.

Share this with others

Get price Free trial