Skip to content

Race condition in ptrace() allows setuid debugging #5230

Description

@cees-elzinga

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions