Linux comes with an interesting system call ptrace
which can be abused to elevate privileges and steal sensitive information.
The ptrace() system call provides a means by which one process
(the “tracer”) may observe and control the execution of another
process (the “tracee”), and examine and change the tracee’s
memory and registers. It is primarily used to implement
breakpoint debugging and system call tracing.
If you have used debuggers such as gdb
, strace
they are based on ptrace
.
From the Linux kernel documentation:
One particularly troubling weakness of the Linux process interfaces is that a single user is able to examine the memory and running state of any of their processes. For example, if one application (e.g. Pidgin) was compromised, it would be possible for an attacker to attach to other running processes (e.g. Firefox, SSH sessions, GPG agent, etc) to extract additional credentials and continue to expand the scope of their attack
This article demonstrates following attack vectors introduced by ptrace
:
- Key-logging : https://github.com/raj3shp/termspy
- Process Injection: https://github.com/raj3shp/ptrace_injection
Key-logging
Examining a process’s memory and registers gives an attacker an easy way to intercept any processes such as bash, sudo, ssh to log key strokes to get hold of secret information such as sudo passwords and SSH keys.
Here is my simple proof of concept project termspy to demonstrate this process.
- First attach to the target process with
PTRACE_ATTACH
which sends aSIGSTOP
signal to the target process making it a tracee of thetermspy
process - Then use
PTRACE_SYSCALL
to restart the stopped process and continue running it until the next entry or exit from a system call execution. - To perform key-logging we focus on
write
syscall since it will be executed when a user types a keystroke into the bash or terminal process. Snippet below shows the registers used for execution of a syscall in x86–64 bit systems
- With
PTRACE_PEEKUSER
we can read the value of these registers and checkrax
register to compare if the system call being executed iswrite
orig_eax = ptrace(PTRACE_PEEKUSER, pid, 8 * ORIG_RAX, NULL)if (orig_eax == SYS_write)
write
syscall takes three parameters which are stored inrdi
,rsi
andrdx
parameter respectively.
We are interested in the second parameter which is a pointer to the char array containing user’s keystrokes. We can read this address with PTRACE_PEEKUSER
and use PTRACE_PEEKDATA
to read the data at this address.
params[1] = ptrace(PTRACE_PEEKUSER, pid, 8 * RSI, NULL);data.val = ptrace(PTRACE_PEEKDATA, child, addr + i * 8, NULL);
- This returns lot of non-ASCII text which we don’t care about so we filter out any non-ASCII characters. For the demo, we just print out the ASCII characters to
stdout
- This works pretty well when attached to a bash or terminal process.
Process Injection
With ptrace
we can also write data into the process effectively achieving the ability to inject code into an existing process. This can be very handy for defense evasion and potentially achieve privilege escalation.
By default many distributions have sudo caching enabled. When a shell process invokes sudo, system caches the password in the form of a token which is valid for certain amount of time so password is not required for any further sudo commands invoked by the same shell until the timer expires. By injecting our shell-code into such shell process, we can effectively become root.
I wrote a small proof of concept project ptrace_injection which uses ptrace
to inject shell code into an existing process with PTRACE_POKEDATA
.
The shell code forks a child process and spawns a reverse shell. If the process we inject into recently ran a sudo command with sudo caching enabled, we can spawn root shell with sudo -i
on our reverse shell connection.
Mitigation
- Restrict
ptrace
to only the root user. You can do this by settingptrace_scope
to admin-only mode.
# sysctl kernel.yama.ptrace_scope=2
- Disable sudo credential caching by setting
timestamp_timeout
to0
. Add following line to the/etc/sudoers
file.
Defaults timestamp_timeout=0