Some binaries prohibit any modification, which means you cannot insert 0xCC (software breakpoint) at the entry point of the target executable. In such cases it would be a good idea to wait for completion of the execl() function (just to make sure that the child process has been replaced with the code of the target executable):
/* PTRACE_SETOPTIONS is used in order to point the ptrace() to certain event types that we need to be notified of (see man ptrace). In this case, we need to know when a call to exec() occurs. */ ptrace(PTRACE_SETOPTIONS, pid, PTRACE_O_TRACEEXEC, NULL); /* wait* functions are used to wait for a process to change state. In our case that would be calling exec() by the child pid – the ID of the process whose state we are expecting to change status – a variable of type int where the status (or description of the new state) will be stored to. The last argument may be 0 or options (see man waitpid) */ waitpid(pid, &status, 0);
The waitpid() function returns -1 on error or the pid of the child whose state has changed on success.
Once waitpid() returns (if it does not return, then check your code) you should let the target process run until a system call occurs. It is strongly recommended to skip several system calls to allow the target process to initialize properly (for example, skip the first 5 system calls):
Call ptrace() this way until you have encountered 5 system calls. You need to remember, that the state of the process changes twice during a system call – first right before “entering” the system call and again a second time immediately after the system call's completion.
Code injection during a system call is a bit complicated and may even be unstable if not properly implemented. It is best avoided and anyway, it is beyond the scope of this article.
Injection using software breakpoint
In most cases, however, unless it is some kind of well- protected software, you should be able to modify the binary and insert a software breakpoint at a proper location and, for the sake of simplicity, let's follow this scenario.
After you have successfully traced the exec() function, all you have to do is to tell the process to keep running with:
What I haven't mentioned yet, is that you have to check for a reason why the traced process has stopped. The waitpid() man page describes a set of macros you may use for this purpose:
WIFEXITED(status) – returns true if the child has exited normally;
WEXITSTATUS(status) – returns the value the child passed to exit() function (if WIFEXITED returned true;
WIFSIGNALED(status) – returns true if the child was terminated by a signal;
WTERMSIG(status) – returns the number of the signal that terminated the child;
WCOREDUMP(status) – returns true if the child produced core dump;
WIFSTOPPED(status) – returns true if the child was stopped; this is what we are expecting;
WSTOPSIG(status) – returns the number of the signal that caused the child to stop; we are expecting signal number 5 (SIGTRAP);
WIFCONTINUED(status) – returns true if the child process was resumed with SIGCONT.
If after all the checks, it appears to be that the child process has stopped due to the software breakpoint, you may start the injection process. My suggestion is to start by copying the registers of the traced process:
ptrace(PTRACE_GETREGS, pid, NULL, regs);
where regs is a pointer to struct user_regs in which the registers are going to be stored.
The next step is to backup the original code of the target process, which is going to be overwritten by your “shellcode”.
Depending on the length of the shellcode, you would have to call ptrace() several times:
shellcode – an array of type unsigned char containing the shellcode itself.
It would be a good idea to put a software breakpoint at the end of the shellcode to use as ret instruction.
Call ptrace() again in order to let your shellcode run:
ptrace(PTRACE_CONT, pid, NULL, NULL);
and waitpid() for the child process to stop again just like you've been waiting for the first breakpoint.
Let it run
Once your shellcode has performed what is was meant to perform (place modified code and link it to the proper locations in the process' memory), it is very important to restore the target process to its original state:
This operation will restore the values of the registers of the target process and detach from it.
At this point your loader may safely exit and let the modified process continue execution.
As those of you familiar with the same sort of procedure in Windows may notice, code injection in Linux is a tiny bit more complicated. However, it is still relatively easy and reliable way to modify an existing executable without actually patching the file.
Alexey Lyashko writes a blog System Programming which is well worth reading if you want the technical take on programming.