Interesting heap challenge named ‘Your Door Got Problem!’
Environment Setup
OS: Parrot Security VM
Tools: pwndbg, pwntools, python3
I did not participate in the challenge, so all I got was the files and this image hinting about heap fengshui.
The given libc version is 2.23, means no tcache and lack of many exploit mitigations such as top chunk size sanity check.
1 | $ ./libc.so.6 |
The binary has no PIE and only partial RELRO, meaning that the GOT can be targeted.
1 | $ checksec master_wang |
Let’s first use patchelf to link the binary to the target libc.
1 | $ cp master_wang master_wang_2.23 |
Now master_wang_2.23 is linked against the target libc, and we can start reversing it.
Reverse Engineering
Open the binary in IDA and decompile it.
The binary is not stripped, so reversing is easy.
main:
1 | int __cdecl __noreturn main(int argc, const char **argv, const char **envp) |
Seems like a typical menu challenge
view:
1 | unsigned __int64 view_huatfriend() |
disown:
1 | unsigned __int64 disown_huatfriend() |
manipulate:
1 | unsigned __int64 manipulate_fengshui() |
It’s good practise to rename variables when reversing. I did not do it because im lazy :p
Anyways now we have some insights into how the binary works.
- We can malloc, read and free chunks with manipulate(), view() and disown() respectively
- We can only malloc chunks of size 0x20, 0x40, 0x60 and 0x80
- Data passed to chunk cannot be modified after input
- Menu selection input is passed through atoi()
- Maximum of 16 allocations possible
- First occurence of the newline character will be substituted by a null byte
- view() calls printf(), so output stops at a null byte
- global variable total_changes stores the number of allocations
- global array fengshui stores pointers to chunk userdata
- fengshui is not updated after chunk is freed, so we have UAF!!
With these information there are many things we can do.
I tried various methods to exploit such as overwriting fengshui or total_changes, but all failed because the manipulate() function reads a newline…
Hence I’ll only be documenting the successful exploit strategy I used below.
Exploitation
General Strategy:
- UAF to leak heap base
- heap fengshui to forge a chunk in unsortedbin, read fd(forward pointer) to leak libc base
- forge fake size field in main_arena with fastbin dup
- link fake chunk into main_arena by poisoning fastbin
- modify top chunk pointer to just before malloc_hook/GOT entry of atoi()
- request a chunk that is serviced by the top chunk, overwrite malloc_hook with one_gadget/atoi() with system
I wrote a python script that wraps around the menu functions, so it’s easier to read the code.
1 | #!/usr/bin/python3 |
First step will be to leak the heap base address, so we can forge overlapping chunks.
We request two 0x80 size chunks, then free them into the 0x80 fastbin.
The first quadword of chunk_A's userdata now contains a pointer to chunk_B, which we can read and leak the heap base address.
We also write a fake size field just before chunk_B, so we can link that fake chunk into the 0x80 fastbin in the next step.
1 | #UAF leak heap |
heap:
1 | pwndbg> dq 0x603000 20 |
It is essential that we free chunk_B first, if not we will end up with a fd in chunk_B pointing to chunk_A, whose address ends in a null byte.
However, we will not be able to leak that fd, because printf() terminates at a null byte.
Next we can do some heap fengshui to get a chunk in the unsortedbin.
1 | #heap fengshui get unsortedbin chunk |
Here we free chunk_B again, essentially performing a fastbin dup attack to link the fake chunk we have set up previously into the 0x80 fastbin.
Next we use the fake chunk to overwrite chunk_B's size to 0xc1, and set up another chunk after it to satisfy the consolidation check when we free normal chunks.
Note that the size 0xc0 is crucial, because it allows us to remainder the freed chunk into two 0x60 chunks exactly, so there are no free chunks left on the heap that interferes with future requests.
Finally we free chunk_B and read its fd to obtain a pointer into the main_arena, a libc symbol.
heap before free:
1 | pwndbg> dq 0x603000 50 |
heap after free:
1 | pwndbg> dq 0x603000 50 |
With a libc and heap leak, we can start causing real damage.
First we free the last 0x80 chunk, because we will use it to poison the 0x80 fastbin.
Then we remainder the unsortedbin free chunk into two 0x60 chunks, and double free one of them.
In the process we overwrite the fd of the freed 0x60 chunk to 0x81.
This address does not have to be valid because we are merely using it to forge a sane size field.
Finally we call malloc three times so that the 0x60 fastbin fd in the main_arena holds the value 0x81.
At the same time we populate the fd of the freed 0x80 chunk with the address pointing to the 0x50 fastbin fd in the main_arena, so we can use the 0x60 fd as a size field for a fake chunk inside of the main_arena.
1 | #remainder unsorted chunk and write 0x81 size field to main arena |
heap:
1 | pwndbg> dq 0x603000 50 |
main_arena:
1 | pwndbg> dq 0x7ffff7dd1b10 40 |
From here it’s easy.
We request the fake chunk in main_arena and overwrite the top chunk pointer with a location of our choosing.free_hook is out of scope, because the top chunk’s size cannot be null and there are no naturally occuring data near the free_hook.
Other hooks of malloc can be hard to trigger, so it’s wise to target the malloc_hook.
However, none of the one_gadgets actually work in this case… so our next best target will be the GOT entry of atoi().
GOT:
1 | pwndbg> x/40gx 0x602000 |
Disassembling the data segment shows that there are two quadwords of padding null bytes before the bss segment, which is great for us.
This means when we request a chunk from the pivoted top chunk, the new top chunk size field will not overwrite any sensitive data and cause a segfault.
So let’s do that in the exploit script.
1 | #link chunk into main arena and overwrite top chunk pointer |
I wrote exactly 0x78 bytes of data so that the binary does not write the newline character into the main_arena and cause segfaults.
Then a call to malloc allows us to overwrite the GOT entry of atoi() to system.
GOT:
1 | pwndbg> x/40gx 0x602000 |
In the process we overwrote the GOT entry of exit with a newline, but it still points to executable memory so that’s fine.
Now any calls to atoi() will be a call to system, and we can use the menu as a shell.
1 | __ __ __ __ __ |
complete exploit script:
1 | #!/usr/bin/python3 |
Conclusion
Interesting challenge with lots of restrictions…
Many exploit ideas failed due to the binary reading a newline, got to say that was pretty annoying.
Nevertheless it forced me to persevere and be a bit more creative in order to feast on the dopamine rush from popping a shell ;)
ps: i think it can be solved by house of orange, but im too lazy to try it. please tell me if it works!
goodbye