HITCON CTF 2019 Quals: Reverse - EmojiVM

Reading time ~15 minutes

This challenge was a VM implemented where every instruction was an emoji. For the first part of the challenge we had to reverse a flag checker program written in this instruction set.

The challenge

The following emojis where mapped the following instructions:

NOP: ๐Ÿˆณ
ADD: โž•
SUB: โž–
MUL: โŒ
MOD: โ“ 
XOR: โŽ
AND: ๐Ÿ‘ซ
IS_LESS: ๐Ÿ’€
IS_EQ: ๐Ÿ’ฏ
JMP: ๐Ÿš€
JMP_IF: ๐Ÿˆถ
JMP_IF_FALSE: ๐Ÿˆš
PUSH_EMOJI: โฌ
POP: ๐Ÿ”
LD: ๐Ÿ“ค
ST: ๐Ÿ“ฅ
NEW: ๐Ÿ†•
FREE: ๐Ÿ†“
READ: ๐Ÿ“„
POP_OBJ: ๐Ÿ“
FLUSH: ๐Ÿ”ก
POP_INT64: ๐Ÿ”ข
EXIT: ๐Ÿ›‘

And to the following values:

0: ๐Ÿ˜€
1: ๐Ÿ˜
2: ๐Ÿ˜‚
3: ๐Ÿคฃ
4: ๐Ÿ˜œ
5: ๐Ÿ˜„
6: ๐Ÿ˜…
7: ๐Ÿ˜†
8: ๐Ÿ˜‰
9: ๐Ÿ˜Š
10: ๐Ÿ˜

The disassembler

The following code disassembles the chal.evm file and outputs the disassembly.

It also skips NOPs and constant numeric operations (additional and multiply), so single-digit constants converted into their original number.

enum OpCode { NOP, ADD, SUB, MUL, MOD, XOR, AND, IS_LESS, IS_EQ, JMP, JMP_IF, JMP_IF_FALSE,
              PUSH_EMOJI, POP, LD, ST, NEW, FREE, READ, POP_OBJ, FLUSH, POP_INT64, EXIT, 
              NUM_0, NUM_1, NUM_2, NUM_3, NUM_4, NUM_5, NUM_6, NUM_7, NUM_8, NUM_9, NUM_10,
              PUSH_VAL, STORE_OBJ_VAL };

class Instruction
{
    public OpCode OpCode { get; set; }
    public long Value { get; set; }
    public int FileOffset { get; set; }
    public int InstructionIdx { get; set; }
    public bool IsNum => OpCode.NUM_0 <= OpCode && OpCode <= OpCode.NUM_10;
    public bool IsPush => OpCode == OpCode.PUSH_EMOJI || OpCode == OpCode.PUSH_VAL;

    public int ObjectIdx { get; set; }
    public int ByteIdx { get; set; }
    public bool IsStore => OpCode == OpCode.ST;

    public override string ToString() => 
        OpCode == OpCode.PUSH_VAL ? $"PUSH {Value}" : 
        OpCode == OpCode.STORE_OBJ_VAL ? $"obj{ObjectIdx}[{ByteIdx}] = {Value}" : 
        $"{OpCode}";

    public static Instruction Push(long argument, Instruction orig = null) => 
        new Instruction() { OpCode = OpCode.PUSH_VAL, Value = argument, FileOffset = orig?.FileOffset ?? 0, 
            InstructionIdx = orig?.InstructionIdx ?? 0 };
    public static Instruction Store(int objectIdx, int byteIdx, long value) => 
        new Instruction() { OpCode = OpCode.STORE_OBJ_VAL, ObjectIdx = objectIdx, ByteIdx = byteIdx, Value = value };
}

class InstrFilter
{
    public int SourceLen { get; set; }
    public Func<Instruction[], bool> Matcher { get; set; }
    public Func<Instruction[], Instruction> Converter { get; set; }

    public InstrFilter(int sourceLen, Func<Instruction[], bool> tester, Func<Instruction[], Instruction> converter)
    {
        SourceLen = sourceLen;
        Matcher = tester;
        Converter = converter;
    }
}

static void EmojiDecode()
{
    var baseDir = @"g:\Dropbox\hack\hitcon19\challs\emojivm_reverse\";
    var opCodes = File.ReadAllLines($"{baseDir}opcodes_enc.txt").Select(
          (codePoint, i) => new { codePoint, opCode = (OpCode)i }).ToArray();

    var prgSrc = File.ReadAllText($"{baseDir}chal.evm");
    var prg0 = new List<Instruction>();
    for (int i = 0; i < prgSrc.Length;)
    {
        var opCodeInfo = opCodes.Single(x => prgSrc.Substring(i, x.codePoint.Length) == x.codePoint);
        prg0.Add(new Instruction() { FileOffset = i, InstructionIdx = prg0.Count, OpCode = opCodeInfo.opCode });
        i += opCodeInfo.codePoint.Length;
    }

    Instruction[] RunFilters(Instruction[] source, InstrFilter[] filters)
    {
        var tail = new Instruction[10];
        for (int i = 0; i < source.Length;)
        {
            var hadMatch = false;
            foreach (var filter in filters)
            {
                var filterInput = source.Skip(i).Concat(tail).Take(filter.SourceLen).ToArray();
                if (filter.Matcher(filterInput))
                {
                    source = source.Take(i).Concat(new[] { filter.Converter(filterInput) })
                        .Concat(source.Skip(i + filter.SourceLen)).ToArray();
                    hadMatch = true;
                    break;
                }
            }

            if (!hadMatch)
                i++;
        }
        return source;
    }

    Instruction[] RunFiltersMulti(Instruction[] source, InstrFilter[] filters)
    {
        while (true)
        {
            var oldSourceLen = source.Length;
            source = RunFilters(source, filters);
            if (source.Length == oldSourceLen)
                break;
        }
        return source;
    }

    var prg1 = RunFiltersMulti(prg0.ToArray(), new[]
    {
        new InstrFilter(2, ins => ins[0].IsPush && ins[1].IsNum, 
            ins => Instruction.Push(ins[1].OpCode - OpCode.NUM_0, ins[0])),

        new InstrFilter(3, ins => ins[0].IsPush && ins[1].IsPush && ins[2].OpCode == OpCode.MUL, 
            ins => Instruction.Push(ins[0].Value * ins[1].Value, ins[0])),

        new InstrFilter(3, ins => ins[0].IsPush && ins[1].IsPush && ins[2].OpCode == OpCode.ADD,
            ins => Instruction.Push(ins[0].Value + ins[1].Value, ins[0])),
    });

    var prg2 = RunFiltersMulti(prg1, new[] { new InstrFilter(4, 
          ins => ins[0].IsPush && ins[1].IsPush && ins[2].IsPush && ins[3].OpCode == OpCode.ST, 
          ins => Instruction.Store((int)ins[2].Value, (int)ins[1].Value, ins[0].Value)) });

    var prg1src = prg1.Select(x => $"{x.InstructionIdx}: {x}").ToArray();
    File.WriteAllLines($"{baseDir}prg1src.txt", prg1src);
}

The manually decompiled code

The disassembed code was manually decompiled to this more readable version:

3:    obj0 = new(60)

12:   obj0 = "*************************************\n\0"
675:  print(pop) obj0
678:  obj0 = "*                                   *\n\0"
1341: print(pop) obj0
1344: obj0 = "*             Welcome to            *\n\0"
2070: print(pop) obj0
2073: obj0 = "*        EmojiVM ๐Ÿ˜€๐Ÿ˜๐Ÿคฃ๐Ÿค”๐Ÿคจ๐Ÿ˜ฎ       *\n\0"
3216: print(pop) obj0
3219: obj0 = "*       The Reverse Challenge       *\n\0"
4017: print(pop) obj0
4020: obj0 = "*                                   *\n\0"
4683: print(pop) obj0
4686: obj0 = "*************************************\n\0"
5349: print(pop) obj0
5352: obj0 = "\n\0\0"
5373: print(pop) obj0
5376: obj0 = "Please input the secret:\0"
5951: print(pop) obj0

5954: obj1 = new(30)
5963: obj2 = new(30)
5972: obj3 = new(30)
5981: obj4 = new(30)

5990: obj2 = "\x18\x05\x1d\x10\x42\x09\x4a\x24\x00\x5b\x08\x17\x40\x00\x72\x30\x09\x6c\x56\x40\x09\x5b\x05\x1a\x00" (len=25)
6363: obj4 = "\x8e\x63\xcd\x12\x4b\x58\x15\x17\x51\x22\xd9\x04\x51\x2c\x19\x15\x86\x2c\xd1\x4c\x84\x2e\x20\x06\x00" (len=25)

6808: obj1 = input = READ()

6811: obj5 = new(5)  ==>  VAR0 (input_len = 0), VAR1 (input_idx = 0), VAR2 (idx_mod4 = 0), VAR3, VAR4

6814: JMP 7052(==inputEnd)     if (input[input_idx] == '\0')
6870: JMP 6997(==inputNewLine) if (input[input_idx] == '\n')

6926: input_len++
6939: input_idx++
6981: JMP 6814

inputNewLine:
  6997: input[input_idx] = '\0'
  7036: JMP 7052(==inputEnd)

inputEnd:
  7052: JMP 8550(==FAIL) if (input_len != 24)

# check dashes (format: ABCD-EFGH-IJKL-MNOP-QRST)
7111: input_idx = 0
7118: JMP 7294(==checkDash) if ((input_idx+1) % 5 == 0)

7177: input_idx++
7190: JMP 7118 if (input_idx < 24)
7278: JMP 7401(==dashesAreOkay)

checkDash:
  7294: JMP 8550(==FAIL) if (input[input_idx] != '-')

7385: JMP 7177

dashesAreOkay:
  7401: input_idx = 0
  7408: idx_mod4 = input_idx % 4
  7421: JMP 7750(==mod4_0) if (idx_mod4 == 0)
  7474: JMP 7820(==mod4_1) if (idx_mod4 == 1)
  7527: JMP 7887(==mod4_2) if (idx_mod4 == 2)
  7580: JMP 7969(==mod4_3) if (idx_mod4 == 3)

bigLoop1:
  7633: input_idx++
  7646: JMP 7408 if (input_idx < 24)

  7734: JMP 8075

mod4_0:
  7750: input[input_idx] + 30
  7767: obj3[input_idx] = input[input_idx] + 30
  7804: JMP 7633(==bigLoop1)

mod4_1:
  7820: obj3[input_idx] = (input[input_idx] - 8) ^ 7
  7871: JMP 7633(==bigLoop1)

mod4_2:
  7887: obj3[input_idx] = ((input[input_idx] + 44) ^ 68) - 4
  7953: JMP 7633(==bigLoop1)

mod4_3:
  7969: obj3[input_idx] = (input[input_idx] ^ 101) ^ (172 & 20)
  8059: JMP 7633(==bigLoop1)


8075: input_idx = 0
8082: idx_mod4 = 0
8089: JMP 8284 if (obj3[input_idx] == obj4[input_idx])


8151: idx_mod4--
8167: input_idx++
8223: JMP 8089 if (input_idx < 24)


8268: JMP 8342


8284: idx_mod4++
8326: JMP 8167


8342: JMP 8550(==FAIL) if (idx_mod4 != 24)


8401: input_idx = 0

decodeFlag:
  8408: obj2[input_idx] ^= input[input_idx]
  8433: input_idx++
  8446: JMP 8408(==decodeFlag) if (input_idx < 24)


8534: JMP 8700(==WIN)

fail:
  8550: obj0 = "๐Ÿ˜ญ\n\0" (LOUDLY CRYING FACE)
  8652: print(pop) obj0
  8684: JMP 8825(==EXIT)

win:
  8700: obj0 = "๐Ÿ˜\n\0" (smiling face with heart-shaped eyes)
  8802: print(pop) obj0
  8805: print(pop) obj2
  8808: obj0 = "\n\0"
  8822: print(pop) obj0
  8825: EXIT

Reversing the flag checker

So the important part of the above code is that it takes our input, converts it byte-by-byte with 4 separate functions (uses the algorithm based on input byteโ€™s index mod 4) and compares to the expected value.

And the โ€œdash checkingโ€ parts are actually was misinterpreted by me while reversing the challenge, I did not really realize this until I wrote this writeup.. :D

Solver

The following code reverses the conversion operation and gives us the flag:

var flagKey = EncodingHelper.GetBytes("\x18\x05\x1d\x10\x42\x09\x4a\x24\x00\x5b\x08\x17\x40\x00\x72\x30\x09\x6c\x56\x40\x09\x5b\x05\x1a\x00");
var toMatch = EncodingHelper.GetBytes("\x8e\x63\xcd\x12\x4b\x58\x15\x17\x51\x22\xd9\x04\x51\x2c\x19\x15\x86\x2c\xd1\x4c\x84\x2e\x20\x06\x00");
var input = toMatch.Select((x,i) => (byte)(
    i % 4 == 0 ? x - 30 : 
    i % 4 == 1 ? (x ^ 7) + 8 : 
    i % 4 == 2 ? ((x + 4) ^ 68) - 44 :
    x ^ 101 ^ (172 & 20))).ToArray();
var flag = EncodingHelper.GetString(CryptoUtils.XorEqual(flagKey, input));

The flag

hitcon{R3vers3_Da_3moj1}

HITCON CTF 2019 Quals: Reverse - CoreDumb

So this challenge was around that we got a Core file instead of a runnable linux binary which checked our flag.To be frank, I donโ€™t reall...… Continue reading

HITCON CTF 2019 Quals: Pwn - Crypto in the shell

Published on October 19, 2019

HITCON CTF 2019 Quals: Misc - heXDump

Published on October 19, 2019