GITS 2015 CTF 'giggles' writeup
giggles was an exploitation challenge worth 300 points at the “Ghost in the Shellcode” CTF 2015.
We are provided with remote access to some kind of bytecode-based virtual machine and an example client script written in Python to access it. We also get the source code of the server binary, which saves us some tedious reverse engineering.
After establishing a TCP connection with the server, the example client script glues together some byte code:
Looking at the server code, the VM supports 7 different instructions:
OP_ADD(dest, src)
adds the value of registersrc
to that of registerdest
OP_MOV(dest, src)
sets the value of registerdest
to that of registersrc
OP_OUT(reg)
sends the value of registerreg
over the network to the clientOP_BEQ/OP_GT(jmp_target, reg1, reg2)
are conditional jumps to instructionjmp_target
that are taken when the value ofreg1
is equal to / greater than that ofreg2
, respectivelyOP_BR(jmp_target)
is a conditional jump to instruction 0.OP_EXIT()
leaves the function
This is the representation of an instruction in the server:
The example script then proceeds to send the byte code to the server
and register it as a function using the ADDFUNC
command:
We note that 2 is specified as the number of function parameters. This is however wrong, actually 4 arguments are provided when the function is called later. In fact the value provided here is totally ignored by the server.
The corresponding handler on the server side adds the function to a global array of function structures:
Initially the instructions are just copied verbatim from the client, but the
function is marked as unverified, so it cannot be called yet. To mark it as
verified, the example client explicity calls the VERIFY
command with the
index of the function as an argument:
The server then checks the bytecode in an attempt to prevent out-of-bounds operands, but fails to do so (more on this later). It then marks the function as verified and we can call it:
Great, so know we have a basic idea of what the protocol is and what the VM can do. Let’s exploit it.
The bug
As mentioned, the verifyBytecode
function is buggy:
There is an off-by-one error: If the operand is equal to n_ops
, the jump is
allowed although the target instruction is out of range. We can exploit this by
defining a function with MAX_OPS
instructions and have it jump to MAX_OPS
.
The program counter will then end up in the header of the following function.
We just have to construct a function header that is also a valid, useful
instruction.
The exploit
Our plan now is to construct a function that is also a valid jump instruction. The memory layouts of the two structures compare as follows:
interpreted as function:
+--------------+--------------+--------------+--------------+----------+------
| num_ops (16) | num_args(16) | verified (8) | op_code (16) | op1 (64) | ...
+--------------+--------------+--------------+--------------+----------+------
interpreted as jump instruction:
+--------------+-------------------------------------------------+------
| OP_BR (16) | jump target (64) | ...
+--------------+-------------------------------------------------+------
We can easily set num_ops = OP_BR
, which happens to have the value 1. The jump
target is pretty much arbitrary, since num_args
is ignored and we can control it.
We only have to make sure that there is a null byte in the correct position,
because verified = 0
is set by the server.
Here is the code to create our trampoline function and function/instruction polyglot:
Now the tedious part is figuring out where to jump. We decided to just jump
into an unverified function that we define later, so we get to execute
arbitrary bytecode without bounds checking. The problem is that an instruction
structure is 26 bytes in size and a function structure 5 + 30 · 26 = 785
bytes, so it is not obvious how to align the instructions properly. We can use
as many functions as we want as padding (as soon as there are less than
MAX_FUNCS
, which is set generously to 64). The offset the first instruction of
function X is 785 · X + 5. The offset of the Yth instruction of the first
function is 5 + 26 · Y. We need to equate the two:
785 · X + 5 = 5 + 26 · Y
The solution with the smallest positive X is X = 26 and Y = 785. So we have to add 24 functions as padding and we need to jump to instruction 785. This is the setup code that makes a connection and adds all the boilerplate functions:
The function we add after this is the one that is going to get called eventually
if we call the first function. Now we can run arbitrary bytecode! E.g., here
is a function that reads the w
th register value, without bounds checking:
It actually uses two OP_OUT
instructions to read a 64-bit value. This will
basically return *(uint64_t*)(registers + w)
. To verify that this works, we can
run the server in a debugger. We had to NOP out a call to alarm
to avoid being
interrupted while debugging. This shows a call to read_relative(1337)
:
$ gdb -p 4077 ./giggles
...
(gdb) set follow-fork-mode child
(gdb) break giggles.c:121
Breakpoint 1 at 0x7f19ed08597e: file giggles.c, line 121.
(gdb) c
Continuing.
[New process 4230]
[Switching to process 4230]
Breakpoint 1, executeFunction (f=0x7f19ed287180 <funcs>, ...
warning: Source file is more recent than executable.
121 switch (curr_op->opcode)
(gdb) p *curr_op
$1 = {opcode = 1, operand1 = 30, operand2 = 0, operand3 = 0}
(gdb) c
Continuing.
Breakpoint 1, executeFunction (f=0x7f19ed287180 <funcs>, ...
121 switch (curr_op->opcode)
(gdb) p *curr_op
$2 = {opcode = 1, operand1 = 785, operand2 = 280267669825,
operand3 = 0}
(gdb) c
Continuing.
Breakpoint 1, executeFunction (f=0x7f19ed287180 <funcs>, ...
121 switch (curr_op->opcode)
(gdb) p *curr_op
$3 = {opcode = 5, operand1 = 1338, operand2 = 0, operand3 = 0}
(gdb) c
Continuing.
Breakpoint 1, executeFunction (f=0x7f19ed287180 <funcs>, ...
121 switch (curr_op->opcode)
(gdb) p *curr_op
$4 = {opcode = 5, operand1 = 1337, operand2 = 0, operand3 = 0}
(gdb)
Clearly the operands are out of bounds. At this point we have an arbitrary read
and write (via OP_MOV
) relative to the registers
array. Turning this into
an absolute read/write is easy too, if we can leak the address of the registers
array. Conveniently, it is located on the stack and we can determine its exact
location from the value of the saved frame pointer:
(gdb) x/20gx registers
0x7fff59040d90: 0x0000000000000000 0x0000000000000000
0x7fff59040da0: 0x0000000000000000 0x0000000000000000
0x7fff59040db0: 0x0000000000000000 0x00007f19ed0855d8
0x7fff59040dc0: 0x00007fff59002030 0x0000000000000004
0x7fff59040dd0: 0x00007f19ee744574 0x65b948277f4f3b00
0x7fff59040de0: 0x0000000000000000 0x0000000000000000
0x7fff59040df0: 0x00007fff59040e50 <-- 0x00007f19ed085efd
0x7fff59040e00: 0x00007f19ed08611c 0x0000000400000000
0x7fff59040e10: 0x00000000000402ed 0x00007f19ee744570
0x7fff59040e20: 0x00007f19ee7445c0 0x00007f19ee744570
(gdb) p 0x00007fff59040e50 - 0x7fff59040d90
$6 = 192
So let’s compute the address of the register
array and, while we’re at it, let’s
also read the saved return address to compute the base address of our binary:
Now we can calculate the offset of arbitrary addresses, relative to registers
:
At this point we thank the authors of the challenge for this nice gimmick:
This buffer is where we want to store our shellcode. To do that, we read
the value of the pointer JIT
, which is located in the .bss
segment of the binary.
We then use a sequence of OP_MOV
s to overwrite the buffer and the return address:
The way function arguments work in our little VM is that they are copied verbatim
into the registers before the function executes. I.e. argument 0 goes into register 0
etc. It is slightly annoying that we have only 9 arguments that we can pass, so
our stage 1 shellcode can only have 28 bytes. We use a read
syscall to load stage 2
right after stage1:
And we choose to also reuse the socket file descriptor for our final shell:
The final exploit in action:
$ python2 exploit.py
[*] register base = 0x7fffb7b91900
[*] exe base = 0x7fef16437000
[*] jit buffer = 0x7fef16431000
[*] Enjoy your shell :)
ls
client.py
giggles
giggles.c
key
cat key
I can't think of anything creative to put in here right now
And yes, it used to give you the flag during the contest :)