I've had occasion to change the functionality of binary programs for a variety of purposes - mostly to instrument for debugging or logging purposes. The techniques used to do this vary but can be used for both passive monitoring or actively changing the functionality of a program. I'd like to consider one of those techniques (
ptrace) in a little more detail here - specifically the ability to stop and arbitrarily modify a running process (think
gdb).
I'm going to walk through a few examples of how to prevent a
ptrace-based approach to modifying a program. For illustrative purposes I'll use the following sample program that maintains a global variable to influence control flow at run time.
long global_flag = 1;
int main () {
while (global_flag) {
fprintf (stderr, "Running ...\n");
sleep (5);
}
fprintf (stderr, "Someone captured my flag!\n");
return 0;
}
The goal in these examples is to prevent the global variable (
global_flag) from being modified from an external process. I'm going to step through a few methods that could be used to modify this variable and how to prevent these techniques in turn.
First, I'll look to just overwrite the value directly. Since we can look at the symbols it is trivial to construct a program that will place data into the memory of our choosing within the running process using
ptrace. Obviously, this case is easier than would be for most programs due to the simplicity of the example. The approach holds, however, regardless of the scale of the actual process.
Suppose our process is PID 11896; we can find the memory location to modify using
nm
...
08048410 t frame_dummy
U fwrite@@GLIBC_2.0
0804a018 D global_flag
08048434 T main
U sleep@@GLIBC_2.0
...
If you don't have the program available you can still get at the symbols by looking in
/proc (e.g.
nm /proc/11896/exe).
The program I'm using to change memory in a particular process:
#include <sys/ptrace.h>
#include <stdio.h>
#include <stdlib.h>
#include <libgen.h>
#include <string.h>
#include <errno.h>
void usage (char * prog) {
fprintf (stderr, "USAGE: %s <pid> <addr> <value>\n", basename (prog));
fprintf (stderr, "-------------------------\n");
fprintf (stderr, " pid Process to modify\n");
fprintf (stderr, " addr Address to change\n");
fprintf (stderr, " value Value to write\n");
exit (42);
}
int main (int argc, char **argv) {
pid_t pid = 0;
unsigned long addr = 0;
long value = 0, old_value = 0;
if (4 != argc) { usage (argv[0]); }
pid = strtol (argv[1], NULL, 10);
addr = strtol (argv[2], NULL, 16);
value = strtol (argv[3], NULL, 10);
if (ptrace (PTRACE_ATTACH, pid, 0, 0)) {
fprintf (stderr, "Unable to attach to PID: %d (%s)\n",
pid, strerror (errno));
return 1;
}
old_value = ptrace (PTRACE_PEEKDATA, pid, addr, 0);
fprintf (stderr, "Original value: %ld\n", old_value);
if (ptrace (PTRACE_POKEDATA, pid, addr, value)) {
fprintf (stderr, "Unable to overwrite data @ 0x%lx (%s)\n",
addr, strerror (errno));
ptrace (PTRACE_DETACH, pid, 0, 0);
return 1;
}
ptrace (PTRACE_DETACH, pid, 0, 0);
return 0;
}
Considering the output of
nm and the PID, I'll call that as follows:
./modify 11896 0804a018 0
Then, in the terminal running the original process, you see the output "Someone captured my flag!" and the process ends.
To prevent the above result, we need to prevent
ptrace from attaching to our running process. We can use
ptrace against itself
within our program to achieve this goal. Since a process can only be traced by a single process at a time we can immediately set to trace ourselves when the program starts. The new program looks like this:
long global_flag = 1;
int main () {
ptrace (PTRACE_TRACEME, 0, 0, 0);
while (global_flag) {
fprintf (stderr, "Running ...\n");
sleep (5);
}
fprintf (stderr, "Someone captured my flag!\n");
return 0;
}
Now, when we try to connect to the process at run time we get an error from
ptrace. This is true for any process that attempts to use
ptrace to this end (e.g.
strace will report: "Unable to attach to PID: 11940 (Operation not permitted)"). Notice that this is also the case when trying to attach to the process as root.
Note for Ubuntu users: it is now the default behavior to prevent attaching to a process unless it is a direct child of the tracing process. The root user can still attach to arbitrary processes but other users are restricted (see
/etc/sysctl.d/10-ptrace.conf or
man prctl).
Unfortunately, that does not entirely solve the problem. If, instead of having the running process, a user can spawn the process within a debugger the above mechanism can still be defeated. Consider the following example.
[ezpz@mercury (ptrace)]$ gdb prevent_2
GNU gdb (Ubuntu/Linaro 7.4-2012.04-0ubuntu2.1) 7.4-2012.04
Copyright (C) 2012 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "i686-linux-gnu".
For bug reporting instructions, please see:
<http://bugs.launchpad.net/gdb-linaro/>...
(gdb) b main
Breakpoint 1 at 0x8048467
(gdb) r
Starting program: prevent_2
Breakpoint 1, 0x08048467 in main ()
(gdb) set {int}0x0804a01c = 0
(gdb) c
Continuing.
Someone captured my flag!
[Inferior 1 (process 12130) exited normally]
(gdb)
Since
gdb can set a breakpoint at main control can be gained (by the debugger) prior to being able to self-trace. This situation can be identified from within the traced program, however, by looking at the return value of the call to
ptrace.
--- prevent_2.c 2014-08-02 23:33:03.091366946 -0400
+++ prevent_3.c 2014-08-02 23:33:06.939366991 -0400
@@ -5,7 +5,10 @@
long global_flag = 1;
int main () {
- ptrace (PTRACE_TRACEME, 0, 0, 0);
+ if (0 != ptrace (PTRACE_TRACEME, 0, 0, 0)) {
+ fprintf (stderr, "Tsk tsk tsk...");
+ return 1;
+ }
while (global_flag) {
fprintf (stderr, "Running ...\n");
sleep (5);
Now
gdb can set the breakpoint and modify the memory but when execution continues the program will exit when the call to
ptrace (from within
gdb) fails.
The observant reader will realize that, from within the debugger, the return value check can also be modified. In fact, nothing prevents someone from directly modifying the binary prior to running the program. There are a variety of mechanisms - both static and dynamic - that can get around the above methods. Some can be prevented; others not. What these mechanisms
do provide is a relatively cheap investment that raises the bar when trying to dynamically change program behavior.