Recently I have become interested in Linux, and am trying to create a program which is capable of injecting a shared object (i.e. .so file, 'dynamically loadable library', "DLL" under Windows.) I know this can be done by setting an environmental variable, but I want to do it on a process which is already running.
I already know how to do this under Windows. There are several ways, but generally speaking you can just call LoadLibrary() by creating a remote thread using CreateRemoteThread(). Of course you need the address of LoadLibrary in the remote process, but (in my experience) it is always at the same offset for every process.
I have done some research as to how this can be done under Linux. For example an interesting article in Phrack 59 shows how this can be done. The article also has a source code attached, but since some assumptions are made of the target process and it is 32 bit, I couldn't get it to work. Other things I bumped into: a codeproject article, but this one only explains how to do it from within gdb. (I would post more links, but the website limits me to 2 :-/.)
To start out, I want to obtain the address of the dlopen() function in the remote process. To do so, I figured out I would have to obtain the process's ELF-header and iterate through the symbol tables. Actually, I managed to do this, by:
1) Obtaining the ELF-header (under 64bits stored at 0x400000 in my experience.)
2) Locating the Global Offset Table in the Program Header marked as DYNAMIC.
3) Retrieving the first link_map by access the second entry in the Global Offset Table.
4) Iterating through the dynamic sections of the link_map chain, thus obtaining the address of the String Table, Symbol Table and Hash Table (*Hash_Table + 0x4 holds the amount of entries in the symbol table.)
5) Looping through the Symbol Table
Some example output from my program:
** looking at lib "" **
Trying to find symbol main in symbol table... numentries: 49
index 1 name: val: 0
...
index 49 name: memcpy val: 0
symbol not found.
** looking at lib "" **
Trying to find symbol main in symbol table... numentries: 11
index 1 name: val: 0
...
index 11 name: __vdso_time val: 0xffffffffff700a80
symbol not found.
** looking at lib "/lib/x86_64-linux-gnu/libc.so.6" **
Trying to find symbol main in symbol table... numentries: 2190
index 1 name: val: 0
...
index 2190 name: wcpcpy val: 0xa3570
symbol not found.
However, I am not able to find a valid address of dlopen! (Or even the address of main, for that matter!) For testing purposes I let the program analyze itself, so I know for sure main exists. I also tried readelf -s to have a look at the symbol tables, and it shows:
Symbol table '.symtab' contains 151 entries:
Num: Value Size Type Bind Vis Ndx Name
...
149: 0000000000401880 216 FUNC GLOBAL DEFAULT 13 main
So, somehow readelf has managed to find main, while I can't. I also had a look at the libelf library, but that relies on reading from the application file, instead of accessing the process's memory (i.e. it can't be used while a process is running.) Does anybody have a clue how I can locate dlopen() in a remote process, or even main, for that matter?
I am running Ubuntu 12.04 64bit.
First, regarding the address of main:
It seems one would have to use the Section Headers to find out the address of main. Doing it just using the dynamic section seems not possible. Running readelf -D -s
and readelf -D --dyn-sym
does not give the address of main either.
Now, regarding finding the address of dlopen
. It turns out I was reading the wrong number of symbolic table entries from the hash tables. There are two types of hash tables (I have encountered so far): DT_HASH
tables, and DT_GNU_HASH
tables. The former have the amount of entries at hash_table_addr + 4
(source), the latter do not specify the amount of hash tables explicitely. One needs to obtain this amount by iterating through the hash table's bucket table. Other than that, my approach was good, and now I am able to find the address of dlopen
, malloc
, etc.
To obtain the number of entries (i.e. size of) a Symbol Table from a Hash Table, one can use (C):
ssize_t ReadData(int pid, void* buffer, const void* source, ssize_t size)
{
// Under Ubuntu and other distros with a 'hardened kernel', processes using this function
// should be run as root.
// See https://wiki.ubuntu.com/SecurityTeam/Roadmap/KernelHardening#ptrace_Protection
iovec local_vec;
local_vec.iov_base = Buffer;
local_vec.iov_len = Size;
iovec remote_vec;
remote_vec.iov_base = Address;
remote_vec.iov_len = Size;
return process_vm_readv(pid, &local_vec, 1, &remote_vec, 1, 0);
}
unsigned long FindNumEntriesHashTable(int pid, void* TablePtr, const void* TableLibAddr)
{
// Check if TablePtr is smaller than 0.
unsigned long pointer = ((long)TablePtr < 0) ? (unsigned long)TablePtr + (unsigned long)TableLibAddr : (unsigned long)TablePtr;
unsigned long ret = 0;
ReadData(pid, &ret, (void*)(pointer + sizeof(Elf_Word)), sizeof(Elf_Word));
return ret;
}
unsigned long FindNumEntriesGnuHashTable(int pid, void *TablePtr, const remote_voidptr TableLibAddr)
{
unsigned long pointer = ((long)TablePtr < 0) ? (unsigned long)TablePtr + (unsigned long)TableLibAddr : (unsigned long)TablePtr;
// Read in required info on the gnu_hash table
unsigned long nbuckets = 0;
unsigned long symndx = 0;
unsigned long maskwords = 0;
ReadData(pid, &nbuckets, (const remote_voidptr)pointer, sizeof(Elf_Word));
ReadData(pid, &symndx, (const remote_voidptr)(pointer + sizeof(Elf_Word)), sizeof(Elf_Word));
ReadData(pid, &maskwords, (const remote_voidptr)(pointer + 2 * sizeof(Elf_Word)), sizeof(Elf_Word));
// Calculate the offset to the bucket table. The size of the maskwords entries is 4 under 32 bit, 8 under 64 bit.
unsigned long masktab_size = (ENV_NUMBITS == 32) ? 4 * maskwords : 8 * maskwords;
unsigned long buckettab_offs = 4 * sizeof(Elf_Word) + masktab_size;
// Read in the bucket table
Elf_Word buckettab[nbuckets];
ReadData(pid, &buckettab, (const remote_voidptr)(pointer + buckettab_offs), nbuckets * sizeof(Elf_Word));
// Loop through the bucket table. If the given index is larger than the already known index, update.
unsigned long num_entries = 0;
for (size_t i = 0; i < nbuckets; i++)
{
if (num_entries == 0 || buckettab[i] > num_entries)
{
num_entries = buckettab[i];
}
}
if (num_entries == 0)
{
return 0;
}
// Add one, since the first entry is always NULL.
return num_entries++;
}
There are so many bad assumptions and mis-understandings in your question, I don't know where to begin.
using CreateRemoteThread()
There is no Linux equivalent for this Windows feature (arguably mis-feature).
Obtaining the ELF-header (under 64bits stored at 0x400000 in my experience.)
This is the default location for non-PIE executable. It is by no means guaranteed.
I want to obtain the address of the dlopen() function in the remote process
You are assuming that there is dlopen in remote process, but that would only be true if the remote process binary was linked against libdl
. That's a small subset of all Linux binaries.
somehow readelf has managed to find main, while I can't.
You don't understand the difference between dynamic and static ELF symbol tables. Running nm a.out
and nm -D a.out
and comparing results will be illustrative. You'll find main
in one, but not the other.
Note: static symbol table is not required at runtime, and could be stripped.
You should probably not use a hardcoded base address for the main executable (because it need not be 0x400000) and iterate over the link_map structure found in the second GOT entry (because anything the dynamic linker wants to find herre might be there instead).
A better solution would be:
- use /proc/$pid/maps to find the mapped ELF files;
- find the dynamic section table from this;
- find dlopen from there;
- if you do not have dlopen you might want to find _rtld_global_ro in ld.so instead, this global variables should have a
_dl_open
function pointer that you may want to call.
This is what libdl does (using _dl_open from by ld.so):
dlopen_doit (void *a)
{
struct dlopen_args *args = (struct dlopen_args *) a;
if (args->mode & ~(RTLD_BINDING_MASK | RTLD_NOLOAD | RTLD_DEEPBIND
| RTLD_GLOBAL | RTLD_LOCAL | RTLD_NODELETE
| __RTLD_SPROF))
GLRO(dl_signal_error) (0, NULL, NULL, _("invalid mode parameter"));
args->new = GLRO(dl_open) (args->file ?: "", args->mode | __RTLD_DLOPEN,
args->caller,
args->file == NULL ? LM_ID_BASE : NS,
__dlfcn_argc, __dlfcn_argv, __environ);
}
with:
# define GLRO(name) _rtld_local_ro._##name
and:
struct rtld_global_ro {
[...]
void *(*_dl_open) (const char *file, int mode, const void *caller_dlopen,
Lmid_t nsid, int argc, char *argv[], char *env[]);
[...]
};