CakeCTF 2022 Writeup

CakeCTF 2022 に DCDC として参加し、 その内の kiwi という問題を解きました。 楽しかった一方、自分にとっては難しい問題が多く、まだまだ力をつける必要があるなと感じました。

[rev] kiwi

バイナリとダミーのflag.txt、それに接続先が提示されます。 バイナリを起動してみるとプロンプトが表示され、正しいキーを入力するとフラグが表示されるようです。

最初に種明かしをすると、有効なキーを入力すると、そのキーで暗号化されたフラグが出力されます。 まず有効なキーのパターンを解析し、その後暗号化されたフラグを復号化するという二段構成になっています。

解析にはGhidraを用いました。

有効なキーのパターンを見つける

入力されたキーが有効か判定し、それを用いているのは cakectf::EncryptionKey::decode です。

関数内部に set_keyset_magic という関数があり、この関数たちを呼び出すことで暗号化の準備が完成します。 Ghidraでデコンパイルするとif文が入り組んでおり、このif文をかいくぐってこれらの関数を呼び出すようなキーを入力する必要があります。

まずは前半部分です。

    while( true ) {
      cVar2 = kiwi::ByteBuffer::readVarUint(bytebuf,&local_34);
      if (cVar2 != '\x01') {
        uVar3 = 0;
        goto LAB_00106145;
      }
      if (local_34 != 2) break;  /* (1) */
      cVar2 = kiwi::ByteBuffer::readVarUint(bytebuf,&local_38);
      if (cVar2 != '\x01') {
        uVar3 = 0;
        goto LAB_00106145;
      }
      local_28 = (Array<unsigned_char> *)set_key(this,mempool,local_38); /* (2) */
      local_30 = (uchar *)kiwi::Array<unsigned_char>::begin(local_28);
      local_20 = (uchar *)kiwi::Array<unsigned_char>::end(local_28);
      for (; local_30 != local_20; local_30 = local_30 + 1) { /* (3) */
        local_18 = local_30;
        cVar2 = kiwi::ByteBuffer::readByte(bytebuf,local_30);
        if (cVar2 != '\x01') {
          uVar3 = 0;
          goto LAB_00106145;
        }
      }
    }

(1) の部分から、 set_key にたどり着くには readVarUintlocal_34 に対して2を出力する必要がありそうです。 readVarUint は内部で readByte という関数を呼び出してwhileループしています。 実験したら、 readVarUint は2文字ずつ読んで数値に変換する関数のようでした。 したがって最初の2文字は 02 でよさそうです。

(2)set_key 内部は次のようになっています。

EncryptionKey * __thiscall
cakectf::EncryptionKey::set_key(EncryptionKey *this,MemoryPool *param_1,uint param_2)
{
  //(...snip...)
  
  *(uint *)this = *(uint *)this | 2;
  AVar2 = kiwi::MemoryPool::array<unsigned_char>(param_1,param_2);

  // (...snip...)
  return this + 8;
}

やってることは、EncryptionKey内部のフラグを立てるかなんかして、MemoryPoolにArrayを作っています。 param_2 が要素数らしいです。

次の入力は要素数が入るらしいので、とりあえず 08 とかにしておきます。

(3) のforループでさっきのArrayに値を読み込んで格納しているっぽいですね。 さっき要素数8にしたので、とりあえず8個分ゼロで埋めておきましょう。 0000000000000000 で。

0123456789abcdef とかでもいいのですが、後々面倒になります。 理由は読み進めるとわかります。

このwhile文にはもう用はないので (1) で2以外をあたえてbreakします。 何を与えるといいかというのは後半部に書いてあります。

    if (local_34 < 3) {
      if (local_34 == 0) {
        uVar3 = 1;         /* (10) */
        goto LAB_00106145;
      }
      if (local_34 == 1) { /* (4) */
        cVar2 = kiwi::ByteBuffer::readVarUint(bytebuf,(uint *)(this + 0x18));
        if (cVar2 != '\x01') {
          uVar3 = 0;
          goto LAB_00106145;
        }
        set_magic(this,(uint *)(this + 0x18)); /* (5) */
        goto LAB_00105fd5;
      }
    }

set_magic にも入っておきたいので、 (4) から、次の値は 01 にしましょう。

(5)set_magic は次のようになっています。

void __thiscall cakectf::EncryptionKey::set_magic(EncryptionKey *this,uint *param_1)
{
  *(uint *)this = *(uint *)this | 1;
  *(uint *)(this + 0x18) = *param_1;
  return;
}

EncryptionKey の内部ビットを立てて、 param_1 の値を EncryptionKey 内部にコピーしています。

さて、続きに何を入れるかですが、実は先の処理を見る必要があります。

main 内部の続きの checkMessage 関数を見ます。

undefined8 checkMessage(EncryptionKey *param_1)

{
  uint uVar1;
  long lVar2;
  undefined8 uVar3;
  int *piVar4;
  Array<unsigned_char> *this;
  
  lVar2 = cakectf::EncryptionKey::magic(param_1);
  if (lVar2 == 0) {
    uVar3 = 1;
  }
  else {
    piVar4 = (int *)cakectf::EncryptionKey::magic(param_1);
    if (*piVar4 == 0xcafec4f3) { /* (6) */
      lVar2 = cakectf::EncryptionKey::key(param_1);
      if (lVar2 == 0) {
        uVar3 = 1;
      }
      else {
        this = (Array<unsigned_char> *)cakectf::EncryptionKey::key(param_1);
        uVar1 = kiwi::Array<unsigned_char>::size(this);
        if (uVar1 < 8) {
          uVar3 = 1;
        }
        else {
          uVar3 = 0;
        }
      }
    }
    else {
      uVar3 = 1;
    }
  }
  return uVar3;
}

(6)cafec4f3 がもう怪しすぎますね。

magic 関数の戻り値が cafec4f3 になればよさそうです。 magic 関数は以下のようになっています。

EncryptionKey * __thiscall cakectf::EncryptionKey::magic(EncryptionKey *this)
{
  EncryptionKey *pEVar1;
  
  if ((*(uint *)this & 1) == 0) {
    pEVar1 = (EncryptionKey *)0x0;
  }
  else {
    pEVar1 = this + 0x18;
  }
  return pEVar1;
}

EncryptionKey の1ビット目が立っていればオフセット0x18の値を返し、そうでなければ0を返します。 このビットは set_magic で立ててあるので大丈夫です。

これより、次に入力する文字列は cafec4f3 …かと思いきや、ちょっとひねりが入れてあります。 readVarUint を見てみましょう。

undefined8 __thiscall kiwi::ByteBuffer::readVarUint(ByteBuffer *this,uint *param_1)
{
  // (...snip...)

  local_11 = 0;
  *param_1 = 0;
  do {
    cVar1 = readByte(this,&local_12);
    if (cVar1 != '\x01') {
      uVar2 = 0;
      goto LAB_00104ddd;
    }
    *param_1 = *param_1 | (local_12 & 0x7f) << (local_11 & 0x1f); /* (8) */
    local_11 = local_11 + 7;                                      /* (9) */
  } while (((char)local_12 < '\0') && (local_11 < 0x23));
  uVar2 = 1;
LAB_00104ddd:
  // (...snip...)
}

(8) で読んだ数値を0x7fで論理積を取り、さらに local_11 でシフトしています。 また (9) を見ると local_11 は7ビットずつシフトしています。

これらを考慮に入れると、数値0xcafec4f3を magic 関数から出力させるには、計算すると、 f389fbd78c を入力する必要があることがわかります。 この辺は紙と鉛筆で計算しました。

最後に decode 関数を脱出するには (10) から 00 を入力します。

これまでの入力を合わせると、 0208000000000000000001f389fbd78c00 となります。 これを入力すると、無事暗号化されたフラグが出力されます。

サーバに接続し、上記の入力をすると、以下のフラグが返ってきました。

bc9f9699b8aebf8380c5aa9ac0c195af9bdeb29c99d99fdb8992baa38c8d868cba81bbaeebb786aba3e2bbb0e7a0b5e1b5ffa3ab94afbffbb5bfb1acf2aca6bd

暗号化されたフラグを復号する

さて、暗号化されたフラグが手に入ったところで第二ラウンド開始です。 暗号化処理は main 内の encryptFlag で行っています。 以下がデコンパイル結果です。

basic_string * encryptFlag(basic_string *enc_flag,Array *raw_flag)
{
  // (...snip...)
  std::vector<unsigned_char,std::allocator<unsigned_char>>::vector
            ((vector<unsigned_char,std::allocator<unsigned_char>> *)enc_flag);
  std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::size();
                    /* try { // try from 0010649e to 00106536 has its CatchHandler @ 00106541 */
  std::vector<unsigned_char,std::allocator<unsigned_char>>::reserve((ulong)enc_flag);
  local_28 = 0;
  while( true ) {
    uVar3 = std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::size();
    if (uVar3 <= local_28) break;
    pcVar4 = (char *)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::
                     operator[]((ulong)raw_flag);
    cVar1 = *pcVar4;
    lVar5 = kiwi::Array<unsigned_char>::data(in_RDX); /* (12) */
    uVar2 = kiwi::Array<unsigned_char>::size(in_RDX);
    local_30 = (long)(int)((((uint)*(byte *)(local_28 % (ulong)uVar2 + lVar5) ^ (int)cVar1) & 0xff |
                           (uint)(uint3)(cVar1 >> 7) << 8) ^ 0xff) ^ local_28; /* (11) */
    std::vector<unsigned_char,std::allocator<unsigned_char>>::emplace_back<unsigned_long>
              ((vector<unsigned_char,std::allocator<unsigned_char>> *)enc_flag,&local_30);
    local_28 = local_28 + 1;
  }
  // (...snip...)
  return enc_flag;
}

while内の (11) が暗号化処理の核です。

lVar5uVar2 はコードをじっとにらむとあの 0000000000000000 のデータとサイズということがわかります。 また、 (uint)*(byte *)(local_28 % (ulong)uVar2 + lVar5) あたりは lVar5[local_28 % (ulong)uVar2] と見えます。 ここで lVar5 は全て0なのでちょっと楽になり、 最終的には ((0 ^ cVar1) & 0xff | (uint)(uint3)(cVar1 >> 7) << 8) ^ 0xff) ^ i (ここで i はループ変数)が暗号化処理だとわかります。

これらの情報を基に solve.cpp を書きました。

#include <cstdio>
#include <string>
#include <vector>
#include <iostream>

using namespace std;

const string enc_flag = "bc9f9699b8aebf8380c5aa9ac0c195af9bdeb29c99d99fdb8992baa38c8d868cba81bbaeebb786aba3e2bbb0e7a0b5e1b5ffa3ab94afbffbb5bfb1acf2aca6bd";

const string fake_flag = "FakeCTF{***** REDUCTED *****}";
const string fake_enc_flag = "b99f9699b8aebf83dddcdfded9d2a3b5abbbaeb8aeaec9c2cdcccfce9e";

uint8_t read_byte(const string &enc, uint32_t idx)
{
    char c1 = enc[idx];
    char c2 = enc[idx + 1];
    string s = string{c1, c2};
    uint8_t i = stoi(s, nullptr, 16);
    return (uint8_t)i;
}

int main()
{
    const string &enc_flag_ref = enc_flag;

    vector<uint8_t> ans = {};
    for (uint32_t i = 0; i < (enc_flag_ref.size() / 2); i++)
    {
        char unenc = 0;
        uint8_t enc = read_byte(enc_flag_ref, i * 2);
        for (char c = 0x00; c < 0x7f; c++)
        {
            if (enc == ((((0 ^ c) & 0xff | (uint32_t)(uint32_t)(c >> 7) << 8) ^ 0xff) ^ i))
            {
                unenc = c;
            }
        }
        ans.push_back((uint8_t)unenc);
    }
    for (int i = 0; i < ans.size(); i++)
    {
        printf("%c", ans[i]);
    }
    printf("\n");
    return 0;
}

上記の solve.cpp をコンパイルし実行するとフラグゲットです。

CakeCTF{w3_n33d_t0_pr3v3nt_Google_fr0m_st4nd4rd1z1ng_ev3ryth1ng}

おしまい。