Remote Library Injection
Remote Library Injection
org
1 Foreword 2
2 Introduction 4
3 Loading a Library 6
3.1 Linux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
3.2 Windows . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
5 Potential Impacts 29
5.1 Worm/Rootkit Deployment Automation . . . . . . . . . . . . . . 29
5.2 Operating System Independence . . . . . . . . . . . . . . . . . . 30
5.3 Anti-Virus Nightmares . . . . . . . . . . . . . . . . . . . . . . . . 30
7 Conclusion 37
1
Chapter 1
Foreword
2
The authors would like to thank:
nologin For continued enthusiasm, motivation,
and editing assistance.
H D Moore, spoonm, For theorizing with the authors and offer-
thief ing always insightful perspectives.
“The Motivated” Everyone who is internally motivated and
driven to learn for their own satisfaction.
Family For understanding and support (in gen-
eral, not assembly language :-)
This document was last modified: April 06, 2004.
3
Chapter 2
Introduction
4
aware of no currently employed methodologies by which this can be detected1 .
This topic will be discussed in more detail in the Prevention and Detection
chapter (6).
The basic process used to perform library injection is directly dependent on the
context from which the library is injected from. This means that the methods
used to inject a library from the local machine versus injection from a remote
connection, such as an exploit, are done by different means. The focus of this
paper will be on the injection of libraries over remote connections as it empha-
sizes the danger of being exposed to a remote exploit that could in turn be
exploited by something that makes use of the topics discussed in this document.
At a high level, the approach used to inject a library through a remote exploit
is relatively straight forward. An exploit author would employ what is referred
to as Multi-Stage Shellcode, or multi-stage payloads, to allow himself the
added flexibility of being able to execute arbitrarily large payloads[13]. The first
stage would make use of a second topic, known as File Descriptor Re-use,
whereby the exploit attempts to locate the file descriptor from which the exploit
originated. Upon successfully locating the file descriptor, the first stage payload
would then read in the second, arbitrarily sized, payload and execute it[13].
It is in this second stage that an exploit author would send the payload for
downloading and injecting the library into the process that the exploit has
targeted. After the library has been loaded, all bets are off. The potential
impacts of a library being injected are discussed in depth in the Potential
Impacts chapter (5).
Without yet understanding the how associated with library injection, it is per-
tinent to consider potential prevention and detection mechanisms. These would
allow a person to defend or acknowledge a compromise that incorporates library
injection. These two points will be discussed in the Prevention and Detection
chapter (6).
Upon completion of this document the authors hope that the reader will have
a complete understanding regarding the concept of Library Injection, thus
enabling the reader to make educated and intelligent decisions as it pertains to
the topic at hand. The following chapters will vary in levels of technical detail,
but one should not be surprised to see code snippets and other very low-level
details.
1 This does not mean that all library injection methods cannot be detected; rather, it means
that current implementations do not have the ability to do so. On-Disk library injection, as
discussed later, can and will be detected by Anti-Virus scanners. However, In-Memory library
injection will not be.
5
Chapter 3
Loading a Library
Before understanding how library injection works, one must understand how a
library is loaded in the first place. The interfaces used to do this vary from plat-
form to platform and as such will be analyzed separately for the two platforms
of focus in this document: Linux and Windows.
3.1 Linux
The standard approach to loading a library in Linux, at least for most distribu-
tions, involves making use of the library [Link] which exports a small number
of functions for interfacing with dynamically loaded libraries. These functions
are actually wrappers for functions that are exported in [Link]. The three
core functions that [Link] provides are:
6
This function takes the opaque pointer retuned from dlopen as the handle
argument and the name of a symbol (e.g. gethostbyname) as the symbol
argument. On success, a pointer to the absolute VMA of the symbol is
returend. If the symbol does not exist in the library passed in, the return
value will be NULL.
3. int dlclose (void *handle);
This function will unload a previously loaded library by passing the opaque
pointer that was returned from dlopen as the handle argument. Upon
success, zero will be returned. Otherwise, non-zero is returned.
Like dlopen, dlclose has the property of indirectly calling the fini
symbol, or more correctly the symbol marked as a destructor, in the li-
brary that was loaded. This can be seen as analogous functionality to
registering a handler with atexit, but instead of running at process exit,
the destructor runs when the library is unloaded.
Though these three functions provide the basic functionality needed to interface
with dynamically loaded libraries, they are not linked to or used by all appli-
cations. Many applications have no need to interface with the dynamic loader
outside of initially resolving and loading dependent libraries which is taken care
of behind the scenes during the initialization portion of execution. As such,
one cannot assume that [Link] will be loaded in the context of a given pro-
cess. This fact will become important later during the chapter on Library
Injection Methods (4).
1. void * dl open (const char *file, int mode, const void *caller);
This function supplies the exact same functionality that dlopen from
[Link] provides. However, its calling convention differs such that in-
stead of using cdecl like [Link] does, dl open uses fastcall whereby
arguments are passed in registers instead of on the stack. For IA-32,
arguments are passed in the following registers:
eax = file
edx = mode
ecx = caller
The return value is exactly the same as dlopen.
2. void * dl sym (void *handle, const char *name, void *who);
This function supplies the exact same functionality that dlsym from [Link]
provides. Like dl open, dl sym uses fastcall linkage. For IA-32, ar-
guments are passed in the following registers:
7
eax = handle
edx = name
ecx = who
The return value is exactly the same as dlsym.
3. void dl close (void * map);
This function supplies the exact same functionality that dlclose from
[Link] provides. This function also uses fastcall. The map argument
is passed in the eax register on IA-32.
8
int access(const char *pathname, int mode);
The pathname argument, which is passed as ebx on IA-32, can be used
as the point of address validation. By passing in an invalid address, the
access system call will return EFAULT in the case that the pointer hap-
pens to be unreadable. If it is not unreadable, another error code will be
returned and that is a clear indication that the address is valid. If an un-
readable address is encountered or a readable address that does not match
the ELF signature, the current address being tried should be incremented
by PAGE SIZE and the loop should repeat itself. If a readable address
is found that matches the ELF signature, the next step is taken. This
address will henceforth be referred to as the absolute base address.
2. Check ELF image type
Upon finding a readable page that matches the ELF signature, the next
step in the process is to verify that the ELF image at the absolute base
address is a library, not an executable. This is done by checking to see if
the e type field of the ELF header is set to ET DYN. If it is not, the current
base address is incremented by PAGE SIZE and the the loop goes back to
step 1. If the ELF image at the absolute base address is a library, the
next step is taken.
3. Enumerate the Program Header Table
After determining that the image at the address is not only an ELF binary,
but also an ELF library, it becomes pertinent to locate the dynamic linkage
information that the library provides to the dynamic loader in order to
facilitate the resolving of dynamic symbols and the names of said symbols.
The way that the library does this is by having a mappable segment for
the dynamic section of the binary1 . This mapping information is stored in
the Program Header Table and each mappable segment has a type that
is used to instruct the interpreter of the library as to how to interpret the
contents of the mapped segment. In the case of the dynamic section, the
program header type, or the p type field, is PT DYNAMIC.
In order to locate the PT DYNAMIC entry, the Program Header Table must
be enumerated. The base address of the table is calculated by adding the
absolute base address with the e phoff attribute of the ELF header. The
number of entries in the table is stored in the ELF header in the e phnum
attribute. Enumeration is then done with standard pointer path as would
be expected during the enumeration of an array of a given data structure.
During the enumeration process, each entry’s p type attribute is com-
pared to PT DYNAMIC. If a match is not located, the loop increments to the
next index and continues. Otherwise, if a match is found, the dynamic
section mapping information has been located. The field of interest for
the dynamic entry is the p offset field which holds that file offset to the
dynamic section entry’s content. This offset is where the actual dynamic
1 The dynamic section is sometimes referred to as ’.dynamic’.
9
linkage information is stored. To convert it to an absolute address, all that
is necessary is to add the absolute base address with the offset specified
in p offset.
If no PT DYNAMIC entry is located, the absolute base address is incremented
by PAGE SIZE and the loop starts over at step 1.
4. Enumerate the dynamic section
Once the dynamic section entry’s content has been located, dynamic link-
age information can then be extracted such that it can later be used to
resolve the symbol dl open. There are two values that need to be ex-
tracted from the section. The first of these values is the offset to the
dynamic symbol table. This table holds an array of ElfXx Sym struc-
tures that make up the group of exported symbols that the library allows
external pieces of code to interact with. This value is identified by the
DT SYMTAB identifier. The second of the two values is the string table as-
sociated with the dynamic symbol table. This is needed due to the fact
that the name of the symbol must be compared with dl open in order to
determine if it is the right symbol or not. This value is identified by the
DT STRTAB identifier.
The dynamic section entry’s content is composed of an array of ElfXx Dyn
structures. Each array entry correlates an identifier with a given value. In
the case of DT SYMTAB and DT STRTAB, this value is an offset from the start
of the file to their respective contents. If neither of the two identifiers can
be located or only one of the two can be located, the absolute base address
is incremented by PAGE SIZE and the loop starts over at step 1. Once both
identifiers are located, the absolute base address should be added to both
of them in order to convert them into absolute addresses.
5. Enumerate the dynamic symbol table
The last step of the process involves enumerating the each of the dy-
namic symbol table entries and comparing each entry’s symbol name with
dl open. This is done by adding the symbol’s st name attribute to the
address of the dynamic symbol string table. The result should give back a
null terminated string for the name of the symbol. If the symbol names do
not match, the loop is repeated. Otherwise, the dl open symbol has been
found and the absolute address can be calculated by adding the st value
attribute to the absolute base address of the library. The result is a direct
virtual address that can be used to call the function.
One consideration that must be kept in mind during this phase is that it is
possible that a the symbol will not be located in a library. For instance, if
[Link] is loaded before [Link], which it nearly always is, the sym-
bol resolution code will encounter [Link] symbols before [Link]
symbols. As such, the symbol resolution code must be robust enough to
handle the scenario where it does not find any matching symbols. One ap-
proach to doing this involves doing a check at the beginning of the symbol
10
enumeration loop to see if the current symbol’s address has gone past or
is equal to the dynamic symbol string table’s address. In the event that
this is true, the absolute base address is incremented by PAGE SIZE and
the loop starts back at step 1. If the current symbol address is not greater
than or equal to the dynamic symbol string table’s address, it is safe to
assume that it is still still within the symbol table2 .
With the absolute memory address of dl open located, all that’s left to do is
load the library itself by calling the function.
3.2 Windows
2 This assumes that the dynamic symbol string table occurs after the dynamic symbol
table in memory. The current linker implementation on all the versions of Linux known to
the authors links libraries in this fashion. If this were not the case, an alternative solution
would be necessary.
11
Chapter 4
This document will discuss two methods by which a library can be injected
remotely. The two methods only differ in approach, but have the same desired
goal as outlined in the introduction. The first of these methods is known as
On-Disk Library Injection which, as the name implies, means that the li-
brary is written to disk and then loaded into the process’ address space. The
second of these methods is known as In-Memory Library Injection which
entails loading the library entirely from memory without any disk activity at
all.
The On-Disk method is the easiest of the two methods but also has the highest
risk of detection. At the time of this writing, Anti-Virus software is capable of
performing On-Access virus scanning which means that the virus scanner will
perform virus checks when a file is accessed, such as when editing or execution
occurs[11]. This means that when the payload used during the conceptual ex-
ploit writes the library to disk, those writes will undergo analysis by the virus
scanner and potentially be detected. Not only that, but the library will also
potentially undergo scanning upon opening and reading of the library during
the loading phase. If the library is detected as a virus, the show stops there.
As such, the On-Disk method should be seen as an inferior method as it suffers
from the same problems that plague the downloading and subsequent execution
of an actual executable.
The second method, In-Memory injection, is far less detectable. In fact, as
stated in the introduction, the authors’ are aware of no virus scanners that, as
they stand at the time of this writing, are capable of detecting this method.
Though virus scanners may detect earlier phases, such as the exploit transmis-
sion over the network, they cannot currently detect the actual library loading
which is the focus of this document.
The following sections will discuss the two implementations in detail and explain
12
how one might approach implementing them across multiple platforms.
4.1 On-Disk
As noted above, the On-Disk method is the process by which a library is written
to disk and subsequently loaded into a process’ address space. The logical steps
taken to do this are exactly the same between Linux and Windows, but varies
greatly in implementation due to some hurdles that must be jumped due to the
fact that the injection payload, or the things that actually perform the library
injection, is running as shellcode. This means that standard library functions
and most luxuries afforded to programmers of a given platform cannot be used,
at least not directly. Regardless of how detectable the On-Disk approach is, it
is nonetheless a viable method of injecting a library.
On both UNIX and Windows, the general approach to implementing the On-
Disk method involves writing a payload that reads in the library from a file
descriptor and then writes it somewhere on disk. Once the library is completely
downloaded, the payload would then call the respective library loading methods
for a given platform.
The following subsections will outline the implementation of the On-Disk method
for both Linux and Windows. The concepts applied to Linux are common to
most UNIX variants.
4.1.1 Linux
13
system call and passing 4 bytes for the len argument. This step makes it
possible to know exactly how many bytes should be read from the socket.
3. Read a chunk of the file from the socket
Read an arbitrarily sized chunk no larger than the amount of data left to
read into an intermediate buffer, such as a stack allocated buffer. Save
the number of bytes read as returned from the socket recv system call for
use in the next two steps.
4. Write the chunk to the opened file on disk
Write the contents of the buffer that was read in from the socket to the
file descriptor that was opened for the library. This write operation, using
the write system call, should use the number of bytes actually read from
the socket as the len parameter.
5. Subtract the number of bytes read from the library’s length
Subtract the number of bytes that were read from the socket from the
length of the library that was read in during step 2. This is used to track
how many bytes are actually left to be read from the socket.
6. If the length is non-zero, repeat steps 3-6
If the amount of length of the library left to be read is not yet zero, that
indicates that there is more data to be read. As such, steps 3-6 should be
repeated until the length does drop to zero.
7. Close the library’s file descriptor
After the entire library has been read in, the file descriptor for the library
should be closed.
8. Find the VMA for dl open
Before loading the library, the dl open function must be resolved. The
process to do this is discussed in the section on Loading a Library for
Linux (3.1).
9. Call dl open with the path to the library
After the address of dl open has been determined, all that remains is to
actually load the library from the disk. The path argument should be set
to the name of the file that was passed in to step 1. The mode parameter
should be set to 0x80000000 OR’d with the binding mode desired, such
as RTLD NOW or RTLD LAZY.
Once dl open returns the library will either have been loaded successfully
or will have failed to load. Validation as to what happened can be done
by analyzing the return value.
14
4.1.2 Windows
15
the caller a mechanism by which they can read an exact number of bytes
from the socket.
4. Read the library from the socket and write it to disk
At this point, everything is accounted for and the library can be read in
from the socket and written to the file. Due to the fact that Windows does
not support the MSG WAITALL flag, an alternative approach can be used
based on the code displayed below. This code snippet will read the library
in as chunks and write those chunks to the file until the total number of
bytes left to read is zero:
char buffer[1024];
int bytes = 0, bytesLeft = libraryLength,
written;
4.2 In-Memory
Of the two methods used to perform library injection, the In-Memory method
is by far the most advanced and dangerous. The benefits of In-Memory library
16
injection include the ability to avoid detection from On-Access virus scanners
due to the fact that the library itself never actually touches the disk. The
process used to achieve this varies from platform to platform, but the general
approach is to hook the underlying file operations that the dynamic loader uses
to load a library. Hooking is the process by which a call to a given function
is routed through an intermediate step, such as a custom function that can
emulate or perform a different operation than was originally intended for the
original function[3]. This concept plays a critical role in achieving the goal of
In-Memory library injection.
The following sections will detail how In-Memory library injection can be im-
plemented across Linux and Windows.
4.2.1 Linux
1. Read the length of the library from the socket
Read the four byte length of the library and store it for later use. This
step can be optimized by passing the MSG WAITALL flag to the socket recv
system call and passing 4 bytes for the len argument. This step makes it
possible to know exactly how many bytes should be read from the socket.
2. Anonymously map memory that is at least the length of the library
In order to be able to store a dynamically sized library, one has two op-
tions. Either the stack can be used, which is quite limited as to the amount
of space it has available for storage, or an anonymously mapped memory
range can be used. The heap is also an option, but it involves a more
tedious process than would otherwise be necessary. The mmap system call
exposes an interface that allows for associating a memory range with a file
descriptor. It also allows for mapping an arbitrary memory range that is
not tied to a file descriptor; this is referred to as an Anonymous Map. It
is the latter of the two capabilities that are of use for In-Memory library
injection. The mmap function is prototyped as follows:
17
Argument Name Argument Value
start NULL
length The length of the library read in from the
socket
prot PROT READ | PROT WRITE | PROT EXEC
flags MAP PRIVATE | MAP ANON
fd -1
offset 0
The return value will be the VMA of the mapped memory, or -1 on failure.
This address should be saved for later use.
3. Read the entire library into the mapped memory
With the memory allocated to store the library in, the next step entails
reading the actual contents into it. This is done by calling recv with the
buf argument set to the VMA that was returned from mmap. The len
argument should be set to the length of the library. Finally, the flags
argument should be set to MSG WAITALL in order to read the whole library
in one swoop. On success, recv will return the number of bytes read which
should be equal to the length of the library.
4. Hook file operation functions
This step is the most complex and requires some understanding of how the
dynamic loader operates. During normal operation, the dynamic loader
([Link]) makes use of a subset of the file operation functions in
order to open, read, and map the library into the process’ address space.
In order to load a dynamic library that does not exist on disk, a program,
or payload in this case, must layer itself in-between the dynamic loader and
said file operation functions. This is done through hooking, as mentioned
earlier in the chapter. The actual file operation functions that dynamic
loader uses, at least at the time of this writing, are as follows2 :
2 This list only includes the file operations that are required to be hooked in order to
18
File Operation Usage
open Used to open a library for subsequent file
operations.
read Used during the initial loading phase to
validate the library as being an ELF im-
age and, potentially, to read in the pro-
gram header table.
lseek In scenarios where the program header ta-
ble exceeds the number of bytes initially
read in during the validation phase with
read, the dynamic loader will use this op-
eration to seek to the start of the program
header table
mmap Used to associate a memory range with
the contents of the library.
fxstat64 Used to get information about the file,
such as its size and mode.
open
The “open” hook involves checking to see if the pathname that was passed
into the function matches the “fake” library name that the hook expects
to see. If it does match, a virtual file descriptor should be returned that
does not conflict with any existing file descriptors and can be used by
subsequent file operations to identify it as being special. The virtual file
descriptor should store information such as the current virtual file offset,
the size of the library in memory, and the base address at which the library
was loaded. This information is then used by subsequent file operation
functions when reading, seeking, and for the other file operations as well.
If the pathname passed in does match the fake library name, the call
should simply be passed to the real open function.
read
The “read” file hook should check to see if the file descriptor passed in
as the fd argument is a virtual file descriptor or a real file descriptor. If
it’s a virtual file descriptor a logical read operation should be emulated
against the memory range. This means that up to count number of bytes
should be copied from the mapped memory range to the buffer passed in
as buf. If the current file offset is equal to the length of the library, zero
bytes should be copied. After a successful read operation the current file
19
offset should be updated by adding the number of bytes actually copied
to the original offset. If the file descriptor passed into read is not a virtual
file descriptor, the call should simply be passed to the real read function.
lseek
The “lseek” file hook, if needed, should emulate file seeking operations
against the mapped memory range, but only if the file descriptor passed
in as the fd argument is a virtual file descriptor. There are three types of
seeking operations: SEEK SET, SEEK CUR, and SEEK END. The first of the
three, SEEK SET, is a way by which a caller can set the file descriptor’s
offset. In the case of a virtual file descriptor, this would involve setting
the current file offset to the argument passed in by offset. The second of
the three, SEEK CUR, is a way by which a caller can update the file offset
relative to its current position. In the case of a virtual file descriptor, this
would be emulated by adding the offset passed in as offset to the current
file offset. In theory, sanity checks are not necessary in this context as it
is unlikely that the dynamic loader will pass invalid offsets. Finally, the
third seek operation, SEEK END, is used when the caller wants to update
the file descriptor’s offset relative to the end of the file. In the case of a
virtual file descriptor, this is emulated by adding the offset passed in as
offset to the size of the library itself and storing sum as the current file
offset. If the file descriptor passed into lseek is not a virtual file descriptor,
the call should simply be passed to the real lseek function.
mmap
The “mmap” hook is arguably the easiest of the set. When a virtual file
descriptor is passed in, the mmap hook should simply call the real mmap
function and map an anonymous memory range based on the arguments
passed in. Once the range has been mapped successfully, the contents
of the library at the offset specified as the offset argument for length
bytes should be copied into the newly mapped memory range. If the file
descriptor passed in is not a virtual file descriptor, the call should simply
be passed to the real mmap function.
fxstat64
The “fxstat64” hook is responsible for giving the caller information about
the file descriptor passed in, such as its size, atime, ctime, among other
things. In the case of emulating this sort of operation on a virtual file
descriptor, all that is really necessary is to attempt to provide the caller
with as much accurate information as possible. For instance, the st size
attribute of the struct stat64 argument passed into the function should
be set to the size of the library. The st uid and st gid attributes should
be set to the uid and gid of the current process, respectively. The st mode
needs to be at least initialized to zero in order to avoid having it be
indicated as something other than a normal file. If the file descriptor
passed in is not a virtual file descriptor, the call should simply be passed
to the real fxstat64 function.
20
5. Find the VMA for dl open
The process to do this is discussed in the section on Loading a Library
for Linux (3.1).
6. Call dl open with a “fake” library name
Once dl open has been successfully located, the next step is to call it with
the path argument set to a unique library name that the hook functions
will know to expect as symbolizing the library that exists in memory. This
will then indirectly call the hook functions described in the previous step
and eventually lead to the loading of the library, even though it does not
reside on disk.
4.2.2 Windows
concept and may not be required depending on the approach taken. These functions include
VirtualQuery, VirtualProtect, FlushInstructionCache, and RtlUnicodeStringToAnsiString.
21
Library Required Function
[Link] LoadLibraryA
VirtualAlloc
VirtualQuery
VirtualProtect
FlushInstructionCache
WriteProcessMemory
[Link] NtOpenSection
NtCreateSection
NtMapViewOfSection
NtQueryAttributesFile
NtOpenFile
RtlUnicodeStringToAnsiString
WS2 [Link] recv
LPVOID VirtualAlloc(
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flAllocationType,
DWORD flProtect
);
To allocate memory for storing the library, the lpAddress argument should
be set to NULL, the dwSize argument should be set to the size of the library,
the flAllocationType argument should be set to MEM COMMIT, and finally
22
the flProtect argument should be set to PAGE READWRITE. On success,
VirtualAlloc should return a pointer to the allocated buffer. Otherwise,
NULL is returned. The pointer that is returned should be saved in some
context for subsequent steps.
One item worth noting is that, by default, the pages in the loaded library
may swap out to disk. If an Anti-Virus scanner were to support swap
scanning it might be possible for it to detect the library. In order to avoid
this, one can make use of the VirtualLock function to pin the allocated
address range for the library in memory.
4. Read the library from the socket and write it to memory
Once the buffer has been allocated to store the library, the next step
is to actually download it from the socket that was passed in from the
first stage loader. One method to doing this is outlined in the following
example code:
BOOL WriteProcessMemory(
23
HANDLE hProcess,
LPVOID lpBaseAddress,
LPCVOID lpBuffer,
SIZE_T nSize,
SIZE_T* lpNumberOfBytesWritten
);
WriteProcessMemory(
(HANDLE)-1,
targetLibraryBuffer,
downloadedLibraryBuffer,
libraryNtHeader->[Link],
NULL
);
Once the PE headers are populated, the next step is to populate each
individual section of the image by enumeration. In order to enumerate
the sections in the image the [Link] attribute
is used from the IMAGE NT HEADER portion of the PE. For each individual
section, WriteProcessMemory should be called as follows4 :
WriteProcessMemory(
(HANDLE)-1,
targetLibraryBuffer + sections[index].VirtualAddress,
downloadedLibraryBuffer + sections[index].PointerToRawData,
sections[index].SizeOfRawData,
NULL
);
24
range instead of a file on disk are listed below along with what their
originally intended purpose is:
NtOpenFile
The “NtOpenFile” hook handles requests to the NtOpenFile function
which is prototyped as[8]:
The hook implementation must inspect the name of the file that is being
passed in to see if it is the “fake” library name or not. This is done by
checking the ObjectName attribute of the ObjectAttributes parameter.
If the library name does match the “fake” library’s name, a unique, iden-
tifiable handle should be returned in the FileHandle parameter. This file
handle should then used by subsequent file operations. If the filename
does not match, the original NtOpenFile shouldcalled.
25
NtQueryAttributesFile
The “NtQueryAttributesFile” hook handles requests to the NtQueryAttributesFile
function which is prototyped as[8]:
The hook implementation must do the same check that the NtOpenFile
hook does by inspecting the ObjectAttributes’ ObjectName attribute to
see if it matches the “fake” name of the library that is being injected. If the
name does match, the hook function should populate the FileAttributes
argument with sane values, such as setting the FileAttributes attribute
to FILE ATTRIBUTE NORMAL5 . After the structure has been initialized, the
hook function should return STATUS SUCCESS. If the filename does not
match, the original NtQueryAttributesFile should be called.
Both of these functions emulate the same behavior as far as the hook
routines are concerned. In the case of NtCreateSection, the hook func-
tion should check to see if the FileHandle argument matches a handle
that may have been previously returned from the NtOpenFile hook. If
it does not, the original NtCreateSection function is called. In the case of
5 Other attributes, such as CreationTime, LastAccessTime, LastWriteTime, and Change-
26
NtOpenSection, the hook should simply check to see if the ObjectAttributes’
ObjectName attribute matches the “fake” library name. If it does not, the
orignial NtOpenSection is called.
In the case where the check passes for the two hook functions, the SectionHandle
argument should be set to the targetLibraryBuffer that was initialized
in the previous steps.
NtMapViewOfSection
The “NtMapViewOfSection” hook handles requests to the NtMapViewOfSection
function and is prototyped as[8]:
27
The end result: the library is loaded, relocated, and initialized.
28
Chapter 5
Potential Impacts
With the how of library injection covered, it would seem prudent to consider
the potential impacts of this technology being incorporated into exploits and
malware. As identified in the introduction, library injection lowers the bar for
exploit writers such that it is no longer a requirement that one know assembly
in any form; rather, all that must be known is how to program in any language
that supports being linked as a dynamically loadable library1 . The following
sections will discuss a number of potential impacts and attempt to analyze the
severity of each
One of the scarier impacts of remote library injection involves the possibility for
writing highly automated worms. These worms would use an arbitrary exploit
to inject one or more libraries. Once the library or libraries load on the target
machine, it would be possible to do a number of things. For instance, the library
or libraries could propagate themselves to every other process on the machine
by replicating into the address space of other processes. This means that not
only would one process be infected, but so too would every other process on the
machine2 . This makes killing the worm a much harder task in that there is not
just one or two processes that can be killed.
Aside from the local propagation to other processes, worm infection techniques
can be made more advanced and intelligent due to the fact that the library,
depending on the method of injection used, will be loaded under the radar of
current Anti-Virus solutions. This allows the worm to maintain a greater level
1 Which, as scary as it seems, includes Visual Basic.
2 This is dependent on the access rights of the infected process.
29
of retention when it comes to being removed by aggressive or passive virus scan-
ning. Since the worm is less likely to be detected, at least the host level, it is
then inherently possible to write arbitrarily complex host infection methodolo-
gies. Simply put, a worm author is afforded more luxury when it comes to
writing non-deterministic infection patterns that make heuristic identification
by virus scanners just that much more complicated.
The luxury of being able to develop an injectable library brings with it the po-
tential for writing advanced, cross platform worms that are capable of infecting
a wide array of operating systems. Granted, the binary format and runtime
libraries between each operating system are typically different, but it is still
possible to write fairly portable code that can then be compiled down into the
binary format of the target machine. Combine this ability with the fact that
library injection, at least In-Memory library injection, is not currently detected
by Anti-Virus scanners and one gets a worm that targets not just one operat-
ing, but instead a number of operating systems. At the time of this writing,
the trend for worm infection seems to be uni-platform in nature from what the
authors have witnessed.
As with all new methods of infection, Anti-Virus vendors will have to react and
come up with a solution to the problem posed by library injection. Granted,
On-Disk library injection already has a means by which it can be detected, but
In-Memory on the other hand is a whole different problem. Potential methods
of detection are discussed in the chapter on Prevention and Detection (6).
30
Chapter 6
This chapter will discuss potential ways in which remote library injection might
be prevented or detected, both passively and aggressively. The methods of
detection, much like the methods of injection themselves, vary greatly from
platform to platform and as such will be discussed separately from one another.
As far as prevention is concerned, the most logical and repeatedly emphasized
solution by the security industry is to ensure that machines remain patched
and up-to-date when it comes to security related issues. Indeed, this does not
help prevent against the unreleased vulnerabilities, but it is a method of pre-
vention nonetheless. The second method of prevention comes in the form of
Host Intrusion Prevention Systems, or HIPS. These packages implement
host level intrusion detection and prevention features such as system call log-
ging and analysis, page execution enforcement such as no-exec stacks, and other
security improvement features such as ASLR. Both Linux and Windows have
HIPS or HIPS-like software components and can be used to help with the pre-
vention of exploits, thusly preventing the injection of libraries.
31
6.1 Linux
An external application can inspect the loaded library list in one of two ways.
The least accurate way involves using the proc filesystem and looking at the
maps file which contains memory mapping information within a given process.
In the case of On-Disk library injection, this method is adequate in detecting
whether or not a potentially malicious library has been loaded. On the other
hand, In-Memory library injection is not quite as simple. The memory mapping
for the library will not show up as being associated with a given file. As such,
one cannot directly correlate a memory range with a malware library.
The second, more accurate option involves walking the linked list of loaded
libraries in the context of the process. This can be done by using the ptrace
function to attach to the process and read memory from within it. In order to
enumerate the linked list of loaded libraries, one must first locate the first entry
in the list. Fortunately, some versions of [Link] have a named symbol
in the bss called dl rtld map. By adding the st value attribute to the base
address of [Link], the VMA for dl rtld map can be calculated. Once the
address is known, ptrace can be used to read the contents of the global variable
which happens to be a struct link map * variable. The link map structure
has the following exposed definition (as found in /usr/include/link.h):
struct link_map {
ElfW(Addr) l_addr;
char *l_name;
ElfW(Dyn) *l_ld;
struct link_map *l_next, *l_prev;
};
The l name attribute is a pointer to the name of the library that was loaded.
The l addr attribute is the base address at which the library was mapped in.
Finally, the l next and l prev attributes are pointers to the next an previous
list entries, respectively. By walking the linked list, one can validate whether or
not an individual library is valid based on a number of things, such as whether
or not it actually exists on disk.
Though this all seems good in theory, there are inherent problems with the fact
that one can easily make this detection method implausible. For instance, if
an injected library were to remove itself from the dl rtld map linked list, one
would not be able to detect that it was actually loaded.
With that said, the authors are not aware of a method that can deterministically
and reliably detect, either passively or aggressively, that a library has been
32
injected into a process.
33
6.2 Windows
The above steps are one way in which loaded libraries in the context of a given
process can be enumerated. Another way involves injecting a custom library that
1 This process is only supported on NT-based versions of Windows.
34
manually enumerates the loaded module list directly. Like Linux, which exposes
the list of load libraries by way of the dl rtld map symbol, Windows too has
a deterministic location from which libraries can be enumerated. The list of
loaded modules is located in the Process Environment Block, or PEB. This is
an undocumented structure that holds information about the state of the process
and can be directly referenced via fs:[0x30] on IA-32. Loader information is
stored in the LoaderData attribute which is of type PEB LDR DATA and is found
in the PEB. The PEB LDR DATA structure has the following definition as taken
from NTInternals The Undocumented Functions[8]:
The LIST ENTRY structure contains a logical previous and next pointer as used
in a doubly linked list. Each entry in the three module linked lists point to a
LDR MODULE structure which contains the following information:
Enumerating the module lists directly allows one a better glance at the actual
state of the loaded libraries in that more information can be gathered vice being
limited to just the name of the library. This approach is not foolproof, however.
It is possible for the injected library to remove itself from the three linked lists
and thus disappear from the record. As such, detecting a malicious library via
this method should be seen as inadequate.
35
6.2.2 Detecting Function Hooks
36
Chapter 7
Conclusion
37
Bibliography
38
[11] Russinovich, Mark. Inside On-Access Virus Scanners.
[Link]
42&ArticleID=300; accessed Apr 02, 2004.
[12] skape. elfcmp.
[Link] accessed Apr 04, 2004.
[13] skape. Understanding Windows Shellcode.
[Link] ac-
cessed Apr 02, 2004.
[14] Sysinternals. Process Explorer.
[Link] ac-
cessed Apr 01, 2004.
[15] Tool Interface Standards. Executable and Linkable Format.
[Link] accessed Apr 03, 2004.
39