A race condition exists in ptrace() that allows any user to attach to setuid processes. The core issue is a lack of synchronisation between ptrace() and execve().
Details
When handling a ptrace() syscall the kernel starts by checking the state of the process, permissions, supplied arguments etc. If all these checks pass the kernel will continue to perform the requested action. This can be seen as two seperate steps.
The problem is that there is a small window between these steps. In this window 'things' could change without the kernel being aware of that. For example, the debuggee might be running with different privileges.
A sample timeline of how the bug might happen:
| Tracer | Debuggee |
----------------------------------------
| | running ... |
| PT_ATTACH | |
| 1. run checks | |
| 2. perform action | |
| | |
| PT_CONTINUE | |
| 1. run checks | |
| 2. perform action | |
| | |
| PT_POKE | |
| 1. run checks | |
| *interrupt* | |
| | execl() setuid |
| | setting m_euid |
| | load elf |
| 2. perform action | |
| BUG HERE | |
Reproducing
The window to race is very small, and in this window a lot of other code must run. To reproduce the bug its easiest to widen the window with a fake sleep at the critical moment.
I used the following patch in Kernel/Ptrace.cpp
case PT_POKE:
+ // Fake a delay to increase the race window
+ for(int i=0; i<4096; i++) {
+ klog() << "PT_POKE: delaying..." << i;
+ }
+ klog() << "PT_POKE: all security checks done, going to poke!";
Before showing the bug you can use the attached demo script to show NORMAL behaviour, even when this patch is applied. The script will:
fork()
- parent: attach to child
- child: run a setuid binary
- parent: call
PT_POKE. this fails because of permissions
Expected output:
$ demo
parent: running with pid xx
parent: attached, calling PT_CONTINUE
child: running with pid xx
child: calling setuid binary
New password:
parent: calling PT_POKE. This should fail.
PT_POKE: Permission denied
Now comment out the sleep in the parent:
// sleep a bit and let the child execute execl
// sleep(3);
This will mimic the timeline above. The code now calls PT_POKE before the child has started the setuid binary. All checks pass, but because of the added print statements it will hang for a bit after the checks. By the time printing is done the debuggee has changed to the setuid passwd, but we are still allowed to poke it.
Expected output:
$ demo_no_sleep
parent: running with pid xx
parent: attached, calling PT_CONTINUE
child: running with pid xx
child: calling setuid binary
New password:
parent: calling PT_POKE. This should fail.
PT_POKE: Bad address <-- BUG: should be permission denied
The error message is a bit misleading. Because the demo tries to poke an unmapped address (0x41414141) and errors out. But to confirm the real problem you can verity the kernel log. It should print "PT_POKE: all security checks done, going to poke!" to indicate it passed all the checks.
Exploitation
Exploitation might be tricky:
- the racing window is very small
- can only do one ptrace() call on debuggee
- dont know anything about memory layout of debuggee
However, I feel that if there is a reliable way to win the race exploitation should be possible. An attacker could for example break ASLR by running the exploit many times.
Winning the race might be possible by using tricks with signals, thread priorities, or abusing side-effects/delays of functions between the two steps. I did not look into this.
Attachments
demo.cpp.txt
A race condition exists in
ptrace()that allows any user to attach to setuid processes. The core issue is a lack of synchronisation betweenptrace()andexecve().Details
When handling a
ptrace()syscall the kernel starts by checking the state of the process, permissions, supplied arguments etc. If all these checks pass the kernel will continue to perform the requested action. This can be seen as two seperate steps.The problem is that there is a small window between these steps. In this window 'things' could change without the kernel being aware of that. For example, the debuggee might be running with different privileges.
A sample timeline of how the bug might happen:
Reproducing
The window to race is very small, and in this window a lot of other code must run. To reproduce the bug its easiest to widen the window with a fake sleep at the critical moment.
I used the following patch in Kernel/Ptrace.cpp
Before showing the bug you can use the attached demo script to show NORMAL behaviour, even when this patch is applied. The script will:
fork()PT_POKE. this fails because of permissionsExpected output:
Now comment out the sleep in the parent:
This will mimic the timeline above. The code now calls
PT_POKEbefore the child has started the setuid binary. All checks pass, but because of the added print statements it will hang for a bit after the checks. By the time printing is done the debuggee has changed to the setuidpasswd, but we are still allowed to poke it.Expected output:
The error message is a bit misleading. Because the demo tries to poke an unmapped address (0x41414141) and errors out. But to confirm the real problem you can verity the kernel log. It should print "PT_POKE: all security checks done, going to poke!" to indicate it passed all the checks.
Exploitation
Exploitation might be tricky:
However, I feel that if there is a reliable way to win the race exploitation should be possible. An attacker could for example break ASLR by running the exploit many times.
Winning the race might be possible by using tricks with signals, thread priorities, or abusing side-effects/delays of functions between the two steps. I did not look into this.
Attachments
demo.cpp.txt