kdnakt blog

hello there.

picoCTF 2024参加レポート&Writeups

3/13-27に開催されたpicoCTF 2024に参加した。

https://play.picoctf.org/events/73

[結果]

チーム成績は2625ポイントで1084/6957位だった。

https://play.picoctf.org/teams/9731

個人成績は...正確なところはよくわからないが、2525ポイントで1150/6957位くらい、のはず。

https://play.picoctf.org/users/kdnakt

2023年に参加したCTFでは上位25~33%くらいの成績だったので、上位17%に入った今回はだいぶ良かったのではなかろうか。2023年にはおそらく解けなかったであろう300ポイント問題もいくつか解けたので満足。

[General Skills]

▼Super SSH (25 points)

sshサーバーに指定されたポート、IDで接続し、パスワードを入力するとフラグが出力される。

▼Commitment Issues (50 points)

git log -pで変更前のファイルにフラグが書かれている。

▼Time Machine (50 points)

git logでコミットコメントにフラグが書かれている。

▼Blame Game (75 points)

git logでコミットユーザー名がフラグになっている。

▼Collaborative Development (75 points)

git branchで出てくるブランチに1つずつスイッチするとフラグの一部が出てくるので結合するだけ。

▼binhexa (100 points)

Binary Number 1: 00000101
Binary Number 2: 10000010
Question 1/6:
Operation 1: ‘&’
Perform the operation on Binary Number 1&2.
Enter the binary result:

こんな感じで、ビット演算の問題が6問出題されるので答えていく。最後に「Enter the results of the last operation in hexadecimal:」と16進数に直す問題が出てくるのでこれに正解するとフラグが得られる。

▼binary search (100 points)

sshで接続すると1から1000までの数値あてを挑まれるので、ひたすら2分探索で当てていくとフラグが得られる。

Welcome to the Binary Search Game!
I'm thinking of a number between 1 and 1000.
Enter your guess: 500
Lower! Try again.
Enter your guess: 250
Lower! Try again.
Enter your guess: 125
Lower! Try again.
Enter your guess: 63
Higher! Try again.
Enter your guess: 94
Higher! Try again.
Enter your guess: 110
Higher! Try again.
Enter your guess: 118
Lower! Try again.
Enter your guess: 114
Lower! Try again.
Enter your guess: 112
Lower! Try again.
Enter your guess: 111
Congratulations! You guessed the correct number: 111
Here's your flag: picoCTF{...}

▼endianness (200 points)

ncコマンドで接続すると、文字列が与えられてリトルエンディアン、ビッグエンディアンでの入力を順に求められるので、以下のようにアスキーコードで回答していくだけ。

Welcome to the Endian CTF!
You need to find both the little endian and big endian representations of a word.
If you get both correct, you will receive the flag.
Word: pypgp
Enter the Little Endian representation: 7067707970
Correct Little Endian representation!
Enter the Big Endian representation: 

[Binary Exploitation]

▼format string 0 (50 points)

以下のような感じでセグメンテーションフォールトを発生させるとフラグが出力されるようになっている。

char flag[FLAGSIZE];

void sigsegv_handler(int sig) {
    printf("\n%s\n", flag);
    fflush(stdout);
    exit(1);
}

int main(int argc, char **argv){
    FILE *f = fopen("flag.txt", "r");
    if (f == NULL) {
        printf("%s %s", "Please create 'flag.txt' in this directory with your",
                        "own debugging flag.\n");
        exit(0);
    }

    fgets(flag, FLAGSIZE, f);
    signal(SIGSEGV, sigsegv_handler);

    (略)

}

main()関数の中ではserve_patrick()が呼ばれている。Gr%114d_Cheeseを入力すると、printf(choice1)が呼ばれたときに、114桁の数字が出力されたことになるので、if (count > 2 * BUFSIZE)を通過できる。

void serve_patrick() {
    printf("%s %s\n%s\n%s %s\n%s",
            "Welcome to our newly-opened burger place Pico 'n Patty!",
            "Can you help the picky customers find their favorite burger?",
            "Here comes the first customer Patrick who wants a giant bite.",
            "Please choose from the following burgers:",
            "Breakf@st_Burger, Gr%114d_Cheese, Bac0n_D3luxe",
            "Enter your recommendation: ");
    fflush(stdout);

    char choice1[BUFSIZE];
    scanf("%s", choice1);
    char *menu1[3] = {"Breakf@st_Burger", "Gr%114d_Cheese", "Bac0n_D3luxe"};
    if (!on_menu(choice1, menu1, 3)) {
        printf("%s", "There is no such burger yet!\n");
        fflush(stdout);
    } else {
        int count = printf(choice1);
        if (count > 2 * BUFSIZE) {
            serve_bob();
        } else {
            printf("%s\n%s\n",
                    "Patrick is still hungry!",
                    "Try to serve him something of larger size!");
            fflush(stdout);
        }
    }
}

serve_bob()はこうなっている。入力が求められるので、そこでCla%sic_Che%s%steakを入力するか、32文字以上入力すると、スタックにあるフラグ文字列が出力された。

void serve_bob() {
    printf("\n%s %s\n%s %s\n%s %s\n%s",
            "Good job! Patrick is happy!",
            "Now can you serve the second customer?",
            "Sponge Bob wants something outrageous that would break the shop",
            "(better be served quick before the shop owner kicks you out!)",
            "Please choose from the following burgers:",
            "Pe%to_Portobello, $outhwest_Burger, Cla%sic_Che%s%steak",
            "Enter your recommendation: ");
    fflush(stdout);

    char choice2[BUFSIZE];
    scanf("%s", choice2);
    char *menu2[3] = {"Pe%to_Portobello", "$outhwest_Burger", "Cla%sic_Che%s%steak"};
    if (!on_menu(choice2, menu2, 3)) {
        printf("%s", "There is no such burger yet!\n");
        fflush(stdout);
    } else {
        printf(choice2);
        fflush(stdout);
    }
}

▼heap 0 (50 points)

こんな感じのコードが与えられる。check_win()の呼び出し時に、safe_varがbico以外の値になっていればOK。write_buffer()を呼び出してinput_dataに33文字以上書き込むと、safe_varが33文字目以降で上書きされてフラグが得られる。

void init() {
    (省略)
    input_data = malloc(INPUT_DATA_SIZE); // INPUT_DATA_SIZE = 5
    strncpy(input_data, "pico", INPUT_DATA_SIZE);
    safe_var = malloc(SAFE_VAR_SIZE);
    strncpy(safe_var, "bico", SAFE_VAR_SIZE);
}

void write_buffer() {
    printf("Data for buffer: ");
    fflush(stdout);
    scanf("%s", input_data);
}
void check_win() {
    if (strcmp(safe_var, "bico") != 0) {
        printf("\nYOU WIN\n");
        (省略)
    }

▼heap1 (100 points)

heap 0と同じ戦略でいけるので省略。

▼heap 3 (200 points)

以前のCTF勉強会でやったuse after freeの類似問題。

// Create struct
typedef struct {
  char a[10];
  char b[10];
  char c[10];
  char flag[5];
} object;
int num_allocs;
object *x;


void check_win() {
  if(!strcmp(x->flag, "pico")) {
    printf("YOU WIN!!11!!\n");

 (省略)
  }
}
void init() {
    printf("\nfreed but still in use\nnow memory untracked\ndo you smell the bug?\n");
    fflush(stdout);
    x = malloc(sizeof(object));
    strncpy(x->flag, "bico", 5);
}
void alloc_object() {
    printf("Size of object allocation: ");
    fflush(stdout);
    int size = 0;
    scanf("%d", &size);
    char* alloc = malloc(size);
    printf("Data for flag: ");
    fflush(stdout);
    scanf("%s", alloc);
}
void free_memory() {
    free(x);
}

free_memory()を呼び出してxを解放した後に、alloc_object()を呼び出すと、解放したばかりのメモリが割り当てられる。

objectの定義が10バイトのフィールド3つ+flagなので、Size of object allocationで34文字を指定して、任意の30文字に続けてpicoと入力するとx->flagが"pico"となるのでフラグが得られる。

問題の命名的にheap 2の方が簡単っぽかったんだけど、こちらは歯が立たずorz

[Forensics]

▼Scan Surprise (50 points)

QRコードが書かれたflag.pngが渡されるので、読み取るだけ。

▼Verify (50 points)

decrypt.shと同階層にfilesディレクトリがあって、大量のファイルが置かれている。decrypt.shはこれを復号するためのもののようなので、試しに1つ実験してみる。

$ ./decrypt.sh files/02kLdPvr 
bad magic number
Error: Failed to decrypt 'files/02kLdPvr'. This flag is fake! Keep looking!

1個ずつやるのはだるいのでxargsコマンドを使って以下のようにすべてのファイルを実行するとフラグが見つかる。

$ ls files | xargs -IXXX ./decrypt.sh files/XXX | grep pico

▼endianness-v2 (300 points)

謎のバイナリファイルが与えられる。先頭の数バイトを見ると、

E0 FF D8 FF 46 4A ....

となっている。FF D8 FF E0...で始まるのがJPEGのはずなので、4バイトずつ逆順になっていると想像できる。

endianness-v2という名前で課題のバイナリファイルを保存しておく。これを1バイトずつ読み込んで、4バイトずつ入れ替えてendianness-v2.jpgというファイルに書き出すプログラムを以下のように実装して実行する(時間がなかったのでChatGPTに書かせた)。変換後のjpgファイルを開くとフラグが描かれている。

byte_array = []

with open('endianness-v2', 'rb') as file:
    byte = file.read(1)
    while byte:
        byte_array.append(byte)
        byte = file.read(1)

result = [0] * len(byte_array)

for index, value in enumerate(byte_array):
    r = int(index / 4)
    new_index = (3 - (index % 4)) + r * 4
    if len(byte_array) < new_index:
        break
    result[new_index] = value

with open('endianness-v2.jpg', 'wb') as file:
    for byte in result:
        file.write(byte)

▼Blast from the past (300 points)

画像ファイルが与えられ、あらゆるタイムスタンプを1970:01:01 00:00:00.001+00:00に変換しろという課題。バイナリエディタで開くと、2023:11:20 15:46:23といったような日付っぽい文字列が何箇所か見えるので、これをちまちまと指定された日付に修正していく。

あとは、ファイルの末尾付近にImage_UTC_Dataという文字列に続いてUnixタイムスタンプっぽい数値がある。これは全部0にして指定されたサイトにアップロードするとエラーになるので、0000000000001と最後だけ1に修正するとフラグが得られる。

[Web Exploitation]

Bookmarklet (50 points)

以下のようなコードが与えられるのでfunction部分を開発者ツールで実行すると復号されたフラグが表示される。

        javascript:(function() {
            var encryptedFlag = "àÒÆަȬëÙ£ÖÓÚåÛÑ¢ÕÓÔÅÐÙí";
            var key = "picoctf";
            var decryptedFlag = "";
            for (var i = 0; i < encryptedFlag.length; i++) {
                decryptedFlag += String.fromCharCode*1
        cipher_text += encrypted_char
    return cipher_text

なので、処理を逆にした以下のような関数を実装して復号するとフラグが得られる。

def dynamic_xor_decrypt(cipher_text, text_key):
    plain_text = ""
    key_length = len(text_key)
    for i, char in enumerate(cipher_text): # 列挙する順番を変更
        key_char = text_key[i % key_length]
        decrypted_char = chr(char ^ ord(key_char)) 
        plain_text = decrypted_char + plain_text # 結合順を変更
    return plain_text

*1:encryptedFlag.charCodeAt(i) - key.charCodeAt(i % key.length) + 256) % 256);
            }
            alert(decryptedFlag);
        })();
    

▼WebDecode (50 points)

about.htmlを開くとtry inspectと書かれているので開発者ツールでみるとnotify_trueというHTML属性に謎の文字列が埋め込まれている。これをBase64でデコードするとフラグが得られる。

▼Unminify (100 points)

Webページが与えられる。HTMLを順に見ていくとCSSのclassがpicoCTF{...}とフラグになっている箇所があるのでそこが答え。

▼No Sql Injection (200 points)

ソースコードを見ると、mangooseが使われていてMongoDBの問題だとわかる。

以下のような感じで、passwordが{で始まり}で終われば攻撃できそうとわかるので、ソースコードから対象のアカウントのメールアドレスを探して入力し、パスワード欄に{ “$ne”: “” }と入力すると、空文字でないパスワードのデータに対するクエリに変換されるのでログインが成功する。

 const users = await User.find({
      email: email.startsWith("{") && email.endsWith("}") ? JSON.parse(email) : email,
      password: password.startsWith("{") && password.endsWith("}") ? JSON.parse(password) : password
    });

ログインのレスポンスにBase64エンコードされたフラグが入っているのでデコードして終わり。

[Cryptography]

▼interencdec (50 points)

Base64エンコードされたっぽい文字列が与えられるのでとりあえずデコード。結果もまだBase64エンコードされてるっぽいので再度デコード。結果が以下のような文字列になるので、シーザー暗号として7文字ずつずらすとフラグが得られる。

wpjvJAM{jhlzhy_k3jy9wa3k_h47j6k69}

▼Custom encryption (100 points)

以下のようなPythonのコードで暗号化が実装されている。

def dynamic_xor_encrypt(plaintext, text_key):
    cipher_text = ""
    key_length = len(text_key)
    for i, char in enumerate(plaintext[::-1]):
        key_char = text_key[i % key_length]
        encrypted_char = chr(ord(char) ^ ord(key_char