Hitcon 2020 - Tenet Writeup

Posted on Jan 23, 2021

2020 is the year that I started playing ctf, this is one of the first challenge that I solved with my teammates in an hard CTF. I had fun developing the idea for the exploit for this challenge, here is the writeup:

Challenge description:

    You have to start looking at the world in a new way.

    nc 52.192.42.215 9427

    Author: david942j

The Goal of the Challenge

The challenge is based on the server.rb ruby script which takes a shellcode as input and wraps it into an ELF executable file that is then run by the time_machine debugger. The goal of the challenge was to initially reverse this debugger and learn what it exactly does. Later on we found out that in order to retrieve the flag the shellcode needed to be executable in two ways: the normal and the reversed one. The peculiarity was that the code would run reversed following the same flow it got in the ‘straight’ way.

The generated ELF

First of all we wanted to test out what kind of wrapping was in place within our shellcode, we found out that seccomp was enabled preventing us from executing every possible syscall but read, write, exit, and sigreturn, as mentioned in the man:

    The only system calls that the calling thread is permitted to make are read(2), write(2), _exit(2) (but not exit_group(2)), and sigreturn(2)

We also found that it initialized a both readable and writable mapping at address 0x2170000 to 0x2171000. Our shellcode started from address 0xdead0080 and needed to be less than 2KB.

The time_machine debugger

We started reversing this binary, we soon realized that it was a debugger executing whatever it’s passed through as a first argument (Our generated ELF). Our shellcode was executed step by step saving in a list every executed instruction address. Once it got to a SYSCALL instruction it checked whether the EAX register was set to 0x3C (sys_exit) and, if so, it started executing every instruction stored in the list in reversed order. The syscall (or sysenter) instructions were completely ignored and even if we got one we couldn’t execute almost anything because of the seccomp. The debugger, once started, also sets an 8byte cookie to 0x2170000, checks if it’s cleared once our shellcode is executed and rechecks if it’s there once it got executed the other way back.

The shellcode

So the challenge was: write down an assembly that could erase the cookie when executed in the normal way and could restore it when executed with the same flow but reversed. Our first tought was to store it into a register and xor it to memory in a way that looked like this:

mov rcx, 0x2170000
mov rdx, 0x0     ; Clean rdx
xor rdx, [rcx]   ; Set/erase rdx
xor [rcx], rdx   ; Erase/set memory
mov rcx, 0x2170000
mov eax, 0x3c
syscall

Of course, it wasn’t that simple, the registers (both the CPU and FP ones) got erased right before the reversed execution, we needed somewhere else to store the cookie. The stack? No, we couldn’t have a stack address in the reversed execution. The rest of the 0x2170000 mapping? No, this debugger checked also that the entire page was NULL(ed). But then we realized that we actually had a memory: the executed instruction order! So the final shellcode looked like this:

mov rdx, 0x2170000 ; initialize rdx to cookie address
mov rsp, 0x2170500 ; improvised stack to store ret values

mov rcx, rdx
add rcx, 0
call confrontoLow ; confronto == compare
add rcx, 0
mov rcx, rdx
add rcx, 0
call confrontoHigh
add rcx, 0

mov rcx, rdx

add rcx, 1
call confrontoLow
add rcx, 1
mov rcx, rdx
add rcx, 1
call confrontoHigh
add rcx, 1

mov rcx, rdx
    .
    . ; Replicated code for every nibble possible value
    .
add rcx, 15 ; While coding at 4am, we didn't realized that up to 7 was enough, we copied more than needed(16 bytes)
call confrontoLow
add rcx, 15
mov rcx, rdx
add rcx, 15
call confrontoHigh
add rcx, 15


push 0 ; clean improvised stack
; reset a bunch of things just to be sure
mov rsp, 0x2170500
mov rdx, 0x2170000
mov rcx, 0x2170500
xor rdx,rdx
xor rbx,rbx
xor rcx,rcx
xor r14,r14
; Or should I comment here? Whatever...
mov rax, 0x3c
syscall


; Check the upper nibble and jump to the calculated function offset
confrontoHigh:
xor rbx,rbx
mov bl, byte ptr[rcx]
shr bl, 4
mov r14, 0xdead035b ; absolute for nibble00High
add r14, rbx        ; multiply by 8 for lazy people
add r14, rbx
add r14, rbx
add r14, rbx
add r14, rbx
add r14, rbx
add r14, rbx
add r14, rbx
jmp r14


; Check the lower nibble and jump to the calculated function offset
confrontoLow:
xor rbx,rbx
mov bl, byte ptr[rcx]
and bl, 0xf
mov r14, 0xdead030b ; absolute for nibble00Low
add r14, rbx        ; multiply by 5 for lazy people
add r14, rbx
add r14, rbx
add r14, rbx
add r14, rbx
jmp r14


nibble00Low:
xor qword ptr [rcx],0x00
ret

nibble01:
xor qword ptr [rcx],0x01
ret
    .
    .
    .
nibble0f:
xor qword ptr [rcx],0x0f
ret


nibble00High:
xor qword ptr [rcx],0x00
ret
nop  ; Needed a 3 byte offset because xor with immediate ≤0x7f is smaller than the other half byte (Wtf x64 assembly?)
nop
nop

nibble10:
xor qword ptr [rcx],0x10
ret
nop
nop
nop
    .
    .
    .
nibble70:
xor qword ptr [rcx],0x70
ret
nop
nop
nop

nibble80:
xor qword ptr [rcx],0x80
ret
    .
    .
    .
nibblef0:
xor qword ptr [rcx],0xf0
ret

And the flow does the rest… When it goes back it initializes a register at the right byte address and the flow ‘remembers’ which value every nibble had by executing the code segment containing the xor with that value.