This is a write-up for challenge Oil system in Xmas CTF 2020.
We are provided with a binary called oil and a example custom script example for it.
Pwn problem. Get the shell!
This program works in two ways, depending on the number of arguments it was executed with. Without any command-line arguments, this program works in a interactive manner, and with a name of a script provided in the command-line argument, it executes that script.
The script is just an sequence of integers separated by a space or a newline. As the name oil stands for One-Instruction Language, those integers are read by a group of 3 and executed in an only way.
void __cdecl doEmul(int *arr)
{
signed int i; // [rsp+8h] [rbp-10h]
int i0; // [rsp+Ch] [rbp-Ch]
int i1; // [rsp+10h] [rbp-8h]
int i2; // [rsp+14h] [rbp-4h]
i = 0;
while ( i >= 0 )
{
i0 = arr[i];
i1 = arr[i + 1LL];
i2 = arr[i + 2LL];
if ( !i && !i0 )
break;
if ( i0 >= 0 && i1 >= 0 )
{
arr[i1] -= arr[i0];
if ( arr[i1] <= 0 )
i = i2;
else
i += 3;
}
else
{
i = -1; // oh no!
}
}
}As you can see, it gets the first number i0 of the group, and the second number i1 of the group, and subtracts arr[i0] from arr[i1]. Then, if the results are not positive, it jumps to the third number. If not, it just fetches the next number. However, if i1 or i0 becomes negative, it will halt.
In the interactive menu, you can write your code, run your code, and inspect it. You can set the program name to any string that starts with a arbitrary letter, and this is important for the exploit later on. In the run code menu, the program will write your code of your name to the filesystem. Then, it will run that program by invoking oil itself again with your program. It uses system() for it. The inspection menu runs cat on your script, so you can see your code.
In the non-interactive mode, the function my teammate named emulator() loads your script on the int[128] array, and executes it by calling doEmul() above.
This binary has most of the protections.
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
There is a obvious OoB write in the function doEmul(). This function does check the value i0 and i1 against >=0, but it does not check if the value exceeds 127. As this array is in the stack frame of emulator(), you can access any stack variables including the return address.
However, we don't know any of the addresses, so we can't just perform ROP to get a shell. Thankfully, this binary is full of system() calls. We can exploit one of them to get a shell easily.
void *view_cached_code()
{
char *basename; // rax
size_t baseLen; // rbx
char *basename2; // rax
memset(run_code_, 0, 0x20uLL);
strncpy(run_code_, "cat cache/", 0xDuLL);
basename = xpg_basename(programName); // <= jump here
baseLen = strlen(basename);
basename2 = xpg_basename(programName);
strncat(run_code_, basename2, baseLen);
system(run_code_);
return memset(run_code_, 0, 0x20uLL);
}The function that my teammate named view_cahced_code() is called in the code inspection menu in the interactive mode. This function executes cat cache/${programName} to show your code. We can jump in the middle of this function, to the part basename=xpg_basename_(programName). As the .bss variable run_code_ is empty when the program is executing in the non-interactive mode, this will cause strncat to just write programName on it. Then, it calls system(). Therefore, if we name our code sh or bash, it will just grant a shell. As you can find out in the cat cache/, we don't even need to name it /bin/sh as the PATH environment variables are alive.
Then its just a problem of writing a code that allows us to jump from emulator() to here. As the offsets of the functions are the same, it can be easily done without getting any leaks. The offset from the array and the return address is 0x298 bytes. The offset from the original return value and the target location above is 0x4c6 bytes. Therefore, doing arr[166] -= -1222 should be more than enough.
So our payload is 2 166 -1222, and named sh.
Executing this file against oil will get a shell, and doing the same thing interactively will get the shell, too. Please refer to the attached solver.py for the actual exploit code.