Quantcast
Channel: CodeSection,代码区,Linux操作系统:Ubuntu_Centos_Debian - CodeSec
Viewing all articles
Browse latest Browse all 11063

Linux process infection (part I)

$
0
0

Among the different tasks that aRed Team should carry out, there is one that is remarkable by its intrinsic craftsmanship: putting anAPT inside a computer system and ensuring its persistence. Unfortunately, most of this persistence mechanisms are based on keeping copies of an executable file in different locations, with one or more activation techniques (e.g. shell scripts, aliases, links, system boot scripts, etc.), and therefore a Blue Team’s security expert would only need to locate a working copy of the file and analyze it in his/her computer.

Although the security expert will find out what is going on sooner or later, it’s also true that some techniques can be implemented in order to difficult (or at least, delay) the detection of the APT in the infected machine. In this series of articles, we will detail a persistence mechanism based on the process tree instead of regular filesystem-based storage.

Prerequisites

This technique will be carried out in a x86-64 GNU/linux, although the theory could be easily extended to any operating system with a more or less complete debugging API. The requisites would be minimal: any modern GCC version would do the job.

Using the address space of other processes as warehouse

The intuition behind this technique is to use the address space of the running non-privileged processes as a storage area by injecting two threads in them: the first thread would try to infect the rest of processes, while the other will contain the payload (which, in this case, will just ensure file-system persistence). If the file is deleted, it will be restored with a different name.

It’s important to have in mind that this technique will be strongly limited by the machines uptime, and therefore it should be used in systems that are not intended to be frequently restarted. In other systems, it could be seen as a complementary persistence mechanism.

Shaping the injection

Obviously, one of the most critical phases of this technique is the code injection itself. As it’s impossible to know where the code will be placed beforehand in the victim’s address space, the code should be PIC (position-independent code). This suggests almost immediately the usage of dynamic libraries, as they can be laid out in memory practically “as is”. However, this has some cons:

Most of the injected information will be metadata (headers and son on) The code necessary to parse and load a library, although not being excessively complex, will not be negligible in comparison with the payload’s size Shared libraries use a broadly known file format, and make the resulting file easily analyzable

Ideally, the injection would be as small as possible: a couple of code pages, maybe an additional page for data and that would be it. All this is possible with linker scripts. However, for this proof of concept, we will content ourselves with a shared library as “first container”.

Another restriction to have in mind is that the target process does not need to be loaded as a dynamic executable (and, therefore, the C library may not be dynamically loaded). Also, manual symbol resolution on a loaded shared library is painful, ABI-dependent and barely maintainable. This means that many standard C functions will be reimplemented by hand.

Also, this injection will be based on the ptrace system call. If the process is not privileged enough (or this feature is explicitly disabled by the administrator), this technique will simply not work.

And finally, restrictions on dynamic memory usage will show up too. The usage of dynamic memory involves dealing with the heap, whose internal structure is far from being standard. In general, it is not desirable to keep a large memory footprint in the program’s address space. Dynamic memory should be used seldomly to reduce the footprint as much as possible.

Roadmap

This proof of concept will do the following:

The library will hold 2 entry points. The location of these entry points will be known beforehand (as they would be at fixed distance from the beginning of the executable) and will correspond to the beginning of the main function of the injected threads. The infection thread will list every running process in the system, locating those that can be potentially attacked. A ptrace(PTRACE_SEIZE) will be attempted against each process, and its memory read in order to detect whether it is already infected. In order to prepare the target address space, system calls must be injected. These system calls must allocate the necessary memory pages to store the injected code. Spawn both threads and continue the execution of the debugged process.

Each one of these phases requires some careful preparation that will be detailed in the following sections.

Preparing the environment

In order to keep the code as clean as possible, a small C program compiled as shared library will be used as starting point. Additionally, in order to run tests before the program is totally autonomous, another small C program that runs specific symbols in a library will be provided. In order to ease the overall development, a Makefile with all the build rules will be also included.

For the entry points of the injectable library, the following template will be used:

void persist(void) { /* Implement me */ } void propagate(void) { /* Implement me */ }

The program that will perform the initial execution of the entry points will be named “spawn.c” and will look like this:

#include #include #include int main(int argc, char *argv[]) { void *handle; void (*entry)(void); if (argc != 3) { fprintf(stderr, "Usagen%s file symboln", argv[0]); exit(EXIT_FAILURE); } if ((handle = dlopen(argv[1], RTLD_NOW)) == NULL) { fprintf(stderr, "%s: failed to load %s: %sn", argv[0], argv[1], dlerror()); exit(EXIT_FAILURE); } if ((entry = dlsym(handle, argv[2])) == NULL) { fprintf(stderr, "%s: symbol `%s' not found in %sn", argv[0], argv[2], argv[1]); exit(EXIT_FAILURE); } printf("Symbol `%s' found in %p. Jumping to function...n", argv[2], entry); (entry) (); printf("Function returned!n"); dlclose(handle); return 0; }

And finally, the Makefile that will compile both programs will be the following:

CC=gcc INF_CFLAGS=--shared -fPIE -fPIC -nostdlib all : injectable.so spawn injectable.so : injectable.c $(CC) $(INF_CFLAGS) injectable.c -o injectable.so spawn : spawn.c $(CC) spawn.c -o spawn -ldl

And it will be enough by running make to compile everything:

% make (…) % ./spawn injectable.so propagate Symbol `propagate' found in 0x7ffff76352ea. Jumping to function... Function returned! System calls

One noticeable apect about the Makefile above is that injectable.so is being compiled with -nostdlib (this was a requisite), and therefore we will not have access to the high level C system call interface. To overcome this restriction, a set of hybrid C and inline assembly will be needed in order to interact with the operating system.

As general rule, x86-64 Linux system calls are performed through the syscall instruction (while in older x86 systems interrupt 0x80 was used instead). In any case, the underlying idea is the same: registers are populated with system call arguments and then the system is called via some special instruction. The contents of %rax are initialized with the system call function code, and its arguments are passed in order %rdi, %rsi, %rdx, %r10, %r8 and %r9. The return value is stored in %rax, and errors are signaled with a negative return value (which corresponds to the inverted errno value). So, a simple “hello world” in assembly using the write() system call may look like

Viewing all articles
Browse latest Browse all 11063

Trending Articles