Midnight Sun CTF finals 2020 - autorev writeup

This challenge involved ‘auto’ reverse engineering 10 programs within a time limit.

Connecting to the challenge via netcat presented the user with the goal:

Please reverse 10 programs before I give you the flag
Here is a base64-encoded program for you:
f0VMR..........
You have 10 seconds provide something to pass in to the program as standard input:

Too slow!
Better luck next time

After taking the base64-encode program, decoding it and decompiling it with Ghidra, we find the required input is a 64 bit value in the testfunc() program - Ghidra simplifies this program, displaying the required parameter.

This is the ghidra decompiled code, showing the main function calling ‘testfunc’ which checks the input against a 64 bit value.

undefined8 main(void){
  int iVar1;
  undefined8 uVar2;
  
  setvbuf(stdout,(char *)0x0,2,0);
  setvbuf(stdin,(char *)0x0,2,0);
  uVar2 = get_input();
  iVar1 = testfunc(uVar2);
  if (iVar1 == 1) {
    puts("You did it!");
  }
  else {
    puts("You failed!");
  }
  return 0;
}

...

ulong testfunc(long param_1){
  return (ulong)(param_1 == -0x67b869127d9430b0);
}

The assembly reveals the input value is taken, placed in RDI, then XORd and ADDd with a number of 64 bit constants before being compared with a final constant (final_c). Running this challenge a number of times reveals the constants change each time along with the order and amount of XORs and ADDs, and sometimes there is a MOV instruction for RDI to another register before operations are performed on it.

undefined testfunc()
  AL:1           <RETURN>
testfunc

ENDBR64
MOV        RAX,0x75af4a9b7ff5a731
ADD        RAX,RDI                 # RDI contains input val
MOV        RDI,-0x21459817860ff2ef
XOR        RAX,RDI
MOV        RDI,-0x7cb8ac47d2e3ea58
ADD        RAX,RDI
MOV        RDI,-0x4a273b94adf2a5a

...

XOR        RAX,RDX
MOV        RDX,0x23a2f8516a4db1c2
ADD        RAX,RDX
MOV        RDX,0x4abcc94853830566
XOR        RAX,RDX
MOV        RDX,0x23292d5bc7a6048b  # final_c
CMP        RAX,RDX
SETZ       AL
MOVZX      EAX,AL
RET

This leaves an algorithm along the lines of:

final_c = (((((((x+a)^b)+c)^d)+e)^f)+….)

We want to find x and are given all the other constants, to reverse we can do:

x = ((((((final_c …..)^f)-e)^d)-c)^b-a)

To do this we have to decode and parse the base64 encoded elf, finding the constants and operations, our solution is pretty basic but does the job with the help of pwntools. We first jump to the function ‘testfunc’, then find the constants by checking the opcodes and then jumping from one constant to the next adding to a list, this list we then reverse and invert the operations to find x.

# Solution
from pwn import *
import numpy as np

# algo in program is:
# n = (((((((x+a)^b)+c)^d)+e)^f)+....)
# we're given program that has constants a,b,c,....n, goal is to find x

# alternate between xor and minus to reverse algo
for i in range(len(const_list)):
    c = const_list[i][0]
    op = const_list[i][1]
    if op == 'xor':
        x = x ^ c
    else:
        x = x - c
return x

# find the constants and operations in the elf file
def parseElf(elfPath):
    e = elf.elf.ELF(elfPath)
    baseaddr = e.functions['testfunc'].address # test func contains algo
    addr = baseaddr
    addr += 4
    constantList = []
    inst = e.read(addr,3)
    if ( (inst[0] == 72) and (inst[1] == 49 or inst[1] == 1 or inst[1] == 137) and (inst[2] == 208 or inst[2] == 248)): # check opcodes
        addr += 3 # skip if mov instr

    while True:
        addr += 2
        constantI = int.from_bytes(e.read(addr,8), "little", signed=True)
        addr += 8

        inst = e.read(addr,3)
        if (inst[0] == 72 and inst[1] == 49 and (inst[2] == 208 or inst[2] == 248) ): #xor
            op = 'xor'

        elif (inst[0] == 72 and inst[1] == 1 and (inst[2] == 248 or inst[2] == 208)): #add
            op = 'add'

        elif (inst[0] == 72 and inst[1] == 137 and inst[2] == 248): #mov
            op = 'mov'
        else:
            constantList.append((constantI,'end'))
            break
        addr += 3 # skip ADD/XOR

        constantList.append((constantI, op))
    return constantList

# Connect and solve!
r = remote('<challenge_url>', 10000)
r.recvline() # 'Please reverse 10 programs before I give you the flag...'
while r.connected:
    print(r.recvline()) # 'Here is a base64-encoded..'
    base64prg  = r.recvlineS()
    print(r.recvlineS()) # 'You have 10 seconds to provide something to pass in..'
    prg = util.fiddling.b64d(base64prg)
    f = open('sdf','wb')
    f.write(prg)
    constantList = parseElf('sdf')
    x = x_is(constantList) # find password
    r.sendline(str(x)) # send password
    print("x is: " +str(x))
    for i in range(5):
        print(r.recvline())