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をとった後の文字列とuStack60
やuStack64
などでxorをとり、一致するならCorrect!
と表示されるようになっています。
よって、Welcome to SECCON 2022
とuStack60
などで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_internal
やllvm
などが見えるので、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
がフラグの長さっぽい
- 0x43434553
はSECC
、0x7b4e4f43
はCON{
だからフラグの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できるよう日頃から鍛錬を積み重ねていきたいです。