How does ptrace work
In particular, we are interested in the TracerPid field which shows us PID of process tracing this process 0 if not being traced. Lets add sleep 10 before detaching part of our injection code and rerun the injection. To counter this we can use hook based approaches.
It consists of a standard sockets-based interface for user space processes and an internal kernel API for kernel modules. We use a process events connector protocol to detect ptrace system calls.
Connector protocol can dynamically trace the following events:. BPF is an in-kernel virtual machine which can be used for system tracing. It is an extremely powerful linux feature that allows the user to trace kernel and user space. Linux started support for bpf system call from kernel 3.
For tracing ptrace we will use bpftrace. It is a high-level tracing language for BPF available in recent Linux kernels 4. Our one-liner prints the process name for every ptrace system call that the process executes. This tracer instruments tracepoints to achieve the desired functionality.
Alternatively, we could use kprobes, but while kprobes allow us to instrument arbitrary kernel functions it is less stable, so if possible, tracepoints are preferred. You can read here more about different linux tracing systems. To resolve that issue, we can modify our tracer to detect only specific types of the ptrace syscalls.
This time we write bpftrace code in a separate file:. We hope you found this post useful, and that you learned about ptrace and some of its usages — including how to disable and dynamically detect it. We highly recommend disabling ptrace in production environments, because malicious actors can instrument it for injection attacks.
If you find it is necessary for ptrace to be enabled, constrain it as much as possible and if feasible add a dynamic ptrace detection mechanism. Before executing the system call, the kernel checks whether the process is being traced. If it is, the kernel stops the process and gives control to the tracking process so it can examine and modify the traced process' registers.
As you can see in the example, a process forks a child and the child executes the process we want to trace. This tells the kernel that the process is being traced, and when the child executes the execve system call, it hands over control to its parent. The parent waits for notification from the kernel with a wait call. Then the parent can check the arguments of the system call or do other things, such as looking into the registers.
When the system call occurs, the kernel saves the original contents of the eax register, which contains the system call number. The first argument determines the behaviour of ptrace and how other arguments are used. The significance of each of these requests will be explained in the rest of the article. The kernel stores the contents of registers in this area for the parent process to examine through ptrace. The status variable in the wait call is used to check whether the child has exited.
This is the typical way to check whether the child has been stopped by ptrace or was able to exit. If you want to read register values at the time of a syscall entry or exit, the procedure shown above can be cumbersome. Now it's time for some fun. In the following example, we will reverse the string passed to the write system call:. What do you do? In this whitepaper I will discuss two little known tools that will allow you access into the inner workings of processes on unix systems.
Did you ever wonder how debuggers work? How they gain complete control over the process they are debugging? The following examples will give you the basics to write your own quick and dirty 'debugger' application, specifically targeted to solving the problem at hand.
Ptrace is basically a kernel hook into the task dispatch logic. It provides a mechanism by which you can write a program that can attach to a target process, breakpoint it, single step it, and have access to its entire address space, stack, and registers. With this, you will be able have your program trace execution of a target process, monitor 'watch points', and check for various program conditions, even when the client is too cheap to spring for a license for the debugger package.
It provides a direct interface to many kernel data structures, as well as most process data structures. It also will allow you read and sometimes write access to a process virtual address space. Most debuggers are based on calls to ptrace.
The exception to this is Solaris. Whereas ptrace exists on Solaris, it is poorly implemented and has terrible documentation. I'll cover both approaches here. This is not a tutorial. Although the man pages for ptrace are usually poorly written they are written for debugger writers, who usually know this stuff already , they provide a good reference point.
I'll provide some interesting examples of code, and some basic descriptions of how it works. The rest is up to you. Again, that's why we get paid the big bucks. But the hope here is that this will provide you with some tools that will help you gain information to solve those 'unsolveable' problems. Assume we have a client application that goes into an infinite loop. We have no further information, and we have no access to any debuggers. Through the 'ps' command, we can identify which process is ringing up the CPU time, but that's about all the info we have.
What we would really like to do is to be able to stop the process and see exactly where the program counter is, and what its stack context looks like. With a debugger this is trivial.
Without one, it is a challenge. Once we have the address we're executing, then we can use a link map or the 'nm' utility to find the name of the routine being executed. A little more math, and we get the exact offset into the routine. Finally, if we have source, we can narrow it down to the lines. The following program uses the ptrace system calls to attach to the supplied target PID.
The act of attaching effectively breakpoints the target process. As we single step the process, we'll record each new IP and SP, until we come back to the original point we started from. This will give us one complete iteration of the offending infinite loop.
The nm 1 utility was run against the offending process's executable image. Dymanically resolved symbols are also printed out, but obviously with no address information. Since all local routines are statically linked, you can see the names of each routine in the process. Other useful utilities along this line are objdump 1 and readelf 1. The objdump 1 utility provides much more useful information than the nm 1 utility, dumping all ELF sections, as well as providing disassembly listings of all code sections.
Objdump 1 comes as part of the GCC toolset, but may not be available on all systems. The nm 1 utility is, and that is why I used it in this example. Ok, a few minor things to note. The main function starts at 0xc0 and runs for at most 0xc bytes. There are a bunch of obvious library calls, and two suspicously named functions, CalcIteration , which starts at 0xe8 and runs for 0x6d bytes, and SubFunc1 , which starts at 0x and runs for 0xc bytes. Ok, this should be one complete iteration of our infinite loop.
From there, we progress for 9 more instructions till we hit 0x So we can infer that the 0x is a printf statement. Proceeding after the printf jump we're still in CalcIteration. We stay in that for the next 10 instructions, till we hit 0x, the starting address of SubFunc , which we execute for 5 instructions.
The last address, 0x is again part of the dynamic relocation jump table, which resolves to a call to sleep. So, what have we found out. Well, without being able to see the source code, and without access to a debugger, we know our infinite loop bounces between the routines CalcIteration and SubFunc1 , and includes one call to printf and one call to sleep.
Enough to solve the problem? Maybe, maybe not, but it sure is a lot more information than we had when we started. Consider the problem where we know that some memory address is getting corrupted, but we have no idea when or where it is getting overwritten.
We've all at times wished for a way to implement a 'watch point', or a way to monitor a known memory location to see when it changes, and exactly what code we were executing when it changed. Some systems now allow this through debuggers, others don't. But we're assuming that we have no access to debuggers, right?
0コメント