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