Show HN: dlinject.py – Inject a .so into a running Linux process, without ptrace
github.comThe tl;dr of the technique is to use /proc/$pid/mem to overwrite the stack. (Since you don't have direct control of the instruction pointer this way, there's some complexity in loading shellcode somewhere and having the process return to it to get it to usefully execute your code, much like an actual stack-corruption exploit.)
On a normal Linux system, /proc/$pid/mem is protected by the same kernel permission check as ptrace, and a Linux security module like Yama, the thing that disables ptracing unrelated processes on Ubuntu etc., will also block this tool (which the README mentions). It seems like it's mostly useful for cases where people are blocking the ptrace syscall (like Docker's default syscall filter, maybe?) and not loading an LSM.
Cool demonstration that blocking the ptrace syscall isn't sufficient. By the way, blocking /proc too isn't sufficient either: there's the process_vm_writev and process_vm_readv syscalls that work like writing/reading /proc/$pid/mem. I think it's harder to write a robust tool using only those syscalls, but I wouldn't bet on it being impossible.
If you really want to do syscall filtering to confine an untrusted process (as opposed to reducing attack surface from potential bugs in otherwise-permissible syscalls, which is I think Docker's goal), you need to start from empty and allow syscalls instead of starting from full and blocking them. Alternatively, maybe just run the untrusted code as another user account or something.
> It seems like it's mostly useful for cases where people are blocking the ptrace syscall (like Docker's default syscall filter, maybe?) and not loading an LSM.
It’s a shot in the dark, but I’d guess this is designed to work with programs that neuter themselves or commit suicide if you ptrace them, as is common for mildly sophisticated malware or (less often) CTF challenges.
How do you detect that you're being ptraced? I see the link to https://www.aldeid.com/wiki/Ptrace-anti-debugging in the README, but
a) the program has to actively run its check while being ptraced to notice. If you attach to the program (which pauses it), inject your code, run your code, and then detach, the program's own code will not notice it's being ptraced, no?
b) if you want to run the program's own code while being ptraced, can't you just stop on the ptrace syscall and lie about its result? I think `strace -e inject=ptrace:errno=0` will avoid the program in that wiki page from realizing it's being traced. (You can use seccomp-bpf for this if you don't want to take the performance overhead of stopping on every syscall.)
> a) the program has to actively run its check while being ptraced to notice. If you attach to the program (which pauses it), inject your code, run your code, and then detach, the program's own code will not notice it's being ptraced, no?
Depends on if all threads were put into a paused state. Also, the program that's being debugged might have spawned some additional processes that will be checking if the parent is being ptraced.
Replace "self" with the PID you want to check if it is being traced:
$ cat /proc/self/status | grep Tracer TracerPid: 0Ahhh, yes, it's probably hard to track down a random subprocess somewhere that's checking and then passing that info back to the original process. (I think the subprocess could even just try to PTRACE_ATTACH the original process and see if it works.)
Some of the detection methods methods were mentioned in a comment (here’s one more: timers), but I should also mention that some binaries try to resist ptrace attempts with ptrace(PTRACE_TRACEME, …) as well if you’re too slow to get in before that.
This might be off topic but I never really understood the utility of PTRACE_TRACEME. The documentation suggests that the parent must be a cooperating process (the wording "probably shouldn't" in the documentation is very suspicious). But if the parent knows it will ptrace the child, it could very well set up a pipe and have the child be blocked on it, then PTRACE_ATTACH, then unblock the child with the pipe.
As such, I never truly understood the semantics behind PTRACE_TRACEME, and I'm not sure what bad things will happen if you use that when the parent isn't expecting to ptrace the child.
As I understand it, PTRACE_TRACEME causes the process to be put into "traced" mode, where signals (and exec* calls, which will cause a SIGTRAP to be sent) cause the process to stop. This is useful in the context of a debugger because the debugger process will fork, the child will call ptrace(PTRACE_TRACEME, …) to put itself into "traced" mode, then exec the process you want to debug and be conveniently placed into ptrace stop at the first instruction to be executed in the new binary.
The problem with PTRACE_TRACEME is that if you do it to yourself and recieve a signal, you're put into ptrace stop there's and there's no way out of it unless the parent knows how to get you out of it (using ptrace, of course). Sending signals will not work, even SIGKILL; somewhat humorously, on iOS, if you attempt to do the equivalent (using the similar PT_TRACE_ME) and oops yourself, the entire system will slowly grind to a halt as it tries to (at least, I think…) SIGKILL your process for a variety of reasons and fails, at some point locking up waiting for process termination.
> Since you don't have direct control of the instruction pointer this way, there's some complexity in loading shellcode somewhere and having the process return to it to get it to usefully execute your code, much like an actual stack-corruption exploit.)
The readme states that the (primary) shell code is written at the EIP location, so no return hijacking is required.
Whoops, yes, I misread that!
> If you really want to do syscall filtering to confine an untrusted process (as opposed to reducing attack surface from potential bugs in otherwise-permissible syscalls, which is I think Docker's goal), you need to start from empty and allow syscalls instead of starting from full and blocking them. Alternatively, maybe just run the untrusted code as another user account or something.
It looks like the gVisor approach for containers (reimplementing parts of the kernel syscalls in userspace and disallowing the rest)
I'm new to the cyber world and have a few questions about how this works under the hood since my exposure to memory management is really minimal. Why did we have to mess with this shellcode within the injector (dlinject.py) rather than just somehow identifying the PID of the target and loading the injected code directly? Is it not possible via some simply python call to perform the injection, and is that why we're playing with the x86?
Also, would this work on systems with ASLR?
Could malicious code be obfuscated, transformed, and then subsequently injected into a process like explorer with this dlinject.py? What are our limitations here?
> Why did we have to mess with this shellcode within the injector (dlinject.py) rather than just somehow identifying the PID of the target and loading the injected code directly? Is it not possible via some simply python call to perform the injection, and is that why we're playing with the x86?
Yes, there is no simple way to do this. Injecting arbitrary code into a process is always finicky, and this project is even more so because it eschews the API used for doing stuff like this (ptrace, which is still on the level of registers and memory addresses) and going through other kernel interfaces to try to perform similar things.
> Also, would this work on systems with ASLR?
It does, because it defeats ASLR by looking at the process’s memory mappings and finding libc in that (which it uses for dlopen).
> Could malicious code be obfuscated, transformed, and then subsequently injected into a process like explorer with this dlinject.py? What are our limitations here?
None, it can inject pretty much anything. The only limitation is security protections that the OS provides to stop you from touching other processes in this way.
There's no function, given the PID of a target process, to inject code directly. In order to load code from a library, you need to A) map the library into memory B) handle relocations (i.e., connect up that library's references to functions in already-loaded libraries like libc) C) start executing it. Step A can be done from a process itself with mmap, but there isn't a function to map memory into another process. Step B is just some modifications to values in memory. Step C involves somehow changing the instruction pointer. All of these are easy to do if you're running code as the process itself - in particular, if you can get control of the instruction pointer, you can avoid doing steps A and B manually by just instructing the program to use the existing libc dlopen() function, which mmaps a library and handles relocations.
So we need to be able to change the instruction pointer of the process and coax it into calling dlopen(). The functionality available to you is:
- You can use ptrace to stop the process, read and write the saved registers, read and write its memory, resume the process, or resume the process until the next instruction (single step) or next syscall. It's the API used by debuggers. If you run `print dlopen("file.so")` from GDB, this is exactly what GDB will do: it'll use ptrace to make up a stack frame to call dlopen with the arguments you specify and then hit a breakpoint, and GDB will print the result. Another good example of using this to inject code for non-malicious purposes is https://github.com/nelhage/reptyr , which lets you tell another process to switch terminals (e.g., move it into a screen/tmux session).
- You can use /proc/$pid/mem or the process_vm_writev syscall to modify the stack but not the instruction pointer, because the stack is in memory. That puts you effectively in the same position as an exploit writer: you have to create a stack frame that returns to custom code somehow, or uses return-oriented programming to call the function you want to call. Then when the program returns from its current stack frame, it'll call your shellcode or ROP gadget.
- You can use /proc/$pid/mem or the process_vm_writev syscall to modify the code at the instruction pointer, even though you can't modify the instruction pointer itself. That's what this program does - it backs up a segment of instructions about to be executed, overwrites it with shellcode, and restores the instructions once the shellcode is done.
ptrace, /proc/$pid/mem, and process_vm_readv/writev have permission checks. On most systems, you have to be running as the same user as the program you're trying to mess with. On systems including the "Yama" Linux security module - notably including Ubuntu - you also have to be an ancestor process of the process you're trying to attach to, which is a security measure intended to make it harder for malware to steal secrets out of other programs you're running, without preventing debuggers etc. from working. See https://www.kernel.org/doc/Documentation/security/Yama.txt for details.
(Again, this is the same interface as a debugger uses. If you're not prevented from debugging your own programs, yes, you can inject malicious code into them without limitation - and it's generally easier to use a debugger than to write your own custom injector to do it.)
If you want to avoid the side effects of SIGSTOP, you can put the target process in a freezer cgroup instead.
This was recently suggested to me on Twitter, I'll have a look at implementing it tomorrow.
What are the circumstances in which someone would be doing something like this?
Debugging a program that resists ptrace.
relatively covert backdoor