SECCON CTF 2022 Writeup

2022/11/12 14:00 (JST) ~ 2022/11/13 14:00 (JST)に開催されたSECCON CTF 2022のWriteupです。

Double Lariatというチームで参加し、全体で33位、国内で7位という結果になりました。国内順位10位以内だと決勝戦に進めるらしいので非常に嬉しいです。

私はrevを担当しbabycmpとeguiteの二問を解いたのでその解説をします。

babycmp

引数にフラグを入力すると、正しいフラグかチェックしてくれるだけのシンプルなフラグチェッカーが与えられます。

$ ./chall.baby SECCON{dummy}
Wrong...

とりあえずGhidraでデコンパイルして、main関数をのぞきます。

undefined8 main(int param_1,undefined8 *argv)

{
    size_t len;
    ulong i;
    size_t j;
    ulong *ptr_s;
    undefined8 uVar1;
    long in_FS_OFFSET;
    undefined4 local_68;
    undefined4 uStack100;
    undefined4 uStack96;
    undefined4 uStack92;
    undefined4 local_58;
    undefined2 local_54;
    undefined local_52;
    undefined4 local_48;
    undefined4 uStack68;
    undefined4 uStack64;
    undefined4 uStack60;
    undefined4 local_38;
    undefined4 uStack52;
    undefined4 uStack48;
    undefined4 uStack44;
    int local_28;
    long local_20;
  
    local_20 = *(long *)(in_FS_OFFSET + 0x28);
    if (param_1 < 2) {
      uVar1 = 1;
      __printf_chk(1,"Usage: %s FLAG\n",*argv);
    }
    else {
        ptr_s = (ulong *)argv[1];
        cpuid_basic_info(0);
        local_28 = 0x380a41;
        local_58 = 0x3032204e;
        local_48 = 0x202f2004;
        uStack68 = 0x591e2320;
        uStack64 = 0x357f1a44;
        uStack60 = 0x2b2d3675;
        local_54 = 0x3232;
        local_38 = 0x35a1711;
        uStack52 = 0x736506d;
        uStack48 = 0x1093c15;
        uStack44 = 0x362b4704;
        local_52 = 0;
        local_68 = 0x636c6557;
        uStack100 = 0x20656d6f;
        uStack96 = 0x53206f74;
        uStack92 = 0x4f434345;
        len = strlen((char *)ptr_s);
        if (len != 0) {
            *(byte *)ptr_s = *(byte *)ptr_s ^ 0x57;
            i = 1;
            if (len != 1) {
                do {
                    j = i + 1;
                    *(byte *)(argv[1] + i) =
                    *(byte *)(argv[1] + i) ^
                    *(byte *)((long)&local_68 +
                            i + ((SUB168(ZEXT816(i) * ZEXT816(0x2e8ba2e8ba2e8ba3) >> 0x40,0) &
                                 0xfffffffffffffffc) * 2 + (i / 0x16) * 3) * -2);
                    i = j;
                } while (len != j);
            }
            ptr_s = (ulong *)argv[1];
        }
        if ((((ptr_s[1] ^ CONCAT44(uStack60,uStack64) | *ptr_s ^ CONCAT44(uStack68,local_48)) == 0) &&
             ((ptr_s[3] ^ CONCAT44(uStack44,uStack48) | ptr_s[2] ^ CONCAT44(uStack52,local_38)) == 0)) &&
             (*(int *)(ptr_s + 4) == local_28)) {
            uVar1 = 0;
            puts("Correct!");
        }
        else {
            uVar1 = 0;
            puts("Wrong...");
        }
    }
    if (local_20 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
        __stack_chk_fail();
    }
    return uVar1;
}

関数の真ん中のあたりに複雑な処理が書かれています。この部分をgdbで動的解析してみると、Welcome to SECCON 2022と引数として与えられた文字列のxorをとるだけだと分かりました。

その処理の後、xorをとった後の文字列とuStack60uStack64などでxorをとり、一致するならCorrect!と表示されるようになっています。

よって、Welcome to SECCON 2022uStack60などでxorをとり、結合することでフラグを入手できました。

from Crypto.Util.number import bytes_to_long

local_48 = 0x202f2004
local_28 = 0x380a41

local_38 = 0x35a1711
uStack52 = 0x736506d

uStack44 = 0x362b4704
uStack48 = 0x1093c15

uStack68 = 0x591e2320
uStack48 = 0x1093c15

uStack64 = 0x357f1a44
uStack60 = 0x2b2d3675

xor_data = [
    local_48,
    uStack68,
    uStack64,
    uStack60,
    local_38,
    uStack52,
    uStack48,
    uStack44,
    local_28
]

key = b"Welcome to SECCON 2022" * 10
key_blocks = [bytes_to_long(key[i:i+4][::-1]) for i in range(0, len(key), 4)]

flag = b''
for blk, dat in zip(key_blocks, xor_data):
    flag += (blk ^ dat).to_bytes(4, 'little')

print(flag)
$ python3 solve.py
b'SECCON{y0u_f0und_7h3_baby_flag_YaY}C'

eguite

eguiteというELFもしくはEXEファイルが与えられます。実行してみると、SECCONのロゴとテキストボックス、ボタンが配置されたシンプルなアプリケーションが起動します。

適当に文字を入力してボタンを押すと、Invalid license.と表示されました。

大まかな動作は分かったのでGhidraでデコンパイルして、main関数をのぞきます。

void main(int param_1,undefined8 param_2)
{
  code *local_8;
  
  local_8 = eguite::main;
  std::rt::lang_start_internal
            (&local_8,anon.0f8b4128f47727310630ca6667e49e18.0.llvm.18286627608682017193,
             (long)param_1,param_2);
  return;
}

std::rt::lang_start_internalllvmなどが見えるので、Rustで書かれたプログラムだと推測しました。その中で、eguite::mainが呼び出されていそうなので処理を見てみます。

void eguite::main(void)

{
  undefined8 in_R9;
  undefined4 local_98;
  undefined4 uStack148;
  undefined4 uStack144;
  undefined4 uStack140;
  undefined4 local_88;
  undefined4 uStack132;
  undefined4 uStack128;
  undefined4 uStack124;
  undefined8 local_78;
  undefined4 local_70;
  undefined4 local_6c;
  undefined8 local_68;
  undefined8 local_54;
  undefined4 local_4c;
  undefined2 local_42;
  undefined4 local_38;
  undefined4 uStack52;
  undefined4 uStack48;
  undefined4 uStack44;
  undefined4 local_28;
  undefined4 uStack36;
  undefined4 uStack32;
  undefined4 uStack28;
  undefined8 local_18;
  undefined4 local_10;
  
  <eframe::epi::NativeOptions_as_core::default::Default>::default(&local_98);
  local_38 = local_98;
  uStack52 = uStack148;
  uStack48 = uStack144;
  uStack44 = uStack140;
  local_28 = local_88;
  uStack36 = uStack132;
  uStack32 = uStack128;
  uStack28 = uStack124;
  local_18 = local_78;
  local_10 = local_70;
  local_6c = 1;
  local_68 = 0x43b4000044480000;
  local_42 = 0;
  eframe::run_native(&DAT_0048e706,0x1a,&local_98,1,
                     &PTR_drop_in_place<eguite_main_{{closure}}>_00725070,in_R9,local_54,local_4c);
  return;
}

eframeという見慣れないものが見えます。調べてみるとeguiというRustのGUIライブラリのものだということが分かりました。

eguiでは、eframe::Appトレイトを実装した構造体でレイアウトなどを定義するらしいので、それっぽいものがないかLabelやNamespacesを見てみました。

すると、eguite::Crackme::onclickという怪しい名前の関数が見つかりました。 この関数の呼び出し元周辺を見てみるとInvalid license.Successfully validated!などの文字列が見つかったので、eguite::Crackme::onclickはCHECKボタンを押されたときに呼び出される関数と考えて良さそうです。

eguite::Crackme::onclickは少し長い関数なので、先頭から少しずつ見ていきます。

bool eguite::Crackme::onclick(long param_1)
{
    uint *puVar1;
    byte bVar2;
    void *pvVar3;
    uint *puVar4;
    long lVar5;
    uint uVar6;
    undefined **ppuVar7;
    uint *puVar8;
    uint uVar9;
    uint *puVar10;
    uint *puVar11;
    uint *local_68;
    uint *local_60;
    undefined4 local_58;
    undefined4 uStack84;
    undefined4 uStack80;
    undefined4 uStack76;
    void *local_48;
    long local_40;
    undefined8 local_38;
    
    if (*(long *)(param_1 + 0x90) != 0x2b) {
        return false;
    }
    puVar11 = *(uint **)(param_1 + 0x80);
    if ((*(uint *)((long)puVar11 + 3) ^ 0x7b4e4f43 | *puVar11 ^ 0x43434553) != 0) {
        return false;
    }
    if (*(byte *)((long)puVar11 + 0x2a) != 0x7d) {
        return false;
    }
    ...

この部分から、次のことを考えました。 - 0x2bがフラグの長さっぽい - 0x43434553SECC0x7b4e4f43CON{だからフラグのprefixをチェックしている - 0x7d}だからフラグのsuffixをチェックしている

よって、param_1をテキストボックスに入力された文字列だと仮定すると、この関数でフラグのチェックを行っていることが確定しました。

フラグの長さprefix、suffixのチェックが終わると、次の処理に移ります。

...
puVar1 = (uint *)((long)puVar11 + 0x2b);
lVar5 = 0x13;
puVar4 = puVar11;
do {
    if (puVar4 == puVar1) goto LAB_0016027f;
    bVar2 = *(byte *)puVar4;
    if ((char)bVar2 < '\0') {
        if (bVar2 < 0xe0) {
            puVar4 = (uint *)((long)puVar4 + 2);
        }
        else {
            if (bVar2 < 0xf0) {
                puVar4 = (uint *)((long)puVar4 + 3);
        }
        else {
                if ((*(byte *)((long)puVar4 + 3) & 0x3f |
                   (*(byte *)((long)puVar4 + 2) & 0x3f) << 6 |
                   (*(byte *)((long)puVar4 + 1) & 0x3f) << 0xc | (bVar2 & 7) << 0x12) == 0x110000)
                goto LAB_0016027f;
                puVar4 = puVar4 + 1;
            }
        }
    }
    else {
        puVar4 = (uint *)((long)puVar4 + 1);
    }
    lVar5 = lVar5 + -1;
} while (lVar5 != 0);
if (puVar4 == puVar1) {
LAB_0016036f:
    ppuVar7 = &PTR_s_src/main.rsgetrandom_getrandom(_00725130;
}
else {
    bVar2 = *(byte *)puVar4;
    uVar9 = (uint)bVar2;
    if ((char)bVar2 < '\0') {
        uVar6 = bVar2 & 0x1f;
        uVar9 = *(byte *)((long)puVar4 + 1) & 0x3f;
    if (bVar2 < 0xe0) {
        uVar9 = uVar6 << 6 | uVar9;
    }
    else {
        uVar9 = *(byte *)((long)puVar4 + 2) & 0x3f | uVar9 << 6;
        if (bVar2 < 0xf0) {
            uVar9 = uVar9 | uVar6 << 0xc;
        }
        else {
            uVar9 = *(byte *)((long)puVar4 + 3) & 0x3f | uVar9 << 6 | (bVar2 & 7) << 0x12;
            if (uVar9 == 0x110000) goto LAB_0016036f;
        }
    }
}
if (uVar9 != 0x2d) {
      return false;
}
...

一見複雑そうに見えますが、if ((char)bVar2 < '\0')の部分は常にfalseになるため、次のように書き換えることができます。

puVar1 = (uint *)((long)puVar11 + 0x2b);
lVar5 = 0x13;
puVar4 = puVar11;
do {
    if (puVar4 == puVar1) goto LAB_0016027f;
    bVar2 = *(byte *)puVar4;
    puVar4 = (uint *)((long)puVar4 + 1);
    lVar5 = lVar5 + -1;
} while (lVar5 != 0);
if (puVar4 == puVar1) {
LAB_0016036f:
    ppuVar7 = &PTR_s_src/main.rsgetrandom_getrandom(_00725130;
}
else {
    bVar2 = *(byte *)puVar4;
    uVar9 = (uint)bVar2;
}
if (uVar9 != 0x2d) {
      return false;
}

処理が非常にシンプルになりました。どうやら、インデックスがlVar5のときの文字が、-かどうかチェックしているようです。

この処理のlVar5を0x1a, 0x21にしたときの処理が続いて、次の処理に移ります。

local_58 = 7;
uStack84 = 0;
uStack80 = 0xc;
uStack76 = 0;
local_68 = puVar11;
local_60 = puVar1;
<alloc::string::String_as_core::iter::traits::collect::FromIterator<char>>::from_iter
          (&local_48,&local_68);
pvVar3 = local_48;
core::num::<impl_u64>::from_str_radix(&local_68,local_48,local_38,0x10);
puVar4 = local_60;
if ((char)local_68 != '\0') {
    puVar4 = (uint *)0x0;
}
if (local_40 != 0) {
    __rust_dealloc(pvVar3);
}
local_58 = 0x14;
uStack84 = 0;
uStack80 = 6;
uStack76 = 0;
local_68 = puVar11;
local_60 = puVar1;
<alloc::string::String_as_core::iter::traits::collect::FromIterator<char>>::from_iter
          (&local_48,&local_68);
pvVar3 = local_48;
core::num::<impl_u64>::from_str_radix(&local_68,local_48,local_38,0x10);
puVar10 = local_60;
if ((char)local_68 != '\0') {
    puVar10 = (uint *)0x0;
}
if (local_40 != 0) {
    __rust_dealloc(pvVar3);
}
local_58 = 0x1b;
uStack84 = 0;
uStack80 = 6;
uStack76 = 0;
local_68 = puVar11;
local_60 = puVar1;
<alloc::string::String_as_core::iter::traits::collect::FromIterator<char>>::from_iter
          (&local_48,&local_68);
pvVar3 = local_48;
core::num::<impl_u64>::from_str_radix(&local_68,local_48,local_38,0x10);
puVar8 = local_60;
if ((char)local_68 != '\0') {
    puVar8 = (uint *)0x0;
}
if (local_40 != 0) {
    __rust_dealloc(pvVar3);
}
local_58 = 0x22;
uStack84 = 0;
uStack80 = 8;
uStack76 = 0;
local_68 = puVar11;
local_60 = puVar1;
<alloc::string::String_as_core::iter::traits::collect::FromIterator<char>>::from_iter
          (&local_48,&local_68);
core::num::<impl_u64>::from_str_radix(&local_68,local_48,local_38,0x10);
puVar11 = local_60;
if ((char)local_68 != '\0') {
    puVar11 = (uint *)0x0;
}
if (local_40 != 0) {
    __rust_dealloc(local_48);
}

この部分では、local_58からuStack80文字分取り出して16進数文字列を整数値に変換する処理を行っているようです。Pythonで書き直すと次のようになります。

puVar4 = int(input_str[0x7:0x7+0xc], 16)
puVar7 = int(input_str[0x14:0x14+6], 16)
puVar8 = int(input_str[0x1b:0x1b+6], 16)
puVar11 = int(input_str[0x22:0x22+8], 16)

最後に、この変数たちを使ってフラグのチェックを行う処理に移ります。

if ((byte *)((long)puVar4 + (long)puVar10) != (byte *)0x8b228bf35f6a) {
    return false;
}
if ((byte *)((long)puVar8 + (long)puVar10) != (byte *)0xe78241) {
    return false;
}
if ((byte *)((long)puVar11 + (long)puVar8) == (byte *)0xfa4c1a9f) {
    if ((byte *)((long)puVar4 + (long)puVar11) == (byte *)0x8b238557f7c8) {
        return ((ulong)puVar8 ^ (ulong)puVar10 ^ (ulong)puVar11) == 0xf9686f4d;
    }
    return false;
}

これまでの情報をまとめると、

  • フラグの19, 26, 33文字目は-
  • フラグの7~19, 20~25, 27~32, 34~42文字の部分はは16進数文字列
  • フラグの長さは43文字

となるため、フラグはSECCON{A-B-C-D}(A, B, C, Dはそれぞれ16進数文字列)という形式になっていることが分かりました。

最後のチェック部分より、以下の式を満たすABCDを求めることができればフラグが手に入ります。

A + B == 0x8b228bf35f6a
C + B == 0xe78241
D + C == 0xfa3c1a9f
A + D == 0x8b238557f7c8
B ^ C ^ D == 0xf9686f4d

この条件式を満たすような値をz3に求めてもらうことで、無事フラグを入手できました。

import z3

s = z3.Solver()

a = z3.BitVec('a', 48)
b = z3.BitVec('b', 24)
c = z3.BitVec('c', 24)
d = z3.BitVec('d', 32)

b_ext = z3.ZeroExt(24, b)
c_ext = z3.ZeroExt(24, c)
d_ext = z3.ZeroExt(16, d)

s.add(a + b_ext == 0x8b228bf35f6a)
s.add(c_ext + b_ext == 0xe78241)
s.add(d_ext + c_ext == 0xfa4c1a9f)
s.add(a + d_ext == 0x8b238557f7c8)
s.add(b_ext ^ c_ext ^ d_ext == 0xf9686f4d)
print(s.check())

model = s.model()
parts = [f"{model[key].as_long():x}"for key in [a, b, c, d]]
flag = "SECCON{" + "-".join(parts) + "}"
print(flag)
$ python3 solve.py
sat
SECCON{8b228b98e458-5a7b12-8d072f-f9bf1370}

感想

solve数が多い問題しか解けなかったことや、時間内にDoroboHを解ききれなかったという反省点がありますが、非常に楽しいCTFでした。

今回のCTFでは、問題に関係ない部分を解析していたり、動的解析をすれば良いのに静的解析に力を入れすぎて時間をかけてしまうことが多かったので、もっと効率よくrevできるよう日頃から鍛錬を積み重ねていきたいです。