As usual, the idea of this blog is a 101 lesson when dealing with this kind of challenges, from Debugger setup to remote shell execution.
I will describe a pwnable 200 called ezhp that I resolved, when dealing with pwnables normally you have two types: those easy to find the vulnerability but difficult to exploit it (the ones that you just sent multiple A's and get the segmentation fault write away) and the ones easy to exploit but hard or tricky to find the vulnerability.
Introduction
So, let's start, by opening the challenge you were presented with below message:
The quote "Luckily when you travel back in time, you still get to use all your knowledge from the present", will make totally sense at the end of the blog. As usual, I am very bad with the hints so did not even read it.
So, we are dealing with a 32-bit ELF Linux binary, good! we can just fire up backtrack and run it, displaying below info:
Please enter one of the following:
1 to add a note.
2 to remove a note.
3 to change a note.
4 to print a note.
5 to quit.
Please choose an option.
So, before doing any reversing, lets step into it with a Debugger to have a better understanding, I will explain two options for this, the first one is using IDA Debugger:
Press F9 in IDA and select "Remote Linux Debugger", then configure the process options, below mine:
The next step is to copy the your linux_server binary from "dbgsrv" folder in IDA to your backtrack machine and run it:
root@bt:~/IDA# ./linux_server
IDA Linux 32-bit remote debug server(ST) v1.17. Hex-Rays (c) 2004-2013
Listening on port #23946...
Set any breakpoint in IDA(F2) and click on Debugger->Start Process (F9). You are ready to start debugging and with the power of IDA to document everything as you go!
After a lot of debugging through all the options in the menu, I realized I was able to add notes with specific size and then write to them with a different size, causing a heap header overflow, I will explain this in a minute, I had certain experience dealing with heap headers overflow so kinda know where I was going.
Note: A chunk in this context is a space allocated in the heap and has a similar structure like this:
struct chunk {
int prev_size;
int size;
struct chunk *fd;
struct chunk *bk;
};
Above structure is stored in the chunk header, where *fd points to the next chunk and *bk to the previous one in a double-linked list.
Before going forward a fundamental knowledge must be known, when the heap allocates new chunks in memory, it decides where to put them based on the size requested, our goal is to request adjacent chunks so that a chunk filled up with more data than expected can overflow the next one, something like this:
By looking add the code of Option 3 to change a note, we can see that the size of the data to include in the note is not restricted and therefore can overflow the current chunk overwriting also the next one:
So, in order to overflow the header of next chunk I followed below steps:
- Option1 : Add note (ID=0 assigned)
- Size=256
- Option2: Add note (ID=1 assigned)
- Size=256
- Option3: to change a note
- Please give me an id: 0
- Please give me a size: 276
Important details of above steps, first, the size chosen equal to 256 is needed in order to make sure to use the whole space of the chunk and force the heap allocator to allocate a new one when adding the second note, if let's say, we would have chosen a size of 10, since too much space remaining in current chunk, a second note added would have not allocated a new chunk.
The second important point, the chunk header is 20 bytes long and is located right after the end of the previous one(if allocated with the right size), and therefore I need 256 bytes to get to the end of the written chunk plus 20 that will overwrite the header of the next one. So, after writing 276 A's to the note 1 and then trying to delete note 2 (which just got its header corrupted) I got a crash!
Program received signal SIGSEGV, Segmentation fault.
--------------------------------------------------------------------------[regs]
EAX: 0x41414141 EBX: 0xB7FC7FF4 ECX: 0x00000001 EDX: 0x41414141 o d I t s z a P c
ESI: 0x00000000 EDI: 0x00000000 EBP: 0xBFFFF478 ESP: 0xBFFFF468 EIP: 0x0804873B
CS: 0073 DS: 007B ES: 007B FS: 0000 GS: 0033 SS: 007B
--------------------------------------------------------------------------[code]
=> 0x804873b: mov DWORD PTR [eax+0x4],edx
0x804873e: cmp DWORD PTR [ebp-0x4],0x0
0x8048742: je 0x804874d
0x8048744: mov eax,DWORD PTR [ebp-0x4]
0x8048747: mov edx,DWORD PTR [ebp-0x8]
0x804874a: mov DWORD PTR [eax+0x8],edx
0x804874d: mov eax,ds:0x804b060
0x8048752: mov edx,DWORD PTR [eax+0x4]
--------------------------------------------------------------------------------
0x0804873b in ?? ()
gdb$
So, we know we just overflowed the header of the next chunk but what info was there, for the sake of this exercise, we will focus on three components only: size of the chunk, *fd and *bk, which are the 12 last bytes of the header (4 bytes each), that means we can influence their values, will come back to this later.
After confirming I was able to overflow the chunk(heap) header, I thought I was dealing with a typical malloc/free heap exploit challenge like this, and believe me I spent some hours going that route. After failing all my attempts I realized I gotta reverse the binary to understand exactly how the heap allocator was working, and below my findings.
Option 1: Adding a note
Every time a new chunk is added (Option 1), the allocated address is stored in a buffer (I called chunk_addresses[]) with a hardcoded address at 0x0804A060, the content of this buffer looks like:
[0] -> Address of note(chunk) 1
[1] -> Address of note(chunk) 2
[2] -> Address of note(chunk) 3
Where the array index represents the ID of the note added.
Option 3: Change a note
If I want to add content to the note 2, I need to choose Option 3 and provide the Id = 1, then the allocator will go to index (ID) inside the chunk_addresses[] buffer, get the address and start writing the content supplied by the attacker, wait a second!!!!! If I find a way to put my own address inside that buffer I can write anything anywhere, well, anywhere where the memory is writable of course. Below the scenario we want to hit, the note 2 is pointing to a different address.
[0] -> Address note 1
[1] -> note 2
[2] -> Address of note 3
Forcing the deallocator to write data in any destination we want! Write down this, we will come back in a minute.
Option 2: remove a note
These allocated chunks are handled in a double-linked list, and therefore, anytime a chunk (node) is deleted a standard process takes place as described below:
In the image above the second note (chunk in the middle ID 1) is being deleted and therefore, the deallocator will do the next steps:
- Get the address where ID1->bk and ID1->fd are pointing to of the chunk being deleted.
- Since ID1->bk is pointing to previous chunk ID 0, go to that chunk and change the ID0->fd to point to chunk ID2.
- Since ID1->fd is pointing to the next chunk ID 2, go to that chunk and change the ID2->bk to point to chunk ID0.
Above steps can be seen inside Delete Option code:
Pay speciall attention on the step 1, the allocator will read the content of the pointers *bk and *fd from the chunk to be deleted, and as we saw, we can overwrite its values setting them with the destination addresses that we want!
Written anything almost anywhere in memory
Now the next question is, what can we do with these findings? After a little bit of thinking, if we point ID2->bd to the address of the chunk_address[] buffer (see Option 3 section above), we can change its content and alter the list of allocated chunks, then we can call Option 3 to write our content to whichever address we just set. Below the scenario:
Create 3 notes, add content to note 2 and overflow it to alter headers of note 3 which will end up with below values:
*bk = 0x0804A060
*fd = 0x0804A070
Then call delete option which will end up with below chunk_addresses buffer[] located at 0x0804A060:
[0] -> Address note 1
[1] -> 0x0804A070
[2] -> Address of note 3
Then, we can call Option 3 to print the content of note 2 (index 1) and guess what? will write our data into the address pointing to 0x0804A070 !
Let's test if it works, and here the second way to debug the binary, when debugging with IDA, all he input entered at run time must be done via the Linux terminal which does not accept hexadecimal values so we need to create a script for that, but still we need to be able to debug the program, so, we will force the binary to listen into a socket on port 444 with below command:
socat tcp-listen:4444,fork exec:./ezph
We can also use netcat to acomplish the same thing:
while true; do nc -vv -l -p 4444 -e ezph; done
Now we can just run our script and connect to port 4444 to send our hexadecimal input.
s.connect(('192.168.74.123', 4444))
But we still need to debug it, so, since we are using python we can pause the script with "raw_input" function:
raw_input('Attach process with gdb here')
Then attach the process with gdb, set any breakpoint needed and let it continue:
>ps -fea|ezph
>gdb attach
>c
Now that everything is ready, let's run the exploit and check the chunk_headers buffer:
gdb$ x/32x 804A070
0x804a070: 0x42424242 0x42424242 0x42424242 0x42424242
0x804a080: 0x42424242 0x42424242 0x42424242 0x42424242
0x804a090: 0x42424242 0x42424242 0x42424242 0x42424242
0x804a0a0: 0x42424242 0x42424242 0x42424242 0x42424242
0x804a0b0: 0x42424242 0x42424242 0x42424242 0x42424242
0x804a0c0: 0x42424242 0x42424242 0x42424242 0x42424242
0x804a0d0: 0x42424242 0x42424242 0x42424242 0x42424242
0x804a0e0: 0x42424242 0x42424242 0x42424242 0x42424242
So, basically what just happened was, by deleting the note 3 (ID2), we altered the address of the note 2 (ID 1) inside the chunk_buffers[] array. and force it to point to 0x0804A070 as shown below:
Great! Now we can place our shellcode in memory! But wait! how can we execute it!
Here is where option 4 comes into play.
Code Execution
The option 4 is to print the content of a note by calling puts function, so, since we can write anything anywhere, what if we overwrite the whole content of puts function with our shellcode, then print the note 1 which should call our shellcode! So, let's find puts address in the binary:
readelf --relocs ezhp
Relocation section '.rel.dyn' at offset 0x348 contains 2 entries:
Offset Info Type Sym.Value Sym. Name
08049ff0 00000406 R_386_GLOB_DAT 00000000 __gmon_start__
0804a040 00000905 R_386_COPY 0804a040 stdout
Relocation section '.rel.plt' at offset 0x358 contains 8 entries:
Offset Info Type Sym.Value Sym. Name
0804a000 00000107 R_386_JUMP_SLOT 00000000 read
0804a004 00000207 R_386_JUMP_SLOT 00000000 fflush
0804a008 00000307 R_386_JUMP_SLOT 00000000 puts
We find it at 0x8004a008, just an small detail to keep in mind, this address is a pointer to a pointer, which means, the first 4 bytes of our controlled buffer (shellcode) must be a valid address, which is going to be 4 bytes ahead where our shellcode starts 0x8004a00C. Below would be the execution flow:
Print note -> [0x8004a008]-> [0x8004a00C]->
So, the last step is to generate our shellcode, we can use metasploit to do that:
>msfpayload linux/x86/shell_bind_tcp LPORT=8888 P
Connecting the dots
And finally, below is the sequence to get a remote shell:
- Option 1: Add a note
- Size:256
- Option 1: Add a note
- Size:256
- Option 1: Add a note
- Size:256
- Option 3: Change a note
- ID=1
- Size:276
- Data Entered to overflow next chunk (ID2)
- Option 2: Remove a note:
- ID=2
- Option 3: Change a note
- ID=1
- Size:256
- Inserting our shellcode
- Option 4: Print note
- ID=1
And..... viola!!!!! We just need to connect to port 8888 on remote host and cat the flag:
shitty_heap_allocators_are_shitty
Hope you enjoy it as much as I did while exploiting it.
You can find my exploit here.