diff options
author | Malfurious <m@lfurio.us> | 2021-08-05 01:01:37 -0400 |
---|---|---|
committer | Malfurious <m@lfurio.us> | 2021-08-05 01:01:37 -0400 |
commit | a00b9120493916d76fd2089c90be95616d4a1462 (patch) | |
tree | 310ef723f307b040f3555b395e9f6bb56ce0b120 /docs/writeups | |
parent | 5970194d1303e16364ff1405f974c995de46203b (diff) | |
download | lib-des-gnux-a00b9120493916d76fd2089c90be95616d4a1462.tar.gz lib-des-gnux-a00b9120493916d76fd2089c90be95616d4a1462.zip |
Add writeup for ImaginaryCTF 2021 / Roolang
Signed-off-by: Malfurious <m@lfurio.us>
Diffstat (limited to 'docs/writeups')
-rw-r--r-- | docs/writeups/ImaginaryCTF_2021/Roolang.png | bin | 0 -> 7788806 bytes | |||
-rw-r--r-- | docs/writeups/ImaginaryCTF_2021/Roolang.txt | 584 |
2 files changed, 584 insertions, 0 deletions
diff --git a/docs/writeups/ImaginaryCTF_2021/Roolang.png b/docs/writeups/ImaginaryCTF_2021/Roolang.png Binary files differnew file mode 100644 index 0000000..f1dd415 --- /dev/null +++ b/docs/writeups/ImaginaryCTF_2021/Roolang.png diff --git a/docs/writeups/ImaginaryCTF_2021/Roolang.txt b/docs/writeups/ImaginaryCTF_2021/Roolang.txt new file mode 100644 index 0000000..9b52316 --- /dev/null +++ b/docs/writeups/ImaginaryCTF_2021/Roolang.txt @@ -0,0 +1,584 @@ +ELF files are sooooo last year :rooVoid:. Instead of making yet another ELF to +reverse, I made a new type of executable binary for you to reverse :rooYay:! It +uses my new language Roolang. Just execute flag.roo to get the flag +:rooPuzzlerDevil:! It's dynamically linked, so make sure you have the provided +roo library files in the same directory :rooCash:. It's not very optimized, so +it might take a moment (sorry about that) :rooNobooli:... + +Special shoutout to :rooRobin: and :rooOreos:, who have nothing to do with this +challenge, but are both very cool people :rooHeart:. Good luck, and I hope you +have fun :rooNervous:! + +Category: re (400 points) +Chall author: puzzler7 +Writeup author: malfurious + + + +Setup +----- +We are provided with a zip archive containing several files: + + blind.roo * + flag.roo (See Roolang.png) + imag.roo * + nobooli.roo * + oreos.roo * + robin.roo * + roolang.py (See Appendix A) + +flag.roo is the primary 'executable'. This file is actually a PNG image, made +up of a matrix of smaller sub-images (emoji from the CTF's Discord server). + +The remaining *.roo files (marked with a '*') are the 'library files' referred +to by the problem description. They are individual original images for each +of the emoji. + +roolang.py is a regular Python script that serves as an interpreter for Roolang. + + + +Execution +--------- +If you run roolang.py on the flag.roo file, it starts to output text: + + $ python roolang.py flag.roo + Parsing... + Running... + The flag is ictf{th ... + +Each additional character takes longer and longer than the last to be displayed. +Their calculation is being slowed by an inefficient algorithm. + + + +Tweaking the interpreter +------------------------ +I initially tried making improvments to the Python code to see what kind of +speedup we could see by optimizing the way it executes the various roolang +instructions. + +For example: I tried reducing the amount of stack manipulation per instruction, +removing checks to useless internal variables, and rewriting operations to take +effect in-place where appropriate. I even replaced their `eval(string+'()')` +method for executing instructions with a proper jump table (via a dictionary). + +This made a _very_ noticable difference in run time (LOL), but is not enough +to make full execution of flag.roo feasible (as expected). So, I moved on to +reverse engineering of the flag file using the interpreter. + + + +Bytecode +-------- +I mentioned earlier that the program is a matrix of images. This actually +forms a linear sequence of emoji, left-to-right, then top-to-bottom. We can +see in the functions robinify() and run() how the input file is processed to +recover the opcode to execute. + +Each emoji is detected, reference to the original emoji ('library') files, and +is tokenized: + + image token + ----------------------------- + robin.roo => r + oreos.roo => o + blind.roo => b + imag.roo => i + nobooli.roo => n + +Every five tokens are grouped as a word. Token ordering is similar to +big-endian, so 'robin' means 'r, o, b, i, n'). + + + +Runtime +------- +The roolang process has access to a stack and a single general-purpose +register ('the register'). + +The calling convention is to push arguments to the stack before jumping. The +return address is expected to be 'below' the arguments. The call instruction +achieves this by cheating the stack and inserting the address below the +already-pushed argument. The subroutine return value is also pushed to the +stack. Similar trickery is done by the ret instruction to pop the return +address out from underneath the return value. + +Execution starts at the beginning of the stream and terminates at the end. By +studying the interpreter, we can see that roolang implements the following +instructions (by Python functions of the same name): + + Opcode Len(words) Mnemonic Description + ---------------------------------------------------------------------------- + robin variable push If the 2nd word is the special value 'rinin', + the value in the register is pushed to the + stack. Otherwise, the 2nd word encodes the + number of additional words to read. These + additional words encode a value to push to + the stack. All encoded values are in base-3 + and use the o, b, i tokens to represent + digits 0, 1, 2 respectively. + + rboin 1 pop Value is popped from the stack + rinin 1 pop reg Value is popped from the stack into register + + rbiin 1 print chr Value is popped and is output (as character) + rboon 1 print Value is popped and is output (as integer) + + riobn 1 add Pop 2 off stack, push sum + rooon 1 sub Pop 2 off stack, push difference + riibn 1 mul Pop 2 off stack, push product + riion 1 div Pop 2 off stack, push quotient + ribon 1 mod Pop 2 off stack, push remainder + ronon 1 and Pop 2 off stack, push bitwise and + roion 1 or Pop 2 off stack, push bitwise or + roibn 1 xor Pop 2 off stack, push bitwise xor + riiin 1 dup Push a copy of the top stack value + rioin 1 swap Swap the top two stack values + + rnbon 2 label Mark a jump point in the program. The second + word identifies the jump point. + + rioon 2 jmp Jump to a jump point, identified by the value + of the second word. + + rbion 2 jnz Jump to a jump point, identified by the value + of the second word. Only jump if the top + stack value is non-zero. + + roiin 2 call Push return address to the stack and jump to + the jump point identified by the second word. + + ribbn 1 ret Pop return address from the stack and jump + to it. + + + +Disassembly +----------- +With this understanding, we can easily modify the interpreter to act as a +roolang disassembler, outputting the instruction mnemonics for operations used +by the flag program (following argument data for multi-word instructions). + +See Appendix B for the disassembly I generated. It is annotated with equivalent +C code to get a better idea of what the program does. + +The program starts out by pushing a bunch of data to the stack for processing. +What follows is a loop which pops a value, performs math on it, and prints a +character to the output. A zero value was pushed to the bottom of the stack +so the program can detect when it has finished. + +Each stack value is XOR'd against the result of a recursive function call +(identified in the flag program by the label 'robin'). Upon closer inspection, +robin appears to implement a fibonacci function. The exact logic of the main +loop is: + + for (i : 0 => len(data)) + { + print_chr(data[i] ^ robin(i)); + } + +i gets farily large, which leads to a ton of redundant processing by robin(). +This is what slows the program, preventing us from finishing it in a reasonable +time. + + + +Dynamic programming solution +---------------------------- +I rewrote the flag program (in Python this time) to leverage dynamic programming +via memoization to accelerate robin() calculations. Each result produced by the +function is cached, so that it doesn't need to be re-computed the next time that +value is part of a call tree. The rest of my program is implemented to match +the logic of flag.roo verbatim. See Appendix C for this solution code. + +"The flag is ictf{thr33_ch33r5_t0_r00r0bin_th3_b3st_0f_u5_a11_r00h3art_7d2e2642}" + + + +================================================================================ += Appendix A: roolang.py = +================================================================================ +#!/usr/bin/env python3 + +import sys +from PIL import Image +import numpy as np + +class Stack(list): + def push(self, x): + self.append(x) + + def peek(self): + return self[-1] + +stack = Stack([]) +program = [] +register = 0 +insn_pointer = 0 + +DEBUG = 0 + +def robinify(im): + tiles = [im[x:x+128,y:y+128,0:4] for x in range(0,im.shape[0],128) for y in range(0,im.shape[1],128)] + R = np.asarray(Image.open("robin.roo")) + O = np.asarray(Image.open("oreos.roo")) + B = np.asarray(Image.open("blind.roo")) + I = np.asarray(Image.open("imag.roo")) + N = np.asarray(Image.open("nobooli.roo")) + d = list(zip([R,O,B,I,N], "robin")) + + ret = '' + for c in tiles: + for pair in d: + if np.all(pair[0]==c): + ret += pair[1] + break + return ret + +def step(): + global insn_pointer + insn = c_insn() + if DEBUG: + print(insn, program[insn_pointer+1], "@", insn_pointer) + eval(insn+"()") + +def run(prog): + global insn_pointer, program + for ch in prog: + if ch not in "robin": + print("Syntax Error") + exit(1) + + if len(prog) % 5 != 0: + print("Syntax Error") + + program = [prog[i:i+5] for i in range(0, len(prog), 5)] + try: + while insn_pointer < len(program): + step() + insn_pointer += 1 + if DEBUG: + print(stack) + except Exception as e: + print("Fatal Error.") + raise e + print() + print(stack) + +def c_insn(): + return program[insn_pointer] + +def robin(): + global insn_pointer + insn_pointer += 1 + toPush = c_insn() + if toPush == "rinin": + stack.push(register) + else: + words = parseDigit(toPush) + toPush = 0 + for i in range(words): + insn_pointer += 1 + toPush += parseDigit(c_insn()) + toPush *= 27 + stack.push(toPush//27) + +def parseDigit(s): + return int(s.replace('o', '0').replace('b', '1').replace('i', '2')[1:-1], 3) + +def rboin(): + stack.pop() + +def riobn(): + stack.push(stack.pop()+stack.pop()) + +def rooon(): + stack.push(stack.pop()-stack.pop()) + +def riibn(): + stack.push(stack.pop()*stack.pop()) + +def riion(): + stack.push(stack.pop()//stack.pop()) + +def ribon(): + stack.push(stack.pop()%stack.pop()) + +def ronon(): + stack.push(stack.pop()&stack.pop()) + +def roion(): + stack.push(stack.pop()|stack.pop()) + +def roibn(): + stack.push(stack.pop()^stack.pop()) + +def riiin(): + x = stack.pop() + stack.push(x) + stack.push(x) + +def rioin(): + x = stack.pop() + y = stack.pop() + stack.push(x) + stack.push(y) + +def rinin(): + global register + register = stack.pop() + +def rbiin(): + print(chr(stack.pop()), end='', flush=True) + +def rboon(): + print(stack.pop(), end='', flush=True) + +def rnbon(): + global insn_pointer + insn_pointer += 1 + +def rioon(): + global insn_pointer + insn_pointer += 1 + for i, word in enumerate(program): + if word == "rnbon": + if i != len(program)-1 and program[i+1] == c_insn(): + insn_pointer = i+1 + return + print("Label not found!") + raise RuntimeError + +def rbion(): + global insn_pointer + if stack.peek(): + rioon() + else: + insn_pointer += 1 + +def ribbn(): + global insn_pointer + retval = stack.pop() + insn_pointer = stack.pop() + if DEBUG: + print("returning to", insn_pointer) + stack.push(retval) + +def roiin(): + global insn_pointer + arg = stack.pop() + stack.push(insn_pointer+1) + stack.push(arg) + rioon() + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: ./roolang.py [filename.roo]") + exit() + + if sys.argv[1].split('.')[-1] != "roo": + print("Invalid file format!") + exit() + + with Image.open(sys.argv[1]) as f: + print("Parsing...") + program = robinify(np.asarray(f)) + print("Running...") + run(program) + print("Finished execution.") + + + +================================================================================ += Appendix B: flag.roo annotated disassembly = +================================================================================ +Parsing... +Disassebling... + + rnbon label rooon: + robin push 0 + robin push 14472334024676096 + robin push 8944394323791450 + robin push 5527939700884769 + robin push 3416454622906725 + robin push 2111485077978096 + robin push 1304969544928756 + robin push 806515533049347 + robin push 498454011879172 + robin push 308061521170150 + robin push 190392490709200 + robin push 117669030460982 + robin push 72723460248127 + robin push 44945570212756 + robin push 27777890035307 + robin push 17167680177653 + robin push 10610209857675 + robin push 6557470319826 + robin push 4052739537835 + robin push 2504730782038 + robin push 1548008755937 + robin push 956722025992 + robin push 591286729974 + robin push 365435296253 + robin push 225851433664 + robin push 139583862488 + robin push 86267571223 + robin push 53316291075 + robin push 32951280083 + robin push 20365011165 + robin push 12586268949 + robin push 7778742098 + robin push 4807527027 + robin push 2971214979 + robin push 1836311808 + robin push 1134903217 + robin push 701408693 + robin push 433494481 + robin push 267914343 + robin push 165580035 + robin push 102334114 + robin push 63246016 + robin push 39088153 + robin push 24157707 + robin push 14930304 + robin push 9227513 + robin push 5702805 + robin push 3524541 + robin push 2178357 + robin push 1346217 + robin push 832119 + robin push 514176 + robin push 317697 + robin push 196465 + robin push 121346 + robin push 75129 + robin push 46403 + robin push 28590 + robin push 17692 + robin push 10993 + robin push 6687 + robin push 4157 + robin push 2668 + robin push 1606 + robin push 957 + robin push 534 + robin push 282 + robin push 128 + robin push 176 + robin push 42 + robin push 94 + robin push 2 + robin push 114 + robin push 108 + robin push 100 + robin push 99 + robin push 35 + robin push 103 + robin push 105 + robin push 85 + + ========================================================================================= + ; main + rnbon label roobn: + robin push 0 + rinin pop reg i = 0 + + rnbon label rooin: + rbion jnz robon for (c on stack) { + rioon goto robbn (exit) + + rnbon label robon: + robin push reg + roiin call robin int p = c ^ robin(i); + roibn xor print(p); + rbiin print char i++; + robin push reg } + robin push 1 + riobn add + rinin pop reg + rioon goto rooin + rioon goto robbn + + ========================================================================================= + ; fib (recursive) + rnbon label robin: + rbion jnz roion int robin(int arg) { + rboin pop if (arg == 0) { + robin push 1 return 1; + ribbn ret } + + rnbon label roion: else { + riiin dup int x = arg - 1; + robin push 1 if (x == 0) { + rooon sub return arg; // return 1; + rbion jnz roibn } + rboin pop + ribbn ret + + rnbon label roibn: else { + rboin pop int y = robin(arg - 2); + riiin dup int z = robin(arg - 1); + robin push 1 return y + z; + rooon sub } + robin push 0 } + rooon sub } + riiin dup + robin push 1 + rooon sub + robin push 0 + rooon sub + roiin call robin -- + rioin swap + roiin call robin -- + riobn add + rioin swap + rboin pop + ribbn ret + ========================================================================================= + + rnbon label robbn: + robin push 0 + rboin pop + robin push 0 + rboin pop + + + +================================================================================ += Appendix C: flag.py solution = +================================================================================ +solutions = {} + +i = 0 + +data = [ 14472334024676096, 8944394323791450, 5527939700884769, 3416454622906725, + 2111485077978096, 1304969544928756, 806515533049347, 498454011879172, + 308061521170150, 190392490709200, 117669030460982, 72723460248127, + 44945570212756, 27777890035307, 17167680177653, 10610209857675, + 6557470319826, 4052739537835, 2504730782038, 1548008755937, 956722025992, + 591286729974, 365435296253, 225851433664, 139583862488, 86267571223, + 53316291075, 32951280083, 20365011165, 12586268949, 7778742098, + 4807527027, 2971214979, 1836311808, 1134903217, 701408693, 433494481, + 267914343, 165580035, 102334114, 63246016, 39088153, 24157707, 14930304, + 9227513, 5702805, 3524541, 2178357, 1346217, 832119, 514176, 317697, + 196465, 121346, 75129, 46403, 28590, 17692, 10993, 6687, 4157, 2668, + 1606, 957, 534, 282, 128, 176, 42, 94, 2, 114, 108, 100, 99, 35, 103, + 105, 85 ] + +def robin(arg): + if arg in solutions: + return solutions[arg] + + if arg < 2: + return 1 + + ret = robin(arg-2) + robin(arg-1) + solutions[arg] = ret + return ret + +while len(data) > 0: + c = data[-1] + data.pop() + print(chr(c ^ robin(i)), end="") + i += 1 |