The task

A screenshot of the competition task.

The shown task can be found here

Linked resources

The tasks links to a webpage and a binary.

The linked page does contain some instructions:

  • Download the binary
  • Run the binary
  • Wait for binary
  • Everything will eb fine

It also tells us that it worked on their infrastructure.

When we try following the instructions we immediately reach a stopper:

file reklest
reklest: Mach-O 64-bit executable x86_64

We got a Mac binary (or freebsd), and more importantly, we can not run it.

Ghidra is brilliant at static analysis, so we open the binary with it.

Checking the entrypoint:

void entry(int param_1,undefined8 param_2)

{
  code *local_10;
  
  local_10 = __ZN7ryklest4main17h0db7a45f0631c966E;
  __ZN3std2rt19lang_start_internal17h36ccce6e8a047133E
            (&local_10,&_anon.7393dc482c8efec160dfd680ea507952.0,(long)param_1,param_2);
  return;
}

We can see a bunch of mangled symbols, lang_start_internal is probably the initialization for the runtime.

ryklest_main seems like the actual program entry point, so we look into it.

  counter = 0xffffffff;
  do {
    scratch = &string_intializing...;
    local_130 = (undefined ***)0x1;
    uStack296 = 0;
    local_118 = (undefined ****)&DAT_10007f870;
    local_110 = 0;
    __ZN3std2io5stdio6_print17ha38795fc370320dbE(&scratch);
    local_50 = (undefined ***)((ulong)local_50 & 0xffffffff00000000 | (ulong)counter);
    local_218 = (undefined ***)&local_50;
    local_210 = 
    __ZN4core3fmt3num3imp52_$LT$impl$u20$core..fmt..Display$u20$for$u20$u32$GT$3fmt17h50838df4a5415920E
    ;
    scratch = &PTR_DAT_1000ac0e8;
    local_130 = (undefined ***)0x2;
    uStack296 = 0;
    local_110 = 1;
    local_118 = &local_218;
    __ZN3std2io5stdio6_print17ha38795fc370320dbE(&scratch);
    counterIsZero = counter != 0;
    counter = counter - 1;
  } while (counterIsZero);

We first count down from 0xffffffff, printing the string initializing and something else.

In the hopes that we don’t actually intialize anything useful we skip over the loop.

  scratch = (undefined **)(___ZN7ryklest4main9OBFSTRING17hc44d85a338dd293eE ^ 0xd8bdeee9c2938e66);
  local_130 = (undefined ***)(_DAT_1000d4398 ^ 0xd598d291c97f7779);
  uStack296 = _DAT_1000d43a0 ^ 0xd32c3e736983bf4;
  local_120 = _DAT_1000d43a8 ^ 0xc428f73fc8f2140;
  local_118 = (undefined ****)(_DAT_1000d43b0 ^ 0xa419a7afe834f505);
  local_110 = _DAT_1000d43b8 ^ 0x4dab6d008d6df4f9;
  __ZN6base646decode6decode17h7723b7c06b238114E(&local_218,&scratch,0x30);
  if ((int)local_218 == 1) {
    local_48 = (undefined ***)local_208;
    local_50 = (undefined ***)local_210;
    __ZN4core6option18expect_none_failed17h686aad664d56bca5E
              ("called `Result::unwrap()` on an `Err` valueinitializing..\n",0x2b,&local_50,
               &PTR___ZN4core3ptr13drop_in_place17h9eefce5a9d028d82E_1000ac0b8,&PTR_DAT_1000ac108);
  }

This seems more interesting, we xor a string in the data section together with immediate data.

Afterwards the string probably gets base64 decoded.

The string in the data section is:

02c9fbb28adf84a81a4646fbcb8ad2e0ac08d54cbef07861
2467b69112d83a6048a17992e1dd5cdeb48e0ef86103e670

The immediate data is:

0xd8bdeee9c2938e66
0xd598d291c97f7779
0x0d32c3e736983bf4
0x0c428f73fc8f2140
0xa419a7afe834f505
0x4dab6d008d6df4f9

Using cyberchef we can decode the data:

this_is_very_s3cret_file13371337.js

This is great news, let’s try to figure out how the string is used.

__ZN5alloc7raw_vec19RawVec$LT$T$C$A$GT$7reserve17h7f54e14bbdf7c90aE(&scratch,0,0x11);
*(undefined *)((long)scratch + zero + 0x10) = 0x2f;
*(undefined8 *)((long)scratch + zero + 8) = 0x312e302e302e3732;
*(undefined8 *)((long)scratch + zero) = 0x312f2f3a70747468;
prefixLen = zero + 0x11;
scratch2 = (undefined ***)scratch;
local_210 = (code *)scratchPlus1;
__ZN5alloc7raw_vec19RawVec$LT$T$C$A$GT$7reserve17h7f54e14bbdf7c90aE
(&scratch2,prefixLen,jsStringlen);
__stubs::_memcpy((void *)((long)scratch2 + prefixLen),strSrc,jsStringlen);
local_40 = jsStringlen + prefixLen;
local_50 = scratch2;
local_48 = (undefined ***)local_210;
__ZN8requests3get17h835de8b04a93914eE(&scratch,&local_50)

It seems like we prepend a string and then execute a http get request.

The string written into scratch is:

http://127.0.0.1/

That request can’t possibly succeed since we didn’t spot a webserver being started so early in the program.

We use the ‘works on our infrastructure’ hint and append the filepath to the initial domain we got.

http://reklest.web.jctf.pro/this_is_very_s3cret_file13371337.js

Contents:

var _0x38d9 = ['charCodeAt', '537865HVvFyd', '153402TvesmL', '637814tgMjYr', '191740qwGPgk',
               'length', '21541OOTfbk', '2FaFWFo', '4DOGutk', '21qyUBwr',
               '500024opEyBW', '2669431PGatTv'];
var _0x2b2f = function(_0x20b4d4, _0x6e7ec) {
  _0x20b4d4 = _0x20b4d4 - 0x68;
  var _0x38d9ae = _0x38d9[_0x20b4d4];
  return _0x38d9ae;
};
(function(_0x610c0b, _0x118d97) {
  var _0xdf5613 = _0x2b2f;
  while(!![]) {
    try {
      var _0x51bf5f = parseInt(_0xdf5613(0x72)) + parseInt(_0xdf5613(0x6f)) +
                     -parseInt(_0xdf5613(0x6d)) * -parseInt(_0xdf5613(0x73)) +
                     parseInt(_0xdf5613(0x68)) + -parseInt(_0xdf5613(0x6e)) *
                     -parseInt(_0xdf5613(0x6b)) + parseInt(_0xdf5613(0x6c)) * 
                     parseInt(_0xdf5613(0x69)) + -parseInt(_0xdf5613(0x70));
      if(_0x51bf5f === _0x118d97) break;
      else _0x610c0b['push'](_0x610c0b['shift']());
    } catch(_0x41e7a9) {
      _0x610c0b['push'](_0x610c0b['shift']());
    }
  }
}(_0x38d9, 0x6f429), text = '{rewJey\x00bnF\x05B_EnEC\x00RZHnSD\x06nCdbEn]\x01\x01ZBnbR\x05CHL');

function xyz(_0x4821d8, _0x1e7b78) {
  var _0x3cb5de = _0x2b2f,
    _0x342b88 = '';
  for(var _0x2e0312 = 0x0; _0x2e0312 < _0x4821d8[_0x3cb5de(0x6a)]; _0x2e0312++) {
    _0x342b88 += String['fromCharCode'](_0x4821d8[_0x3cb5de(0x71)](_0x2e0312) ^ 
                 _0x1e7b78['charCodeAt'](_0x2e0312 % _0x1e7b78[_0x3cb5de(0x6a)]));
  }
  return _0x342b88;
}

Since humans are bad at numbers and the numbers are all identifiers we should rename them to something more sensible.

var lookupTable = ['charCodeAt', '537865HVvFyd', '153402TvesmL', '637814tgMjYr', '191740qwGPgk',
                   'length', '21541OOTfbk', '2FaFWFo', '4DOGutk', '21qyUBwr',
                   '500024opEyBW', '2669431PGatTv'];
var lookUp = function(paramA, paramB) {
  paramA = paramA - 0x68;
  var result = lookupTable[paramA];
  return result;
};
// Gets execute
(function(paramA, paramB) {
  var lookUpLocal = lookUp;
  while(!![]) {
    try {
      var hash = parseInt(lookUpLocal(0x72)) + parseInt(lookUpLocal(0x6f)) +
                 -parseInt(lookUpLocal(0x6d)) * -parseInt(lookUpLocal(0x73)) +
                 parseInt(lookUpLocal(0x68)) + -parseInt(lookUpLocal(0x6e)) *
                 -parseInt(lookUpLocal(0x6b)) + parseInt(lookUpLocal(0x6c)) *
                 parseInt(lookUpLocal(0x69)) + -parseInt(lookUpLocal(0x70));
      if(hash === paramB) break;
      else paramA['push'](paramA['shift']());
    } catch(_0x41e7a9) {
      paramA['push'](paramA['shift']());
    }
  }
}(lookupTable, 0x6f429), text = '{rewJey\x00bnF\x05B_EnEC\x00RZHnSD\x06nCdbEn]\x01\x01ZBnbR\x05CHL');

function xyz(paramA, paramB) {
  var lookUpLocal = lookUp,
    result = '';
  for(var i = 0x0; i < paramA[lookUpLocal(0x6a)]; i++) {
    result += String['fromCharCode'](paramA[lookUpLocal(0x71)](i) ^
                 paramB['charCodeAt'](i % paramB[lookUpLocal(0x6a)]));
  }
  return result;
}

There is an immediately executed function, it hashes elements of the lookuptable, rotating it until the hash is equal to the argument. Running it we get a new ordering for our lookuptable:

["637814tgMjYr", "191740qwGPgk", "length", "21541OOTfbk",
"2FaFWFo", "4DOGutk", "21qyUBwr", "500024opEyBW",
"2669431PGatTv", "charCodeAt", "537865HVvFyd", "153402TvesmL"]

Now we can try to figure out what xyz does.

As a first step we inline the lookUpLocal function:

function xyz(paramA, paramB) {
  var result = '';
  for(var i = 0x0; i < paramA[lookupTable[2]]; i++) {
    result += String['fromCharCode'](paramA[lookupTable[9]](i) ^
                 paramB['charCodeAt'](i % paramB[lookupTable[2]]));
  }
  return result;
}

Replacing the lookups with the values after rotation:

function xyz(paramA, paramB) {
  var result = '';
  for(var i = 0x0; i < paramA["length"]; i++) {
    result += String['fromCharCode'](paramA["charCodeAt"](i) ^
                 paramB['charCodeAt'](i % paramB["length"]));
  }
  return result;
}

So xyz is just a plain old regular xor function. The cyphertext is probably the one embedded in the code.

Since the beginning of the flag is know we xor the cyphertext with it to get a hint what the key might be. It might be the one from the binary, or it might have something to do with all the numbers in the js file.

xyz(text, 'JCTF') -> '1111';

This seems suspicious. The text also seems fairly regular; maybe the key is just 1?

xyz(text, '1') -> 'JCTF{TH1S_w4snt_tr1cky_bu7_rUSt_l00ks_Sc4ry}';