Post

FuzzyLand: babyheap

A heap overflow challenge where partial RELRO allows overwriting the exit GOT entry to call a hidden launch_shell() function.

Category
pwn
Flag
Flag withheld
FuzzyLand: babyheap

TL;DR

The binary stores two small structures on the heap and copies user input with strcpy. By overflowing the first heap object, I can overwrite the pointer used by the second strcpy. I point that second write at the exit GOT entry and write the address of the hidden launch_shell() function. When the program later calls exit, execution is redirected into launch_shell().

flowchart LR
  A[Overflow first heap object] --> B[Overwrite pointer used by second strcpy]
  B --> C[Point write at exit GOT entry]
  C --> D[Write launch_shell address]
  D --> E[Program calls exit]
  E --> F[launch_shell opens a shell]

Initial Analysis

The challenge only provided a binary, so I started by opening it in Binary Ninja. The interesting parts are main, the hidden launch_shell() function, and the GOT/PLT area.

Binary Ninja decompilation of main Hidden launch_shell function in Binary Ninja GOT and PLT entries in Binary Ninja

The binary contains a function named launch_shell(), but main never calls it directly. A classic ret2win approach would normally overwrite a saved return address, but this program does not give a useful return path from main. That means I need another way to redirect control flow.

The useful target is the exit GOT entry at 0x404050. If I can overwrite that entry with the address of launch_shell(), the program will jump to launch_shell() when it tries to exit.

Why The GOT Is Writable

The binary uses partial RELRO.

Partial RELRO check output

Partial RELRO means the binary has a GNU_RELRO segment, but it does not use BIND_NOW. As a result, .got.plt remains writable at runtime. Full RELRO would make this attack much harder because the dynamic linker would resolve symbols early and the GOT would become read-only.

For this challenge, partial RELRO makes the exit GOT entry a viable control flow target.

Vulnerability

The program allocates heap objects and then copies user-controlled data into them with strcpy. The first copy can overflow into the next heap object.

The important detail from the decompiler is that the second strcpy uses a pointer stored in the heap object. If I corrupt that pointer during the first input, the second input becomes an arbitrary write primitive:

  1. First input: overflow the heap object and overwrite the destination pointer.
  2. Second input: strcpy writes my bytes to the corrupted destination pointer.

In this case, I overwrite that pointer with 0x404050, the address of the exit GOT entry.

Finding The Offset

To find the offset to the pointer, I used a simple alphabet pattern and watched which bytes appeared in the corrupted heap structure.

1
2
3
4
5
6
7
8
9
haicha21@vm# ./babyheap < letters.bin
Hello :)
I'm gonna tell you a little about myself
0x2aaaab4c92a0 = { 1, 0x2aaaab4c92c0 }
0x2aaaab4c92e0 = { 2, 0x2aaaab4c9300 }
Now tell me what to put into the first one.
0x2aaaab4c92a0 = { 1, 0x2aaaab4c92c0 }
0x2aaaab4c92e0 = { 5353172790000830793, 0x4c4c4c4c4b4b4b4b }
Now tell me what to put into the second one.

The corrupted pointer became 0x4c4c4c4c4b4b4b4b, which corresponds to the pattern bytes KKKKLLLL in little-endian order. That puts the destination pointer 40 bytes after the start of the first input.

So the first payload is:

1
40 bytes padding + p64(0x404050)

Payloads

The second payload writes the address of launch_shell() to the exit GOT entry. Binary Ninja shows launch_shell() at 0x401216, so the second payload is just that address in little-endian form.

Conceptually, the payloads look like this:

1
2
3
4
5
6
7
from pwn import p32

exit_got = 0x404050
launch_shell = 0x401216

payload1 = b"A" * 40 + p32(exit_got)
payload2 = p32(launch_shell)

After the first input, the second strcpy writes to exit@got. After the second input, exit@got points to launch_shell().

Remote Exploit

The remote service expects two separate inputs, so I used a Python script to send both payloads and then read the flag in ./flag.txt.

After both payloads are sent, the program calls exit, jumps to launch_shell(), and opens a shell.

[+] Opening connection to chals.fuzzy.land on port 5502: Done
Hello :)
I'm gonna tell you a little about myself
0x108cc2a0 = { 1, 0x108cc2c0 }
0x108cc2e0 = { 2, 0x108cc300 }
Now tell me what to put into the first one.
0x108cc2a0 = { 1, 0x108cc2c0 }
0x108cc2e0 = { 4702111234474983745, 0x404038 }
Now tell me what to put into the second one.

[flag withheld]

Challenge Attachments

This post is licensed under CC BY 4.0 by the author.