My journey on one NULL byte overwrite to remote code execution - R3CTF/YUANHENGCTF 2024 Nullullullllu writeup
Here’s my writeup for the “easiest” pwnable challenge in r3CTF 2024 - Nullullullllu. Only 13 out of 764 teams solved this challenge so I think it’s definitely not easy!
tl;dr
Overwrite the lsb of stdin’s _IO_buf_base so that in the next getchar we can overwrite stdin’s _IO_FILE structure itself.
Control stdin’s _IO_buf_base and _IO_buf_end to gain arbitrary write.
Overwrite _IO_list_all and use house of cat exploit chain to gain RCE.
Challenge environment: Glibc 2.39 (which is the latest version!), protection are all on except canary.
I only show the important parts of the challenge here.
Free libc leak and an arbitrary write NULL byte to any libc address.
Vulnerability analysis
Trivial, we can write a NULL byte in libc. But how to exploit?
Exploitation Plan
My first thought was of course: what the…freak? Can we overwrite some function pointer? It turns out that in libc 2.39, there aren’t many “raw” function pointers.
Then after some googling I found this awesome article House of red.
Yeah basically it’s the same idea! I actually traced source code during the competition.
If you dig into source code of getchar (libio/getchar.c):
We can see that _IO_getc_unlocked is important here. And after some more cross reference, we found that it’s actually calling this macro:
Condition (_fp)->_IO_read_ptr >= (_fp)->_IO_read_end is true here so it’ll call __uflow(stdin) in libio/genops.c:
And _IO_UFLOW is just a macro jumping to the __uflow function of vtable of an _IO_FILE structure. (libio/libioP.h)
After some dynamic analysis we can see that it actually calls function _IO_file_underflow in gdb (or _IO_new_file_underflow in source code libio/fileops.c, they are the same).
The core part of _IO_new_file_underflow is:
So basically if we can control stdin’s _IO_buf_base to somewhere with lower address, we can read more than 1 char! (We didn’t change _IO_buf_end)
We found that if we write the lsb of _IO_buf_base to 0 (which is 0x7ffff7fae963 to 0x7ffff7fae900 in this case), it’ll point to the address of _IO_write_base!
So we can overwrite the _IO_FILE structure itself! The next getchar will actually read from 0x7ffff7fae900 to 0x7ffff7fae964. Then we can control _IO_buf_base and _IO_buf_end to gain arbitrary write.
The rest is just finding how to code execution in latest glibc 2.39. Fortunately house of cat exploit chain with exit triggering FSOP still works! See more details in the exploit script.
Exploit Script
Epilogue
Though this challenge seems unsolvable at first, it’s actually very interesting! Thanks uz56764 for this awesome challenge.