Advertisement
Guest User

Untitled

a guest
Jun 19th, 2017
80
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. CVE-2007-4573: The Anatomy of a Kernel Exploit
  2.  
  3. CVE-2007-4573 is two years old at this point, but it remains one of my favorite vulnerabilities. It was a local privilege-escalation vulnerability on all x86_64 kernels prior to v2.6.22.7. It’s very simple to understand with a little bit of background, and the exploit is super-simple, but it’s still more interesting than Yet Another NULL Pointer Dereference. Plus, it was the first kernel bug I wrote an exploit for, which was fun.
  4.  
  5. In this post, I’ll write up my exploit for CVE-2007-4573, and try to give enough background for someone with some experience with C, Linux, and a bit of x86 assembly to understand what’s going on. If you’re an experienced kernel hacker, you probably won’t find much new here, but if you’re not, hopefully you’ll get a sense for some of the pieces that go into a kernel exploit.
  6. The patch
  7.  
  8. I’ll start out with the patch, or rather a slightly simplified version, that omits some hunks that will be irrelevant for my discussion. Then I’ll explain the context for the patch, and by that point we’ll have enough context to understand the exploit code.
  9.  
  10. A simplified version of the patch follows (The original is 176df245 in linus’s git repository) Note that this patch was applied to v2.6.22 – These files have moved around, so pull out an older kernel if you’re trying to follow along at home:
  11.  
  12. --- a/arch/x86_64/ia32/ia32entry.S
  13. +++ b/arch/x86_64/ia32/ia32entry.S
  14. @@ -38,6 +38,18 @@
  15.         movq    %rax,R8(%rsp)
  16.         .endm
  17.  
  18. +       .macro LOAD_ARGS32 offset
  19. +       movl \offset(%rsp),%r11d
  20. +       movl \offset+8(%rsp),%r10d
  21. +       movl \offset+16(%rsp),%r9d
  22. +       movl \offset+24(%rsp),%r8d
  23. +       movl \offset+40(%rsp),%ecx
  24. +       movl \offset+48(%rsp),%edx
  25. +       movl \offset+56(%rsp),%esi
  26. +       movl \offset+64(%rsp),%edi
  27. +       movl \offset+72(%rsp),%eax
  28. +       .endm
  29. @@ -334,7 +346,7 @@ ia32_tracesys:
  30.         movq $-ENOSYS,RAX(%rsp) /* really needed? */
  31.         movq %rsp,%rdi        /* &pt_regs -> arg1 */
  32.        call syscall_trace_enter
  33. -       LOAD_ARGS ARGOFFSET  /* reload args from stack in case ptrace changed it */
  34. +       LOAD_ARGS32 ARGOFFSET  /* reload args from stack in case ptrace changed it */
  35.        RESTORE_REST
  36.        jmp ia32_do_syscall
  37. END(ia32_syscall)
  38.  
  39. The patch defines the IA32_LOAD_ARGS macro, and replaces LOAD_ARGS with it in several places (I’ve only shown one for simplicity). LOAD_ARGS32 differs only slightly from the LOAD_ARGS macro that it is replacing, which is defined in include/asm-x86_64/calling.h:
  40.  
  41. .macro LOAD_ARGS offset
  42. movq \offset(%rsp),%r11
  43. movq \offset+8(%rsp),%r10
  44. movq \offset+16(%rsp),%r9
  45. movq \offset+24(%rsp),%r8
  46. movq \offset+40(%rsp),%rcx
  47. movq \offset+48(%rsp),%rdx
  48. movq \offset+56(%rsp),%rsi
  49. movq \offset+64(%rsp),%rdi
  50. movq \offset+72(%rsp),%rax
  51. .endm
  52.  
  53. As the name suggests, LOAD_ARGS32 loads the registers from the stack as 32-bit values, rather than 64-bit. Importantly, in doing so it takes advantage of a quirk in the x86_64 architecture, that causes the top 32 bits of the registers to be zeroed if you write to the 32-bit versions. LOAD_ARGS32 thus zero-extends the 32-bit values it loads into the 64-bit registers.
  54. System call handling
  55.  
  56. So, why is this patch so important? Let’s look at the context for the LOAD_ARGS → LOAD_ARGS32 change. ia32entry.S contains the definitions for entry-points for 32-bit compatibility-mode system calls on an x86_64 processor. In other words, for 32-bit processes running on the 64-bit machine, or for 64-bit processes that use old-style int $0x80 system calls for whatever reason.
  57.  
  58. There are three entry points in the file, one for 32-bit SYSCALL instructions, one for 32-bit SYSENTER, and one for int $0x80. They are all very similar, and we will only consider the int $0x80 case here. At boot-time, Linux configures the processor so that int $0x80 will dispatch to the ia32_syscall entry point. Ignoring a bunch of debugging information, tracing, and other junk, this entry point’s code is essentially simple:
  59.  
  60. ENTRY(ia32_syscall)
  61.        movl %eax,%eax
  62.  
  63.        pushq %rax
  64.        SAVE_ARGS 0,0,1
  65.  
  66.        GET_THREAD_INFO(%r10)
  67.        orl   $TS_COMPAT,TI_status(%r10)
  68.        testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%r10)
  69.        jnz ia32_tracesys
  70.  
  71.        cmpl $(IA32_NR_syscalls-1),%eax
  72.        ja ia32_badsys
  73.  
  74. ia32_do_call:
  75.        IA32_ARG_FIXUP
  76.        call *ia32_sys_call_table(,%rax,8)
  77.  
  78.        movq %rax,RAX-ARGOFFSET(%rsp)
  79.        jmp int_ret_from_sys_call
  80.  
  81. %eax, according to Linux’s syscall convention, stores the syscall number. The mov zero-extends it into %rax, and then we save it and the syscall arguments onto the stack.
  82.  
  83. The next block retrieves the struct thread_info for the current task, sets the TS_COMPAT status bit to indicate that we’re handling a 32-bit compatibility mode syscall, and then checks the thread’s flags to determine whether this thread has been flagged for extra processing on syscall entry. If so, we jump away to code to handle that work.
  84.  
  85. Next (at the cmpl), we check to make sure that the requested syscall is in-bounds, and branch to an error path if not.
  86.  
  87. IA32_ARG_FIXUP is a simple macro that moves registers around to translate between the Linux syscall calling convention and the x86_64 calling convention, which each hold arguments in different registers. Once we’ve fixed up the registers, the call instruction indexes the system call table by the system call number, looks up the address stored there, and calls into it to dispatch the syscall.
  88.  
  89. Finally, we save the return code from the system call into the register area on the stack, and jump to code to handle the return to userspace.
  90.  
  91. One thing we should notice about this code is that when we check that the syscall is in bounds, we compare against the 32-bit %eax register, but when we actually dispatch the syscall, we use the full 64 bits in %rax. The movl at the top of the function serves to zero-extend %eax, so that normally, the top 32 bits of %rax are zero, and this distinction doesn’t matter.
  92.  
  93. The problem arises in the “traced” path in ia32_tracesys, which is (again, with some extra code removed):
  94.  
  95. ia32_tracesys:
  96.        movq %rsp,%rdi        /* &pt_regs -> arg1 */
  97.        call syscall_trace_enter
  98.        LOAD_ARGS ARGOFFSET  /* reload args from stack in case ptrace changed it */
  99.        jmp ia32_do_call
  100.  
  101. Essentially, ia32_tracesys just calls into the C function syscall_trace_enter, with a pointer to the registers saved on the stack, and then restores the register values from the stack and jumps back to execute the system call.
  102.  
  103. Herein lies the problem. If syscall_trace_enter replaces the on-stack %rax with a 64-bit value, and LOAD_ARGS restores it, then the %eax/%rax distinction above becomes a problem. Aas long as %eax is less than (IA32_NR_syscalls-1), %rax can be much larger than the size of the syscall table, causing the call to index off the end of it.
  104. ptrace(2)
  105.  
  106. So what happens inside syscall_trace_enter, and how can we take advantage of that to load a 64-bit value into the restored %rax? Well, that turns out to be the code that handles processes traced by the ptrace(2) process-tracing mechanism, which among other things, allows the tracer to stop a child process before each system call, and inspect and modify the child’s registers for the system call procedes.
  107.  
  108. Reading ptrace(2), we find that we can use ptrace(PTRACE_SYSCALL,…) to cause a process to execute until its next system call, and then, once it’s stopped, we can use ptrace(PTRACE_POKEUSER,…) to modify the tracee’s registers.
  109. Putting it all together
  110.  
  111. So, to exploit this bug, we need to:
  112.  
  113.    * Have a 64-bit process attach to some process with ptrace.
  114.    * Use PTRACE_SYSCALL to stop that process at its next syscall
  115.    * Have the process execute an int $0x80
  116.    * Have the parent modify %rax in the child to be 64 bits wide, and allow the child to continue.
  117.  
  118. At this point, the child will index waaay off the end of the syscall table — so far off, in fact, that it will wrap around past the end of memory (On x86_64, the entire kernel is mapped into the last 2 GB of address space). Since the kernel and user programs run in the same address space, this means that, with an appropriate choice of %rax, the kernel will dereference an address in the user address space to find out the address of the function it should jump to in order to handle the system call.
  119.  
  120. My entire exploit code follows. This is not fully weaponized at all – it depends on tweaking for the specific target kernel, for one, but it works. (Well, if you can find an unpatched kernel anywhere any more these days, it works). Nowadays, if I were writing an exploit like this, I’d plug it into something like Brad Spengler’s Enlightenment, which takes care of most of the annoying bits of executing shell-code in-kernel to change the current user, disable any security modules that might be problematic, and work across kernel versions, as necessary.
  121. /* code is modded sorry.... find it yaself :P */
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement