Shared library injection into a Linux process (part 1)
Introduction
Since my childhood, I wanted to know how do game cheats work. And because it has been a long time since I haven’t touch any process from the inside, the time has come! In this article series, I will show some of my works about cheats development. I just began this long journey and I don’t even know yet if I will finish it (yeah, procrastination).
As explain into the title, in this article I describe the first part of a method allowing to inject code into a running process. Before writing these few lines, I developed a simple tool that is able to automatize the process. Later, in my future works, I’ll try to use it in order to inject some cheats into video games. Currently, no protection against anti-cheats is implemented and I think it will be detected… If needed, this one will be updated.
I warn you that I didn’t invent anything and the method presented here is well known.
The simplest solution
I choose to develop my own tool in order to learn and to have more flexibility. You can achieve the same thing just by using gdb:
#!/bin/sh
# sh injector.sh $(pidof XXX) /path/to/your/lib.so
gdb -n -p ${1} -batch \
-ex "set \$__libc_dlopen_mode=(void*(*)(const char*, int))__libc_dlopen_mode" \
-ex "call \$__libc_dlopen_mode(\"${2}\", 2)" \
-ex "quit"
The roadmap
The method I used to inject code into a process is based on several facilities:
: this function allows to dynamically load a shared library. This one works like__libc_dlopen_mode
dlopen
(man page) but is directly provided by the libc rather than the libdl.ptrace
(man page): this syscall allows to control a process by attaching and modifying it. With it, you can easily stop a process, run it instruction by instruction, modify its memory (code and data) and so on./proc/[pid]/maps
(man page): this file contains the memory footprint of a process. It allows to grab some useful information like mappings addresses.
The roadmap of the injector tool is simple:
- Compute the address of
into the victim process.__libc_dlopen_mode
- Attach the victim process with
ptrace
. - Modify the victim process to make it execute the
function.__libc_dlopen_mode
In this article, I only explain the first topic: how to retrieve the address of a function into another process? Others subjects will be described later.
Theory: how to compute an address
The injector needs to make the victim process call the function __libc_dlopen_mode
. To do that, it needs to get the address of the function into the victim process memory. Obviously, it’s not as easy as it looks because of the ASLR (Address Space Layout Randomization).
However, the ASLR is not very useful in this case because we have a full access to the computer (especially maps files). Just a little remember about the ASLR. Each time a process starts, its libraries are loaded into a random position inside its memory. For example, here is the memory mapping of two processes:
As shown on this picture, the libc is not loaded at the same address in these two processes, so: addr(injector.libc) != addr(victim.libc)
. However, despite the randomization, we can see that the offsets between the libc address and the addresses of its own functions remain unchanged offset(libc, libc.dlopen)
(in red).
Thus, with these knowledge, it’s possible to compute the address of any function provided by a lib into a victim process from another one. Here’s how to compute the offset(libc, libc.dlopen)
:
offset(libc, libc.dlopen) = addr(injector.libc.dlopen) - addr(injector.libc)
We can now compute addr(victim.libc.dlopen)
:
addr(victim.libc.dlopen) = addr(victim.libc) + offset
In the next sections, I describe how to get all the information.
How to get addr(injector.libc)
and addr(victim.libc)
?
By using the /proc/[pid]/maps
file! Just parse the file and get the executable section of the libc. Here’s an excerpt of a process maps:
$ cat /proc/2842/maps | grep libc
address perms offset dev inode pathname
7f0865f4a000-7f0865f6f000 r--p 00000000 fd:00 12981143 /usr/lib64/libc-2.30.so
7f0865f6f000-7f08660be000 r-xp 00025000 fd:00 12981143 /usr/lib64/libc-2.30.so
7f08660be000-7f0866108000 r--p 00174000 fd:00 12981143 /usr/lib64/libc-2.30.so
7f0866108000-7f0866109000 ---p 001be000 fd:00 12981143 /usr/lib64/libc-2.30.so
7f0866109000-7f086610c000 r--p 001be000 fd:00 12981143 /usr/lib64/libc-2.30.so
7f086610c000-7f086610f000 rw-p 001c1000 fd:00 12981143 /usr/lib64/libc-2.30.so
7f0866197000-7f0866198000 r--p 00000000 fd:00 1978213 /usr/share/locale/en_GB/LC_MESSAGES/libc.mo
To simplify the task, I developed a basic Python function (a part of my proc library) that parses the maps file and returns each mappings. Here is an example of its use in which I request executable mappings of the libc into the Python process and the victim one:
import os
from maps import *
injector_pid = os.getpid()
victim_pid = 2842
filter_ = and_(has_perms('x'), has_path('/usr/lib64/libc-2.30.so'))
injector_libc = get_maps(injector_pid, filter_)[0]
victim_libc = get_maps(victim_pid, filter_)[0]
print(f'addr(injector.libc) = {hex(injector_libc.start_address)}')
print(f'addr(victim.libc) = {hex(victim_libc.start_address)}')
Here is the output:
addr(injector.libc) = 0x7f37bd72b000
addr(victim.libc) = 0x7f0865f6f000
Now, we are missing only one value. That’s the purpose of the next section!
How to get addr(injector.libc.dlopen)
?
In C/C++/ASM, this is a very easy task (to reduce the amount of code, I don’t check errors, etc.):
#include <stdio.h>
#include <dlfcn.h>
int main(int argc, char** argv)
{
void* libc_handle = NULL;
void* __libc_dlopen_mode_addr = NULL;
libc_handle = dlopen("/usr/lib64/libc-2.30.so", RTLD_NOW);
__libc_dlopen_mode_addr = dlsym(libc_handle, "__libc_dlopen_mode");
printf("__libc_dlopen_mode addr: %p\n", __libc_dlopen_mode_addr);
dlclose(libc_handle);
return 0;
}
But how to do it in Python? I use the same method thanks to ctypes
(in the default Python library). Here is an example:
libc = ctypes.CDLL('/usr/lib64/libc-2.30.so')
sym = getattr(libc, '__libc_dlopen_mode')
ptr = ctypes.cast(ctypes.addressof(sym), ctypes.POINTER(ctypes.c_ulonglong))
injector_dlopen = ptr.contents.value
print(f'addr(injector.libc.dlopen) = {hex(injector_dlopen)}')
As in C, this method makes a dlopen
(the true one!) then a dlsym
to get the address of the function. It’s not hard to code but is heavy: the library is loaded into the injector process only to compute an offset… For the libc, it doesn’t matter because this one is already loaded, but for others…
Here is the output:
addr(injector.libc.dlopen) = 0x7f0866085a20
Compute addr(victim.libc.dlopen)
We just have to apply the formula found in the previous section (addr(victim.libc.dlopen) = addr(victim.libc) + offset
):
victim_dlopen = victim_libc.start_address + offset
print(f'addr(victim.libc.dlopen) = {hex(victim_dlopen)}')
And here is the output:
addr(victim.libc.dlopen) = 0x7f0866085a20
Voilà! We have the address of __libc_dlopen_mode
!
Python library
I talk earlier about my Python library. I develop this one to meet my needs. For example, it’s easy to retrieve the address of a function:
victim = Process(victim_pid)
victim_dlopen = victim.get_sym_addr('/usr/lib64/libc-2.30.so', '__libc_dlopen_mode')
print(f'addr(injector.libc.dlopen) = {hex(injector_dlopen)}')
As we will see, this one provides some other interesting features. For instance calling a function or a syscall into the victim process:
# function call
victim.call(exit, 0)
# syscall
victim.syscall(__NR_write, 1, 0x..., 10)
This one is functional and my injector uses it. You can find it here:
https://github.com/d33d33l4bs/proc
Le mot de la fin
Now that we have the address of __libc_dlopen_mode
, we will see in the next articles how to make the victim process call it.
If you have any questions or suggestions, don’t hesitate to post a new comment.
P.S: beware of the bearded man called Kami.