FSOP on stdin
Background
This technique was used to solve a challenge called suisoku
created by Luma
, which was the least solved pwn (among the solveable ones, ofc), having only two solves.
Analysis
The flag is loaded somewhere in data, and the target is to read from that address
void *sub_401296()
{
FILE *stream; // [rsp+0h] [rbp-10h]
stream = fopen("flag.txt", "r");
if ( stream )
{
*(_BYTE *)(fread(&unk_404240, 1uLL, 0x7FuLL, stream) + 4211264) = 0;
fclose(stream);
return &unk_404240;
}
else
{
printf("Error reading flag. Contact admin.");
return 0LL;
}
}
The vulnerability was quite easy to spot, the full code being under 100 lines. Here are the relevant functions
int __fastcall custom_read(__int64 a1, int a2, const char *a3)
{
size_t v3; // rax
LODWORD(v3) = a2;
if ( (unsigned __int8)a2 <= 8u )
{
memcpy(s1, (const void *)(a1 + 2), (unsigned __int8)(a2 - 2));
v3 = strlen(a3);
if ( (unsigned __int8)(a2 - 2) == v3 )
{
LODWORD(v3) = strcmp(s1, a3);
if ( !(_DWORD)v3 )
LODWORD(v3) = puts("Wow, you guessed it!");
}
}
return v3;
}
ssize_t __fastcall custom_print(__int64 a1, unsigned int a2)
{
ssize_t result; // rax
result = a2;
if ( (unsigned __int8)a2 <= 8u )
return write(1, (const void *)(a1 + 2), (unsigned __int8)(a2 - 2));
return result;
}
These were called from the main function using this logic
v4 = 2;
while ( v4 )
{
fgets(s, 256, stdin);
s[strcspn(s, "\n")] = 0;
if ( s[0] == -59 )
{
custom_read((__int64)s, (unsigned __int8)s[1], v5);
--v4;
}
else if ( s[0] == -48 )
{
custom_print((__int64)s1, (unsigned __int8)s[1]);
}
}
puts("Goodbye.");
The vuln is a TOCTOU interger overflow, the program first checks if the length (a2) is equal or lower than 8, if yes then it unsigned subracts two from it and uses that as length, this is problematic as if lengths such as 1
and 0
are sent, they will pass the first check, but after the -2
they will become large numbers (254, 255 respectively).
This, in turn, allows us to read and write up to 255 from an 8-byte buffer. Should be easy to get the shell now, right?
Exploitation
Actually, not really, as the buffer is located just before .bss
, so we have almost nothing we can overwrite. The only thing available in the 255 bytes after the buffer is a stdin
pointer, but can we actually exploit just this?
Firstly, we can easily leak the libc base using the print function, as the stdin pointer is a libc address. Now that we have that, what can we accomplish by overwriting a stdin
pointer?
Doing some research into this, it turns out we can gain arbitrary write using a technique called FSOP. I won’t provide the payload here, as it is quite large, and numerous articles on this topic are already available on the internet.
Now armed with an arbitrary write, I chose to target the libc stdout
structure to get an arbitrary read. The issue I had during the CTF, which took me a long time to spot, was that I was writing directly to stdout
, so when I sent another command to exit the program after writing to it stdout
, the fake FILE structure header would be overwritten, and the exploit would fail.
To resolve this issue, we can simply write at stdout-0x10
or any other offset preceding it. This ensures that when we provide the 2 bytes for exit, they are written at stdout-0x10
, thereby not affecting our payload.
Alternatives
In addition, during the CTF, I also discovered an alternate path that sadly only worked locally, as it required a very specific stack setup.
If you go back to the main function, we can see that puts
was called the first time only after we exited the loop already, so in theory, if we had a one-shot gadget, we could overwrite the libc got for puts with it.
The local stack layout was something like
random address
puts
flag_addr
So I found a perfect gadget for this case (another pop was done during the pop rbp of a function, removing the random address from the stack)
0x0000000000125a00 : pop rax ; pop rdi ; call rax
But sadly, the remote instance had a slight difference, so this didn’t work.