SECCON Beginners CTF 2022 自分で解いたところだけ write-up

なんか内輪の Discord サーバーでやるぞって投稿があったので、チームに入って解けそうなものだけ解いた。基本的に腕力で殴っていたのであまり参考にはならないかもしれない。チームには後から入ったので一部問題は既にチームメイトが解いていた。最終チーム順位は26位。CTF自体はたぶん数年ぶり。

チームメンバーの write-up

scrapbox.io

misc

misc / ultra_super_miracle_validator

なんかSATソルバーがうんぬんらしいが全然知らなかったので手で yara ファイルを紐解いた。@428rinsuki update_name SATソルバー

さすがに読みやすくするPythonスクリプトは書いた。

読みやすくするPythonスクリプト

import re

variables = {}

def d(x):
    try:
        s = bytes.fromhex(x[2]).decode("utf-8")
        variables[x[1]] = s
        return x[0] + " # " + s
    except:
        variables[x[1]] = "hex_" + x[2].replace(" ", "")
        return x[0]

rule = open("rule.yara", "r").read()
rule = re.sub(r"(\$x\d+) = \{(([0-9A-Fa-f]{2} )*[0-9A-Fa-f]{2})\}", d, rule)
rule = re.sub(r"(x\d+)(\)|\n)", r"\1 \2", rule)
for k, v in variables.items():
    rule = rule.replace(k + " ", k + "_" + v + " ")

open("rule.yara.readable.txt", "w").write(rule)

紐解いた後

rule MalElf {
    meta:
        description = "Malicious ELF binary"

    strings:
        $x1_らせん階段 = {e3 82 89 e3 81 9b e3 82 93 e9 9a 8e e6 ae b5} # らせん階段
        $banned_x2_カブト虫 = {e3 82 ab e3 83 96 e3 83 88 e8 99 ab} # カブト虫
        $x3_廃墟の街 = {e5 bb 83 e5 a2 9f e3 81 ae e8 a1 97} # 廃墟の街
        $x4_イチジクのタルト = {e3 82 a4 e3 83 81 e3 82 b8 e3 82 af e3 81 ae e3 82 bf e3 83 ab e3 83 88} # イチジクのタルト
        $x5_ドロローサへの道 = {e3 83 89 e3 83 ad e3 83 ad e3 83 bc e3 82 b5 e3 81 b8 e3 81 ae e9 81 93} # ドロローサへの道
        $x6_特異点 = {e7 89 b9 e7 95 b0 e7 82 b9} # 特異点
        $banned_x7_ジョット = {e3 82 b8 e3 83 a7 e3 83 83 e3 83 88} # ジョット
        $x8_天使 = {e5 a4 a9 e4 bd bf} # 天使
        $x9_紫陽花 = {e7 b4 ab e9 99 bd e8 8a b1} # 紫陽花
        $banned_x10_秘密の皇帝 = {e7 a7 98 e5 af 86 e3 81 ae e7 9a 87 e5 b8 9d} # 秘密の皇帝
        $banned_x11_hex_82e782b982f18a4b9269 = {82 e7 82 b9 82 f1 8a 4b 92 69}
        $x12_hex_834a83758367928e = {83 4a 83 75 83 67 92 8e}
        $x13_hex_94709ad082cc8a58 = {94 70 9a d0 82 cc 8a 58}
        $x14_hex_834383608357834e82cc835e838b8367 = {83 43 83 60 83 57 83 4e 82 cc 83 5e 83 8b 83 67}
        $banned_x15_hex_8368838d838d815b835482d682cc93b9 = {83 68 83 8d 83 8d 81 5b 83 54 82 d6 82 cc 93 b9}
        $x16_hex_93c188d9935f = {93 c1 88 d9 93 5f}
        $x17_hex_8357838783628367 = {83 57 83 87 83 62 83 67}
        $x18_hex_93568e67 = {93 56 8e 67}
        $x19_hex_8e87977a89d4 = {8e 87 97 7a 89 d4}
        $x20_hex_94e996a782cc8d6392e9 = {94 e9 96 a7 82 cc 8d 63 92 e9}
        $x21_hex_3089305b3093968e6bb5 = {30 89 30 5b 30 93 96 8e 6b b5}
        $banned_x22_0K0v = {30 4b 30 76} # 0K0v
        $x23_hex_5ec3589f306e8857 = {5e c3 58 9f 30 6e 88 57}
        $x24_hex_30a430c130b830af306e30bf30eb30c8 = {30 a4 30 c1 30 b8 30 af 30 6e 30 bf 30 eb 30 c8}
        $x25_hex_30c930ed30ed30fc30b53078306e9053 = {30 c9 30 ed 30 ed 30 fc 30 b5 30 78 30 6e 90 53}
        $x26_hex_7279757070b9 = {72 79 75 70 70 b9}
        $banned_x27_hex_30b830e730c330c8 = {30 b8 30 e7 30 c3 30 c8}
        $x28_Y)O = {59 29 4f 7f} # Y)O
        $x29_hex_7d2b967d82b1 = {7d 2b 96 7d 82 b1}
        $x30_hex_79d85bc6306e76875e1d = {79 d8 5b c6 30 6e 76 87 5e 1d}
        $x31_+MIk-+MFs-+MJM-+lo4- = {2b 4d 49 6b 2d 2b 4d 46 73 2d 2b 4d 4a 4d 2d 2b 6c 6f 34 2d} # +MIk-+MFs-+MJM-+lo4-    
        $x32_+MEs-+MH = {2b 4d 45 73 2d 2b 4d 48} # +MEs-+MH
        $x33_+XsM-+WJ8-+MG4-+ = {2b 58 73 4d 2d 2b 57 4a 38 2d 2b 4d 47 34 2d 2b} # +XsM-+WJ8-+MG4-+
        $x34_+MKQ-+MME-+MLg-+MK8-+MG4-+ML8-+M = {2b 4d 4b 51 2d 2b 4d 4d 45 2d 2b 4d 4c 67 2d 2b 4d 4b 38 2d 2b 4d 47 34 2d 2b 4d 4c 38 2d 2b 4d} # +MKQ-+MME-+MLg-+MK8-+MG4-+ML8-+M
        $x35_+MMk-+MO0-+MO0-+MPw-+MLU-+MHg-+M = {2b 4d 4d 6b 2d 2b 4d 4f 30 2d 2b 4d 4f 30 2d 2b 4d 50 77 2d 2b 4d 4c 55 2d 2b 4d 48 67 2d 2b 4d} # +MMk-+MO0-+MO0-+MPw-+MLU-+MHg-+M
        $x36_+cnk-+dXA-+c = {2b 63 6e 6b 2d 2b 64 58 41 2d 2b 63} # +cnk-+dXA-+c
        $x37_+MLg-+MOc-+MMM-+ = {2b 4d 4c 67 2d 2b 4d 4f 63 2d 2b 4d 4d 4d 2d 2b} # +MLg-+MOc-+MMM-+
        $x38_+WSk-+T3 = {2b 57 53 6b 2d 2b 54 33} # +WSk-+T3
        $x39_+fSs-+ln0-+g = {2b 66 53 73 2d 2b 6c 6e 30 2d 2b 67} # +fSs-+ln0-+g
        $banned_x40_+edg-+W8Y-+MG4-+doc- = {2b 65 64 67 2d 2b 57 38 59 2d 2b 4d 47 34 2d 2b 64 6f 63 2d} # +edg-+W8Y-+MG4-+doc-

        
    condition:
        not (
            # solved らせん階段
            ($x1_らせん階段 or $x6_特異点 or $x12_hex_834a83758367928e or not $x21_hex_3089305b3093968e6bb5 or $x32_+MEs-+MH )
            #solved 廃墟の街
            and ($x3_廃墟の街 or $x5_ドロローサへの道 or not $banned_x11_hex_82e782b982f18a4b9269 or $x24_hex_30a430c130b830af306e30bf30eb30c8 or $x35_+MMk-+MO0-+MO0-+MPw-+MLU-+MHg-+M )
            #solved 紫陽花
            and (not $x3_廃墟の街 or $x31_+MIk-+MFs-+MJM-+lo4- or $banned_x40_+edg-+W8Y-+MG4-+doc- or $x9_紫陽花 or $banned_x27_hex_30b830e730c330c8 )
            #solved イチジクのタルト
            and ($x4_イチジクのタルト or $x8_天使 or $banned_x10_秘密の皇帝 or $x29_hex_7d2b967d82b1 or $banned_x40_+edg-+W8Y-+MG4-+doc- )
            #solved イチジクのタルト
            and ($x4_イチジクのタルト or $banned_x7_ジョット or $banned_x11_hex_82e782b982f18a4b9269 or $x25_hex_30c930ed30ed30fc30b53078306e9053 or not $x36_+cnk-+dXA-+c )
            #solved 天使
            and ($x8_天使 or $x14_hex_834383608357834e82cc835e838b8367 or $x18_hex_93568e67 or $x21_hex_3089305b3093968e6bb5 or $x38_+WSk-+T3 )
            #solved +MMk-+MO0-+MO0-+MPw-+MLU-+MHg-+M
            and ($x12_hex_834a83758367928e or $banned_x15_hex_8368838d838d815b835482d682cc93b9 or not $x20_hex_94e996a782cc8d6392e9 or $x30_hex_79d85bc6306e76875e1d or $x35_+MMk-+MO0-+MO0-+MPw-+MLU-+MHg-+M )
            #solved +fSs-+ln0-+g
            and ($x19_hex_8e87977a89d4 or $x21_hex_3089305b3093968e6bb5 or not $x32_+MEs-+MH or $x33_+XsM-+WJ8-+MG4-+ or $x39_+fSs-+ln0-+g )
            #solved +MLg-+MOc-+MMM-+
            and ($banned_x2_カブト虫 or $x37_+MLg-+MOc-+MMM-+ or $x19_hex_8e87977a89d4 or not $x23_hex_5ec3589f306e8857 )
            #solved not $x5_ドロローサへの道
            and (not $x5_ドロローサへの道 or $x14_hex_834383608357834e82cc835e838b8367 or $x23_hex_5ec3589f306e8857 or $x30_hex_79d85bc6306e76875e1d )
            #solved not $x5_ドロローサへの道
            and (not $x5_ドロローサへの道 or $x8_天使 or $x18_hex_93568e67 or $x23_hex_5ec3589f306e8857 )
            #solved イチジクのタルト
            and ($x33_+XsM-+WJ8-+MG4-+ or $banned_x22_0K0v or $x4_イチジクのタルト or $x38_+WSk-+T3 )
            #solved +fSs-+ln0-+g
            and ($banned_x2_カブト虫 or $x20_hex_94e996a782cc8d6392e9 or $x39_+fSs-+ln0-+g )
            #solved 廃墟の街
            and ($x3_廃墟の街 or $banned_x15_hex_8368838d838d815b835482d682cc93b9 or not $x30_hex_79d85bc6306e76875e1d )
            #solved 特異点
            and ($x6_特異点 or not $x17_hex_8357838783628367 or $x30_hex_79d85bc6306e76875e1d )
            #solved 天使
            and ($x8_天使 or $x29_hex_7d2b967d82b1 or not $x21_hex_3089305b3093968e6bb5 )
            #solved らせん階段
            and (not $x16_hex_93c188d9935f or $x1_らせん階段 or $x29_hex_7d2b967d82b1 )
            #solved not $x5_ドロローサへの道
            and ($x20_hex_94e996a782cc8d6392e9 or $banned_x10_秘密の皇帝 or not $x5_ドロローサへの道 )
            #solved not $x13_hex_94709ad082cc8a58
            and (not $x13_hex_94709ad082cc8a58 or $x25_hex_30c930ed30ed30fc30b53078306e9053 )
            #solved Y)O
            and ($x21_hex_3089305b3093968e6bb5 or $x28_Y)O or $x30_hex_79d85bc6306e76875e1d )
            #
            and not $banned_x2_カブト虫 
            #solved 廃墟の街
            and $x3_廃墟の街 
            #
            and not $banned_x7_ジョット 
            #
            and not $banned_x10_秘密の皇帝 
            #
            and not $banned_x11_hex_82e782b982f18a4b9269 
            #solved 834383608357834e82cc835e838b8367
            and $x14_hex_834383608357834e82cc835e838b8367 
            #
            and not $banned_x15_hex_8368838d838d815b835482d682cc93b9 
            #
            and not $banned_x22_0K0v 
            #solved \x72\x79\x75\x70\x70\xb9
            and $x26_hex_7279757070b9 
            #
            and not $banned_x27_hex_30b830e730c330c8 
            #
            and $x34_+MKQ-+MME-+MLg-+MK8-+MG4-+ML8-+M 
            #
            and $x36_+cnk-+dXA-+c 
            # solved +MLg-+MOc-+MMM-+
            and $x37_+MLg-+MOc-+MMM-+ 
            #
            and not $banned_x40_+edg-+W8Y-+MG4-+doc- 
        )
}

それでこういうソースコードにして:

Cソースコード

int main(){
    puts("らせん階段");
    puts("廃墟の街");
    puts("紫陽花");
    puts("イチジクのタルト");
    puts("天使");
    puts("+MMk-+MO0-+MO0-+MPw-+MLU-+MHg-+M");
    puts("+fSs-+ln0-+g");
    puts("+MLg-+MOc-+MMM-+");
    puts("Y)O");
    puts("\x83\x43\x83\x60\x83\x57\x83\x4e\x82\xcc\x83\x5e\x83\x8b\x83\x67");
    puts("\x72\x79\x75\x70\x70\xb9");
    puts("+MKQ-+MME-+MLg-+MK8-+MG4-+ML8-+M");
    puts("+cnk-+dXA-+c");
    system(<<<command_string>>>);
    return 0;
}

あとCのソースを1行にする必要がある。cat source.c | tr -d '\n' | sed -e 's/ //g' > src1line.c みたいな感じで。

適当にlsしたら flag.txt があったので cat flag.txt しておしまい。

misc / hitchhike4b

Python のモジュールはカレントディレクトリにある場合拡張子抜きのファイル名がそのままモジュール名になるので、__main__ が指すモジュールの名前を知ればフラグ後半がわかる。

__main__ を入れるとflag前半と __main__ の実態のファイル名が出る。ので、ファイル名から拡張子を抜いたモジュール名 app_... のヘルプを表示して終了。気づいてしまえば簡単。

reversing

基本的に全部デバッガは使わず Ghidra で逆コンパイルしてCコンパイラが通る程度まで型を付けて都合の良いプログラム (例えば暗号化処理が XOR だったら 暗号文を食わせるだけの main を書くとか) を書くというろくでもない手法で済ませた。というか M1 Mac とその上のVM内で解いたので x86_64 Windows/Linux 上で動くデバッグ環境を用意するのがだるかった。Ghidra は偉大。

reversing / WinTLS

TLSは Thread Local Storage らしい。が結局 Ghidra で中身読んで処理をPythonで再現して解いた。Ghidra はスタックに積まれている固定長配列 char[64] のような型を認識しないがちなのでそういうのが出てくると困惑しがち。

s = "tfb%s$T9NvFyroLh@89a9yoC3rPy&3b}"
ss = "c4{fAPu8#FHh2+0cyo8$SWJH3a8X"
ii = 0
iii = 0
for i in range(256):
    if (i % 3 != 0) and (i % 5) != 0:
        print(s[iii], end="")
        iii += 1
    if (i%3) == 0 or (i%5) == 0:
        print(ss[ii], end="")
        ii += 1

reversing / ransom

keyがランタイム時に生成されているがkeyは適当に生のTCPで送っているのでpcapファイルを見ればよい。暗号化処理は結局XORなので鍵を固定させて暗号文を再度暗号化させるプログラムを書いた (暗号化部分はまるまる Ghidra でリバースしたのを持ってきた)。Ghidra の bytechar ではなく unsigned char にするべきだということに気づかずちょっと時間を溶かした。

Cソースコード

#include <stdio.h>
#include <string.h>

#define uint unsigned int
#define ulong unsigned long
#define byte unsigned char

void swap(char *param_1,char *param_2)

{
  char cVar1;
  
  cVar1 = *param_1;
  *param_1 = *param_2;
  *param_2 = cVar1;
  return;
}
void generateTable(char *key,char *table)

{
  uint uVar1;
  size_t keyLength;
  int iVar2;
  int v;
  int i;
  int j;
  
  keyLength = strlen(key);
  v = 0;
  for (i = 0; i < 0x100; i = i + 1) {
    table[i] = (char)i;
  }
  printf("a\n");
  for (j = 0; j < 0x100; j = j + 1) {
    iVar2 = (uint)(byte)table[j] + v + (int)key[j % (int)keyLength];
    uVar1 = (uint)(iVar2 >> 0x1f) >> 0x18;
    v = (iVar2 + uVar1 & 0xff) - uVar1;
    swap(table + j,table + v);
  }
  return;
}

void encryptIt(char *table,char *plain,char *encrypted)

{
  size_t plainLength;
  uint i8bit;
  uint local_20;
  ulong i;
  
  i8bit = 0;
  local_20 = 0;
  i = 0;
  plainLength = strlen(plain);
  for (; i < plainLength; i = i + 1) {
    i8bit = i8bit + 1 & 0xff;
    local_20 = (byte)table[(int)i8bit] + local_20 & 0xff;
    swap(table + (int)i8bit,table + (int)local_20);
    encrypted[i] = plain[i] ^ table[(byte)(table[(int)local_20] + table[(int)i8bit])];
  }
  return;
}

int main() {
                  char* key = "rgUAvvyfyApNPEYg";  // from pcap
                  char plain[] = {0x2b, 0xa9, 0xf3, 0x6f, 0xa2, 0x2e, 0xcd, 0xf3, 0x78, 0xcc, 0xb7, 0xa0, 0xde, 0x6d, 0xb1, 0xd4, 0x24, 0x3c, 0x8a, 0x89, 0xa3, 0xce, 0xab, 0x30, 0x7f, 0xc2, 0xb9, 0x0c, 0xb9, 0xf4, 0xe7, 0xda, 0x25, 0xcd, 0xfc, 0x4e, 0xc7, 0x9e, 0x7e, 0x43, 0x2b, 0x3b, 0xdc, 0x09, 0x80, 0x96, 0x95, 0xf6, 0x76, 0x10, 0};
                  char encrypted[200];
  char table [264];                                            
  generateTable(key,table);
  encryptIt(table,plain,encrypted);
  fputs(encrypted, stdout);
return 0;
}

reversing / please_not_debug_me

デバッグするな了解!じゃあ静的解析で行きます!

stage1は memfd_create で作ったFILE*に内蔵したxorred ELFファイルをXORを解いて書き込んで fexecve で実行している。

このバイナリの部分をひっこ抜いて適当に自分でXORを解いて stage2 の elf を割り出してくるが、なんか Ghidra がELFファイルの読み込みで死ぬ。調べてみるとどうやら section header の箇所が消されているらしく (アンチ解析としてまあまあある手法らしい)、適当に Hex Fiend の ELF Binary Template を利用して section header なんてないという値に書き換えてどうにかした…はず。

あとは Ghidra で読むだけ。control flow obfuscation っぽいのが入っているがまあこれくらいだったら普通に読める。

web

web / Ironhand

(初出時 reversing になっていたのを修正)

なんか最初JWTだしalg=noneで抜けるのかなと思っていたが全然違った。/static/ のハンドラにディレクトリトラバーサルがある (というのはチームメンバーの @otofune418 が見つけた)。

がなんか素直に ..%2F..%2F とかやるとnginxに 400 Bad Request って言われる。どんな設定しとんじゃいと見に行ったら merge_slashes というのがあったのでスラッシュ二重にしたら行けるかな?と思って試したら行けたので /static/..%2F%2F..%2Fproc%2Fself%2Fenviron を読んでJWTのシークレットを抜いて https://jwt.io/ で適当に署名付けて完。/proc/self/environ は改行で区切るとかじゃなく \x00 で区切っていることに注意。

400 Bad Request はよくわからん。WAF想定?

crypto

crypto / Command

encrypt command で 32bytes のバイナリ(をhexにしたもの)が降ってきて、前16bytesはIV、後16bytesは暗号文。

AES の CBCモードは 前の暗号文 (ない場合はIV) を復号した結果にXORする。ので、暗号文とその暗号文を生成するのに使った平文とIVが既知なら、keyを知らなくてもIVを変えることで任意の暗号文にできる(らしい) (1ブロックで済まないとたぶんもっと面倒くさい…というかできない?)。

暗号文はそのままにして、IV を 暗号文を作った時のIV ^ (暗号文を作った時の平文 ^ 欲しい結果の平文) で投げる。paddingのことを忘れずに。

あとがき

pwnable 系は普段全くやっていなかったこともあり全く検討も付かなかったので人に任せていたが、とはいえ他のメンバーもそこまで詳しい人がいるわけではなかったのであまり点数が取れなかったのが心残り (じゃあ reversing は普段してるのか?) 。

cryptoも数学が絡みそうなところは検討が付かなかった。ちゃんと数学をやったほうがいいのかもしれん。

というわけで、最後にこの曲を貼っておきます。

(魔王城でおやすみ、曲で知ってちょっと見たいなという気持ちがあるのだが、Amazon Prime Video はプライム対象ではなくレンタルだし、dアニメニコニコ支店からは以前なくなってしまったらしいし、というわけで見るすべがない。とはいえどこかをわざわざ新規に契約するほど見たいわけでもないし…。どちらかに入ったら教えてください)