Baby_guess was a kernel module, which provided a simple xor encryption and “guessing” function in the kernel.
The module registered a socket, with which we could communicate with it. The socket had two custom functions, which can be triggered via ioctl and setsockopts.
Analysis
Decompilation from ida and ghidra looked quite awkward and used a lot of local variables, which made it a bit hard to spot the bug.
So, here’s some stripped pseudo code to make it more obvious.
ioctl
With the comments, it should be quite easy to see.
Leaking via compare
Option 0x1337, which is used to compare our input buffer with the encrypted device buffer, verifies that the size of our input buffer is not bigger than 0x100 bytes. But it uses the validated size only for copying our buffer to kernel memory. When doing memcmp to compare our buffer with the device buffer it uses the original size from our request.
By this, we will be able to leak data behind k_buffer by setting size to 0x101 and then repeat the requests over and over, while forging an encrypted device buffer, which contains a “compare” byte at offset 0x101. We can then check, if the compare was successful. This way we can guess/bruteforce the kernel stack guard, module address and kernel address. Forging the encrypted buffer will be a bit tricky, since we normally can only put 0x100 bytes into it, but let’s get back to this later.
Kernel stack overflow
Option 0x1338 has a similar bug, though this time it’s “the other way round”. copy_from_user will use the original unvalidated size, while memcmp uses the validated one.
This enables us to do an easy stack overflow. We just need to get some leaks first, so we know the kernel stack guard and have some useful gadgets to create a rop chain.
Set device buffer size
This function will set the size for the encryption method. Though it will check, that the size must not be bigger than 0x100. But since there are no locks, and it stores the size in dev_info.size at the beginning, then does the check, print a kernel message and sets it back, this will be easily raceable!
setsockopts
The module also had a custom setsockopts function, with which we can encrypt our user input into the device buffer.
With optname as 0xdeadbeef we can trigger the encryption function, which will use the size from dev_info.size to copy our buffer into the device buffer.
As mentioned above, setting dev_info.size can be race. We can just start a thread, which will call set_dev_info_size(0x1000) over and over again and the chance that encrypt_buffer will use our invalid dev_info.size is very high.
This, combined with option 0x1337 from ioctl can then be used to retrieve kernel leaks from the stack.
So, let’s start with the exploit itself.
Leak magic key
For using the compare method to leak stack values, we need to exactly control what’s stored in the encrypted device buffer. For this, we first need to know the magic key, which is initialized with 256 random bytes at module initialization.
But we can get this easily via the compare option 0x1338 by just comparing the first byte for range 0x0 - 0x255 and then increase the size, until we have “guessed” the complete key.
Leak stack guard and kernel addresses
Knowing the magic_key, we can now exactly control what will be stored in the device buffer, so we can now start to forge buffers to guess/bruteforce the stack guard and kernel addresses.
It’s pretty similar to guessing the magic key, only that we have to overcome the size restriction of 0x100 this time. As mentioned above we can do this by starting a thread, that will set the size of the device buffer to 0x200 over and over again.
When the race is won, the encryption function in setsockopts will copy 0x200 bytes from our buffer into the device buffer (and xor the first 0x100 bytes of it). We can check, if the race was successful, since setsockopt will return 0x200 if so (0x100 if we missed it).
We’ll now just increment the byte at offset 0x100 and call the compare method of the module with size 0x101. As soon as it succeeds, we know, that we guessed a correct byte, increase the offset and continue.
rop to overwrite modprobe_path
With all the needed leaks, we can now get back to option 0x1338 for overflowing kernel stack and trigger a ropchain.
After doing a quick test, to see that modprobe_path exploit would work in this challenge, I just did a simple ropchain for overwriting modprobe_path (and let it crash afterwards…)
Obviously the ropchain will crash after it copied /tmp/c to modprobe_path, but it will just kill our exploit process and get us back to the prompt (while modprobe_path will still be overwritten).
From here, we can then trigger modprobe via dummy to copy the flag into tmp and make it readable.