Binary's a little wacky, let's look into it.
There's no libc used whatsoever, everything is syscalls.
When the binary starts, it calls the _start function. This function is simple, it uses sys_write to print out "Can you pwn me?", then calls fn1, then jumps to the function nope.
Let's disassemble fn1. sets rax to the xor of 0xbeef and 0xdead, and pushes this onto the stack.
It then moves rbp to be rsp-0x400, and reads 0x800 bytes from stdin at rbp. Afterwards, it pops rax off of the stack, XORs it with 0xbeef, and checks if the value is 0xdead.
If not, it jumps to the nope function. Otherwise, it rets, popping rbp off the stack before hand
This creates a simple buffer overflow. At the time of our input, the stack looks like this
rbp 0x400 bytes rsp -> value to be popped into RAX value to be popped into RBP return address previous stack frame
we can overflow with 0x400 bytes of padding, and then the value of 0xdead ^ 0xbeef.
The binary has no protections whatsoever, including lack of NX.
There is a RWX segment within the binary. Because fn1 uses rbp to mark where it starts its input, and we get a pop into rbp, we can set rbp to be the address of the RWX segment, and then ret into the instruction in fn1 that starts the input. Then, we enter shellcode, and ret to that address. Script below. (I wrote custom shellcode that uses the fact that /bin/sh is written just before it)
The binary is stripped of symbols, and even radare2 cannot resolve main, so this is a little difficult.
I stepped through in gdb in __libc_start_main until the call rax instruction.
At this point, rax was 0x4011d6, indicating to us that this was the address of main. I used x/100i to view all the instructions at this point, and found a small little buffer overflow.
We can use pattern.py to find the offset till the return address is 497.
Looking a little past main, there appears to be another function. It calls open, then read, then puts.
If we do some calculation on RIP and use x/s, we find that it calls open on flag.txt! This must be the flag function.
Essentially, we have a simple ret2win exploit. Overwrite ret address with the flag function.
Conveyor belt.
Running the binary, we have two options - add a part to the conveyor belt, and review the belt. When we review the belt, it goes through all the parts we have one by one, printing them, asking us if they are safe. If we say they aren't(that is, not responding with 'Y' or 'y') then we can edit the part.
Let's chuck it into ghidra and see what more we can get.
First of all, we see that the add_part function is like so - It seems to take a parameter being the address of the previous part. allocate 0x80 bytes of data. Read 0x80 bytes of data from stdin into this place. If the string contains "sh", say the part isn't safe, free the allocated data, return the parameter we got(essentially dont make any chunk and pretend nothing ever happened)
If not, then edit datapointer+0x78 to be the address of the previous part. Essentially, the parts are in the structure
Forming a list. The problem is, whenever it asks us to edit a part, it reads 0x80 bytes when only 0x78 are the data segment - giving us an overwrite of the previous_part field. More on this later.
Let's look at the safety check function. It starts on the last part, printing it, asking is if it's safe, and allowing us to edit it if it isn't. Here's our main vuln. An extra 8 bytes are read, letting us overwrite the previous_part field. The function then grabs the previous part field, visits that, prints it, asks if it's safe, etc. etc. until it hits a previous_part field of 0.
What can we do with this? We can create a single part, then activate the safety check. We can edit the previous part field in-place, and then it'll go wherever we want for the next part! This creates two things
Arbitrary read, as it'll print out the part.
Arbitrary write, as we can say the part isn't safe, and then edit it.
Therefore...
We can set the previous part field to puts@GOT, allowing us to read a libc address. Once we read this, we'll edit puts@GOT too! Let's edit it with the address of system.
Now what? It'll look 0x78 bytes later for the address of the next part, then continue. It'll grab this address, call puts("Next part:") and then calls puts on the next part.
Therefore, if we pretend the next part is the address of /bin/sh, it'll call system("/bin/sh") for us. So, our exploit:
Create new part
Safety check. Say the part isn't safe. Edit it with 0x78 bytes of junk + address of puts@GOT
It'll print the value of puts@got, which is puts@LIBC. Read this value, and subtract appropriate offset to get the libc base. Say part isn't safe. Send system address + 0x70 bytes of junk + /bin/sh address such that whenever it calls puts it'll actually call system, and the next thing it will call puts on is /bin/sh
Script below.