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}