So, from the name and desc, as well as the fact you're provided with the libc binary, we know this is a ret2libc right off the bat. Inspecting the source code, we can see the read function is called into a 16-byte array, reading 0x100 bytes from stdin. This opens up a lot of room for overflow.
The array is stored at rbp-0x10, leaving 24 bytes of padding until our ROP chain.
We can use ret2plt to leak a libc address by calling the libc function puts via the PLT on the puts entry in the GOT, effectively printing a libc address back to ourselves. This works because there's no PIE in the binary, so the GOT and the PLT are stable.
So, we can first send padding + poprdi + puts@got + puts@plt + main so that the program calls puts on puts@got, sending us a libc address, and then rets back into main so we can get another input. On this second input, now that we know exactly where the libc is and have defeated ASLR, we send padding + poprdi + /bin/sh address + ret gadget + system address. This forces the program to call system("/bin/sh"), popping a shell for us. The ret gadget is needed to fix stack alignment.
Read source, there's a code var.
It's set to 0. At the end, if it's not 0, a shell is popped.
Pretty simple chall, just spam chars and a shell pops, cat flag.txt
Same thing, except code must be 0xcafebabe.
Let's disassemble main, we'll find the difference between our input(rbp-0x20) and the var(rbp-0x8) is 24 bytes, so send 24 bytes + p64(0xcafebabe)
ret2win exploit. There's a function called binFunction.
Our input is at rbp-0x10, so 0x10 + 8 bytes until return address.
Overwrite return address with address of binFunction, which pops a shell.
From the desc we know it's a format string. There's no symbol for main, so i opened it up in radare2.
It opens mallocs a heap address, stores it on the stack, opens flag.txt
, and reads it to that heap address. We can use the format specifier %s to read the flag by referencing this heap address.
I didn't bother actually calculating the offset and just bruteforced it.
First things first: let's run checksec. There's Partial RELRO, and no PIE. This opens a bundle of attacks, but for now let's not comment on that.
Running the program, we get one input. Spamming lots of chars, we get a stack smashing error. That means a canary.
Opening it up in radare2, we can inspect the "hidden" main function. We see it reads 0x120 bytes into rbp-0x110. Buffer overflow? Kinda, but I didn't use it except to trigger a canary mismatch.
Most importantly, it calls printf on our input directly, opening a range of format string attacks. The first thing that pops into mind is a format string overwrite, but what to overwrite? The only libc function called after our input is printf-ed is __stack_chk_fail, but that's only called if there's a canary mismatch.
Our goal? Overwrite __stack_chk_fail@GOT with the address of main, then trigger a canary mismatch. Everytime the canary mismatches, it'll try to call stack chk fail, but instead it'll just call main again. This gives us infinite calls of main, so we can do whatever we want with format strings including writing and reading before we deliver the final exploit.
I cut my exploit in 4 stages.
Stage 1: Overwrite __stack_chk_fail@got with the address of main. Trigger canary mismatch, main will call again
Stage 2: Leak __libc_start_main_ret using %77%lp. Make sure to trigger canary mismatch in order to call main again
Stage 3: Calculate libc base. Overwrite printf@GOT with system@libc. Trigger canary mismatch for the final time.
Stage 4: Enter /bin/sh. The program will attempt to call printf("/bin/sh"), actually calling system("/bin/sh"), popping a shell.
Note: remotely, for some reason, the shell is really unstable? After one command it breaks and disconnects. Still some form of temporary shell though, enough to cat flag.txt.
There's a lot of unnecessary bloat that I'll mostly ignore.
Cracking open the binary in ghidra, we see that the first input we get is actually a scanf, scanning a decimal integer into a variable. It then checks this integer against 1. If the integer isn't 1, it pops a shell - zsh. Nothing happens remotely, likely they do not have zsh. So that's not very useful.
What happens if the integer is 1? It continues with the rest of the execution of the program. The program opens up a never-ending prompt in which it asks for an input. If the input is the same as notflag{a_cloud_is_just_someone_elses_computer}\n when strcmp-ed,the program tells us we did it and rets. This will be useful later on to deliver our final exploit.
In the prompt, 0x200 bytes are read into rbp-0x90, creating a clear buffer overflow vulnerability.
Sadly, however, every protection is on. Canary, RELRO, PIE and most likely ASLR. We're going to need to leak somehow.. but how?
The read function does not null terminate. That means our input will not be null terminated as a string unless we enter a null byte. Not only this, but the program calls printf("%s??", input), printing the input back to us. Printf will only know the end of a string once it hits a null byte, meaning we can leak values off of the stack!
For example, say this was the stack
00 00 00 00 00 00 00 00 00 \
we could write like so
41 41 41 41 41 41 41 41 0a \
now, when it prints, it'll keep printing, leaking the canary and saved RBP .fini address.
NOTE: Canaries start with null bytes! We will have to overflow one byte of the canary so that we can read the rest. It doesn't matter that we overflow the canary until we make the program ret, but by then we will know what the full canary is and be able to replace it.
We can use this again to leak the saved ret address, which will be __libc_start_main_ret.
Once all these values are leaked, we send the finished exploit. notflag{a_cloud_is_just_someone_elses_computer}\n\x00 + padding + canary + more padding + poprdi + /bin/sh address + retgadget + systemaddress
NOTE: We can leak the binary base through the .fini address. We don't strictly need it here, as libc has ROP gadgets, but it's useful.