Extracting initramfs, we’ll find a module chall.ko, which will create a device /dev/brainfuck64, that can be accessed via ioctl.
We can use that, to initialize a context object, write brainfuck code to it and execute it in kernel space (and afterwards read the output generated).
Reversing the module…
So, there are three different request types we can send
0xAC1DC0D3 will initialize the brainfuck context and let us define the size of the output buffer.
0xD00DC0D3 will copy the output buffer of the brainfuck context to a user buffer, enabling us to read the output.
0xBAADC0D3 will copy a brainfuck program (max 500 bytes) from the user buffer and executes it.
I totally overlooked the ^ operator for writing a complete qword while doing this challenge, which would have made it probably much easier to write the needed values into kernel space compared to my approach, but well…
The main problem here is, that we can define the size of the output buffer, but no boundary checks are done in the interpreter, so we can walk around freely in kernel space and read and change values.
My attack plan thus was
Allocate a program with output buffer of size 32
Since the buffer isn’t initialized, we can directly read a heap address from its output buffer
Send a brainfuck program which will
walk to the FD of the next free kernel chunk
overwrite the FD with an arbitrary address of our choice
Allocate another program
since the allocation for context will request a 32 byte chunk our FD will be put into the bin list
allocating the output buffer for the context will use our corrupted FD and serve us an arbitrary chunk for it
Send another brainfuck program to write to the arbitrary chunk
Since the kernel has no kaslr enabled, we can use hardcoded addresses and don’t really need leaks. Thus the easiest approach to get the kernel symbols, is to extract initramfs, change the init script, so we will login as root and just grep kallsyms.
Setting up a base script for executing the different ioctl requests…
Leaking a heap address from the kernel (not really needed, since we don’t have kaslr, but might help when switching to remote, where addresses might be a bit off).
Since overwriting modprobe_path went pretty well in the last kernel challenge, I decided to go for it again
Preparing it at the start of our exploit
So, we’d want to overwrite the FD of the next free chunk with a pointer to modprobe_path (I pointed it 0x10 above to not interfere with FD/BK). Could have probably used the ^ operator for that, but didn’t reverse the module that far, when doing the challenge, because I already had another option in mind.
With + and - we can increase and decrease the values for every byte, and since we know the original and the destination value, I just generated a brainfuck program, which will walk over the bytes and increase/decrease them, depending how they are compared to the destination byte.
The generated programs could easily get bigger than 500 bytes, but that’s not really a problem, since the context will stay the same between mutliple executions, so we can just split it up into multiple buffers afterwards.
After this was executed, the FD will now point above modprobe_path
So, when we now allocate another brainfuck context, it will allocate 32 bytes for the context object, putting our fake FD in the bin list and then allocate another 32 byte chunk for the output buffer (which will then be served with our modprobe chunk).
Now, we just need another brainfuck program to overwrite the string with the name of our exploit script, which will copy the flag to the user directory and make it readable.
Everything’s prepared, and we just have to trigger modprobe_path
Since the kernel will not recognize the file format of dummy, it will try to execute the file at modprobe_path (which is now our exploit script, copying the flag into the user directory).