Before continuing learning about process_vm_readv and process_vm_writev we'll stop for a second and analyze how everything we've learned so far related to the domain of binary exploitation. Then, we'll continue exploring process_vm_writev/readv to hack our own game and once hacked we'll write a trainer also using these system calls.
A Radiography of Code Injection
At this point, it will be beneficial to make a summary of all the code injection techniques we’ve discussed. At its basis, code injection requires, roughly speaking, three steps: get the code you want executed into the target process memory, then jump into that code, and finally, optionally, return control back to the original program. Throughout this chapter, we have seen how to achieve this in a controlled environment, introducing several techniques that are used in other disciplines like exploit development.
As a summary, let’s make a list of the different options we’ve learned so far to force a program to execute the code we want. If you think about this, most of what you’ve learned so far is the foundation for exploit development. In a real exploit, you have to do exactly what we’ve been doing but using a vulnerability. Leaving the vulnerability aside, which is, let’s say, the arbitrary part, you will end up applying one of the techniques we’ve seen so far.
Note that in this section, we’ll be talking about code execution vulnerabilities. There are other types of vulnerabilities that require manipulation of credentials, files, links, and so on. Those are different kinds of vulnerabilities, and we’re not going to talk about them here.
Step 1. Get Your Code into the Target Process
Step 1 is getting the code into the target process. We’ve learned how to modify any program using:
ptraceand thePTRACE_POKEandPTRACE_PEEKcommandsprocess_vm_writev/readv/dev/PID/mem
This step sometimes mixes up with step 2 in the sense that whenever you need to write something into memory to change flow control, that process will usually be part of the same process of getting the code into memory.
Overall, all those techniques require the attacker to get quite some privileges in the system. That’s why privilege escalation is a thing. Once you get root, you can apply all those techniques, so, despite what you may be thinking, they are useful and used in real life.
In the more general case, a vulnerability will be exploited to get your code injected into the target process. Each vulnerability is different, but we could talk about vulnerability families.
The most common ones to get your code injected are:
- Buffer Overflow Vulnerabilities. These are the most classical ones. The program doesn’t properly check the size of the data and ends up writing beyond the buffer and eventually reaching a return address (if the buffer is in the stack) or some other pointer that will be used later by the program to change its flow. Buffer overflows can be easily avoided using modern mitigation techniques like stack canaries, PIE binaries, ASLR, non-executable stacks (DEP/NX, Data Execution Prevention/Non-Executable memory), etc.
- Heap Vulnerabilities. Those are harder to implement, and there are also some subfamilies like heap overflow, use after free, double free, etc. Most of these techniques allow you to get a write primitive that can be used to implement some of the techniques in Step 2 (flow control). In case you’re curious, overwriting the heap may overwrite vtables for heap-allocated objects in languages like C++, or fool the memory allocator to write in the wrong position when it modifies the linked lists maintained internally, for example, for the free memory blocks. PIE, DEP/NX, and ASLR already make heap exploitation harder. CFI (Control Flow Integrity) also makes some of this kind of exploits much, much harder.
- Format String Vulnerabilities. This kind of vulnerabilities may also provide a write primitive like the heap-based one or a read primitive that may be used to leak information, like code pointers, which can be used to circumvent ASLR and let the attacker guess the process memory map, get stack canaries, etc.
These are some of the most classical ways to allow an attacker without enough privileges to write into a process memory. As you can see, there are really only two options. Either a second process writes into the memory using operating system services, or the target process is fooled to write into the memory itself. Despite the option selected, the final goal is to write into the target process memory. This takes us to Step 2.
Step 2. Change Control Flow
Previous steps let us write into the process memory. Now we need to know where to write to make the target process change its flow towards the code we injected or some existing code that may be useful. Again, we’ve seen a few options so far:
- Option 1. Inject code into the code segment and
manipulate registers using
ptrace. This is the obvious solution, and we’ve already seen it at work in this chapter. In this case, we use the memory write capability ofptraceto write our code and then change registers directly to get it executed. However, if we useprocess_vm_writevor/proc/PID/mem, we’ll need to use other techniques to get it executed because we cannot modify values of registers using those system calls. - Option 2. Overwrite existing code in the process
that will eventually be executed by the application. We’ve already seen
how this works in a previous section; basically, once again, we need to
write into the process memory, either using a vulnerability,
ptrace,process_vm_writev, or directly accessing/dev/PID/mem. This is how binary instrumentation tools like Frida work. - Option 3. Use already existing flow modification
paths in the file. Here we have quite some options:
- Option 3.1. Inject a return address into the stack
and wait until a function returns. This is used by classical buffer
overflows. In general, we use the
retinstruction in the existing function to jump somewhere else by writing a fake return address into the stack. This is normally associated with a stack overflow vulnerability. In the classical ones, the code was also injected into the stack, and the return address was overwritten to jump back into the stack and run the injected code. Nowadays, stacks are marked as non-executable, and this doesn’t work anymore. - Option 3.2. Return to libC. This is basically the
same thing, but used when the stack is not executable, and there is no
way to inject code into an executable segment. In this case, the address
injected into the stack is an existing function, like
system, for example, which will be called on return from the function whose stack was manipulated. It’s like Option 3.1, but jumping into code that is already in the memory address space of the process. This technique was introduced when the stack started to be non-executable and there was no possible way to write code in a segment with execution permissions. - Option 3.3. Return Oriented Programming family.
Sometimes we can easily jump back to a libC function or into a
syscallinstruction, but we need some values in registers or in memory to make thatripmodification useful. ROP will allow us to build the state we need by pushing a set of addresses into the stack pointing to the so-called gadgets. A gadget is a piece of code that already exists in the program and that does something and then jumps. The original ROP technique usedretas the way to modify therip, and therefore, all gadgets were something followed by aret. We’ll use this technique later in the book. This is also intended to avoid non-executable stacks, reusing existing code but allowing much more control than with the return to libC technique.
- Option 3.1. Inject a return address into the stack
and wait until a function returns. This is used by classical buffer
overflows. In general, we use the
- Option 3.4. Another way to change the program flow
is to make existing function calls in the program jump to different
places. This can happen in a few ways, usually exploiting indirection in
programs (function pointers), for example, overwriting function pointers
on heap-created objects, or on dynamic binaries due to the way dynamic
linking works. Without going into details, dynamic binaries don’t call
library functions directly; they do it through an indirection using a
table named
GOT(Global Offset Table). There’s an entry per function used by the program, so once you modify an entry, any subsequent call to that function by the program will move its flow to a new place. Modern programs useRELO(Relocation Read-Only) protection, which basically fills the GOT and then makes that memory read-only, so it’s not possible to change any entry in the table once the program execution starts. This disables lazy binding and therefore the starting time of the process is longer because the linker needs to resolve all the symbols used in advance.
As you can see, there are quite some options, actually, there are
even more, but at the end, all of them revolve around the same concepts.
Believe it or not, the examples we’ve seen in the previous section cover
everything until Option 3.3, which we’ll cover in a later chapter as I
said. Option 3.4 is pretty straightforward, and you should be able to
implement it yourself with the information provided so far, just looking
for details on how the GOT is used by the dynamic linker
and how to find it in the DYNAMIC segment in the ELF file.
So, for the time being, I’ll leave all this as an exercise to the
reader.
Overall, exploit development revolves around creative ways to achieve these two operations: write memory and change registers.
Step 3. Get control back
This last step is optional and depends on the reasons you wanted to
inject the code. We have seen destructive code injections in which
attackers just want their code executed and don’t care at all about what
happens afterwards. However, in some cases, they may want a
non-destructive injection so the original program continues working
normally after the injection, or they can get the control back again.
This last case is common on instrumentation tools that don’t rely on
ptrace.
So again we have a few options here to keep the control of the hijacked process:
- Option 1. Using
ptracecommands. We can usePTRACE_SINGLESTEPto just run an instruction and get the control back, orPTRACE_SYSCALLto get the control back whenever a system call is executed. These options are interesting when we are injecting simple system calls. - Option 2. Use a breakpoint. For this option, we
also need
ptrace; however, it allows us to inject more complex code, such as a function call. Running the function usingSINGLESTEPuntil we find aretinstruction doesn’t look like a good solution. Instead, we can just inject thecallinstruction followed by anint3. When the function returns from its execution, no matter how big it is, it hits the breakpoint and we get the control back. - Option 3. Patch the binary together with the code to be injected to jump into some other code under the attacker’s control. This is actually a variation of option 2, but instead of using a processor interrupt to jump somewhere, we directly add the code to jump to wherever we want.
This last option usually requires either working on an instrumented copy of the code or storing and restoring parts of the original code.
As I mentioned, there are more and more subtle techniques for each one of the steps, but they all try to achieve the same goal. The overall process is always the same, so it will be very easy for you now to understand existing exploits or new exploitation techniques that will come.
Injecting code without
ptrace
I mentioned before that we can implement many of these techniques
without using ptrace. After all, either
process_vm_readv/writev or writing to
/dev/pid/mem doesn’t really require the use of
ptrace; however, process_vm_readv/writev
requires the use of mprotect to change VMA’s permissions to
write, for example, in the text segment, and that may be a
bit tricky without using ptrace.
Even when, in principle, we can write in a process’s memory at any
time, it is better to first stop the process to avoid side effects in a
similar way to what ptrace does. However, we won’t be able
to force the process into the STOPPED_TRACED state.
Anyways, what PTRACE_ATTACH does is send a
SIGSTOP signal to the process to stop it. And we can do the
same before writing into memory. Then send a SIGCONT signal
to let the process continue.
However, what we cannot do is arbitrarily change registers as we do
with ptrace. Instead, we need to inject code to do whatever
we want, and for the execution of that code using one of the techniques
we already know: injecting instructions in an address the process will
execute later, pushing the address in the stack and waiting for a
ret, updating a GOT entry, etc. The main
characteristic of all those techniques is that they require memory
writing capabilities but not register modifications.
When working this way, we usually want to inject some sophisticated code into the target process, able to perform operations we want arbitrarily, using whatever communication mechanism we want: signals, shared memory, sockets, etc., and force its execution using some of the previous techniques. Then, you can run that code in a separated thread or instrument the code to force its execution periodically (installing a signal handler or patching some instruction in the main loop of the program). This more elaborated code is usually referred to as an agent because we can ask it to do stuff for us.
This agent code will receive commands and execute them for us. As it
is running inside the target process, it can access all its memory,
registers, and execute any code as the target process. It behaves like
our own ptrace in user space.
As you can see, what is important is to understand how processes work. Then, you can find your way around to get code execution in the process. Attackers usually exploit vulnerabilities to get their code into a running project. Reverse engineers use system interfaces to get the code into a running project. Then, both of them use similar techniques to redirect the program flow to the injected code and take control of the process, each one for a different reason.
EXERCISE 1
Modify the previous examples in the chapter to inject code in our game’s
mainfunction without usingptrace.
EXERCISE 2
Write a simple shared library with monitor capabilities. Start with something simple like read/write memory or load another shared library and run some code. Add some kind of user interface to it like a socket or a pipe so you can send commands to it to run the commands it offers. That will be the starting point for an instrumentation system.
Let’s continue the exploration of
process_vm_readv/writev with a couple of use cases
specifically tailored for these powerful system calls.
Data Dumping: Game Hacking
A commonly used technique to crack games is to dump the process memory multiple times and look for differences between dumps. The number of lives or the value of your health points are numbers stored somewhere in memory. The idea is that you dump the process memory and when some event happens in the game (you die, for example), you dump it again and look for the changes in memory positions. Hidden inside the differences in the dumps are the memory addresses of the variables you are looking for (lives, HP, ammo, etc.).
process_vm_readv is perfect for this task, and our
picoRPG game will give us a pretty simple environment where we can check
the technique live. Let’s start with the dumping program. I’m pretty
sure you can write it yourself at this point, but in case you have any
doubts, this is a possible implementation.
#define _GNU_SOURCE
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/uio.h>
int show_diffs (uint8_t *addr, uint8_t *new, uint8_t *old, ssize_t len) {
int flag = 0;
for (int i = 0; i < len; i++) {
if (new[i] != old[i]) {
if (flag == 0) {printf ("\n0x%p : ", addr + i);flag = 1;}
printf ("%d/%d ", new[i], old[i]);
} else flag = 0;
}
printf ("\n");
}
int main (int argc, char *argv[]) {
pid_t pid = atoi(argv[1]);
uint64_t addr = atol(argv[2]);
ssize_t size = atol(argv[3]);
struct iovec local1[1], remote[1];
// Find code cave
local1[0].iov_base = malloc (size);
local1[0].iov_len = size;
remote[0].iov_base = (void*)addr;
remote[0].iov_len = size;
uint8_t *p, *old = malloc (size);
process_vm_readv(pid, local1,1, remote,1,0);
memcpy (old, local1[0].iov_base, size);
while (1) {
process_vm_readv(pid, local1,1, remote,1,0);
show_diffs ((uint8_t*)addr, local1[0].iov_base, old, size);
memcpy (old, local1[0].iov_base, size);
printf ("Press any key....\n");
getchar ();
}
return 0;
}Our program just accepts three arguments: the PID of the process to monitor, the target memory address to read, and the size in bytes of the memory we want to monitor. Then it just runs a loop, copying the address block to monitor from the remote process and comparing it with the previous one.
The show_diffs function just checks the newly read
buffer against the last read buffer and shows the addresses that are
different. Note that we have to pass the target address to properly show
the memory address of interest in the target’s memory addressing
space.
In this case, we already know how our game was coded, but you could
easily (for this example) figure out that the game state is stored in a
global variable that lives in the .data segment, quickly
examining the disassembler output. In the worst case, you may have to
try different memory areas like the heap or the
stack if you don’t want to look at the code.
So, in order to be able to run the program, we need to check the
process memory map and obtain the base address of the data
segment, which will be the segment with read and write permissions
backed by our binary file. Alternatively, you can include the code for
doing this inside the program yourself. I’ll show you an example of a
single execution on my box; in yours, all numbers will be completely
different, so try to understand what we’re doing and choose the proper
addresses in your own system.
$ cat /proc/3656946/maps
55f3f556c000-55f3f556d000 r--p 00000000 fd:01 49676378 /opt/syscalls/process_vm/game
55f3f556d000-55f3f556e000 r-xp 00001000 fd:01 49676378 /opt/syscalls/process_vm/game
55f3f556e000-55f3f556f000 r--p 00002000 fd:01 49676378 /opt/syscalls/process_vm/game
55f3f556f000-55f3f5570000 r--p 00002000 fd:01 49676378 /opt/syscalls/process_vm/game
55f3f5570000-55f3f5571000 rw-p 00003000 fd:01 49676378 /opt/syscalls/process_vm/game
55f414eb7000-55f414ed8000 rw-p 00000000 00:00 0 [heap]
7f6641172000-7f6641175000 rw-p 00000000 00:00 0
__--snip--__
Towards the end, you can see the heap and just above our
data segment. If the program dynamically allocates the game state using
malloc, then we should look into the heap, and
if it creates it in the stack, we should use the stack;
however, that is much trickier because the values in the stack are
changing continuously, and we may have to do some extra filtering.
On the bright side, games are pretty cool programs that squeeze CPU and memory to the extreme. In a sense, coding games is pretty much similar (conceptually at least) to hacking or system programming. Anyhow, in search of this extreme performance, games usually have their own memory allocation algorithms that tend to be simple because simple means fast, and when you have to render the screen, play the music, read user inputs, calculate the position of enemies, NPCs, objects, etc., at least 50 times per second, well, speed is really important. Overall, what that means is that scanning games’ memory is sometimes easier than scanning a regular program’s memory.
Let’s see how our dumper performs with our testing game. Let’s launch picoRPG in a console and get its PID and memory map as shown above.
Now, we launch the dumper, using the data segment base
address and size:
$ ./dump1 3656946 $((0x55f3f5570000)) $((0x1000))
Press any key....
It will just wait there and read the process memory every time we press a key. Right now, it won’t show anything because nothing has changed in the process memory. Let’s now go to the game and let’s attack a monster:
$ ./game
picoRPG 0.1
-----------
---------------
+ New Monster SPAWNED
PLAYER HP:100 XP:10
MONSTER HP:29 XP:8
* (a)ttack (f)ly ?
a
+ MONSTER ATTACK 1
+ PLAYER ATTACK 5
PLAYER HP:99 XP:10
MONSTER HP:24 XP:8
* (a)ttack (f)ly ?
Now, in the console running our dumper we just press enter:
0x0x55f3f5570040 : 99/100
0x0x55f3f5570048 : 24/29
Press any key....
Cool, we’ve found the player and monster health points. The first one shows a memory address whose value was 100 and now is 99. That’s our player. The second one shows a memory address that previously was 29 and now is 24. Now you can keep playing until your experience points are raised when you kill a monster (or you can fly and let them go down) and use the dumper to find the address for the XPs.
This is an oversimplified example for a game with a pretty minimal state and nothing else on the data segment, which isn’t that realistic, however, you can see how these kinds of tools work. In real life, all this is much more complicated, however, the techniques still apply; you just need to use more sophisticated tools able to do advanced filtering and recognize data types in memory, such as pointers, or being able to interpret some memory addresses like bytes, words, double words, or floating-point values. Any of those improvements are just basic logic that, at this point, you can implement by yourself.
Data Forcing: Writing a Trainer
Another classical element of game hacking is to provide a trainer for
the game. A trainer is a program that allows the user to change game
parameters that can reduce the difficulty a lot. For example, for our
simple RPG game, a trainer could let you set the starting health and
experience, so if you make it pretty high, you won’t ever lose the game.
Some trainers are pretty sophisticated including cool GUIs where you can
do your adjustments using the mouse. We’ll write something simpler.
Let’s use the process_vm_writev system call to write a
program that constantly overwrites the player health to its maximum
value so it won’t ever die.
The code for this is pretty simple, but I’ll include it here for completeness.
#define _GNU_SOURCE
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/uio.h>
int main (int argc, char *argv[]) {
pid_t pid = atoi(argv[1]);
uint64_t addr = atol(argv[2]);
int val = atoi (argv[3]);
struct iovec local1[1], remote[1];
local1[0].iov_base = &val;
local1[0].iov_len = sizeof(val);
remote[0].iov_base = (void*)addr;
remote[0].iov_len = sizeof(val);;
while (1) {
process_vm_writev(pid, local1,1, remote,1,0);
usleep (500);
}
return 0;
}No surprises right? It just overwrites the memory address passed as a parameter with the value passed as a parameter every 500 ms. This is maybe the simplest possible trainer you could write for a game. Try to experiment with it and include commands to overwrite also the XP, or immediately kill a monster setting its HP to 0. Sky is the limit.
Get the code at https://github.com/0x00pf/lbofs