picoCTF Forensics "SideChannel"

# picoCTF "SideChannel"　writeup
[picoCTF](https://play.picoctf.org/)のSideChannelという問題を解いてみました。内容が非常におもしろかったので、ぜひ最後まで読んでみてください。

## 問題の概要
![description.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4094191/12b7f380-0a5a-46b8-a595-73e34c0128de.png)

今回の問題はpicoCTFのForensicsカテゴリーにある問題です。
内容は、pin_checkerという実行ファイルがacceptするPINをさがそうというものです。

## 問題の分析

この配布されたものを実行してみます。

８桁の数字を入力してみると、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4094191/afa1eafb-fba4-45c1-b76a-cb6d1e676f52.png)

８桁以外の数字を入力したり数字以外を入力すると、このように拒否されるようです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4094191/413a6fe9-2977-489d-90ac-9a76da486571.png)

一応、このファイルの情報を調べてみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4094191/5e27c8e4-a18f-45f9-98f9-f66a106b4a11.png)

あまり、使えそうな情報はないみたいです。普通にこのPINコードを探す方法を模索するべきのようです。

### ヒントを見てみる。

CTF初めたばかりの自分はどうしてもヒントが欲しい。。
というわけで、ヒントを全部見ちゃいましょう。

**ヒント:**
* **ヒント1:** `Read about "timing-based side-channel attacks."`
* **ヒント2:** `Attempting to reverse-engineer or exploit the binary won't help you, you can figure out the PIN just by interacting with it and measuring certain properties about it`
* **ヒント3:** `Don't run your attacks against the master server, it is secured against them. The PIN code you get from the pin_checker binary is the same as the one for the master server.`

かなり重要な内容がありますね。

1. `timing-based side-channel attacks`という手法を使うよ。
2. リバースエンジニアリングで頑張っても無駄だよ。
2. サーバーへは攻撃しないで。意味ないよ。

それでは、この`timing-based side-channel attacks`をつかって、PINを探していきましょう。

## timing-based side-channel attacks

タイミングサイドチャネル攻撃とは、プログラムの処理時間（応答時間）のわずかな差を測定することで、通常は外部からは知り得ない内部情報を推測する攻撃手法です。

例えば、PINコードの検証のように、入力された文字が正しいかどうかを1文字ずつ順に比較するプログラムでは、正しい文字を入力した場合と、途中で間違った文字を入力した場合とで、処理にかかる時間に微細な差が生じることがあります。この時間の差を繰り返し測定し、分析することで、正しい情報を特定できる可能性があります。

つまり、このpin_checkerの応答時間を計測し、入力時間が短い、もしくは長い場合の入力からPINコードを推測することができます。

私の戦略は以下の通りです。

1. PINチェッカープログラムの応答時間を測定する仕組みを構築する。
2. PINの各桁を0から9まで総当たりで試行し、それぞれの応答時間を複数回測定して平均を算出する。
3. 最も応答時間が長い（または短い、プログラムの実装による）数字をその桁の正しい値として特定する。
4. このプロセスをPINの全桁に対して繰り返し、完全なPINを解明する。

### 応答時間を計測するためのコード

コードを書くのは面倒なので、AIに全部やらせましょう。
ソースコードは以下の通りです。

```python

import time
import subprocess
import sys

def measure_time(pin_attempt, executable_path='./pin_checker'):
    command = [executable_path]
    try:
        start_time = time.perf_counter()
        process = subprocess.run(command, input=pin_attempt + '\n', 
                                 capture_output=True, text=True, timeout=1.0)
        end_time = time.perf_counter()
        return end_time - start_time
    except subprocess.TimeoutExpired:
        print(f"Timeout for {pin_attempt}", file=sys.stderr)
        return float('inf')
    except Exception as e:
        print(f"Error running subprocess for {pin_attempt}: {e}", file=sys.stderr)
        return float('inf')

def main():
    pin_length = 8 # PINは8桁と指定されている

    print("--- PIN Checker Timing Analysis ---")
    print(f"PIN is expected to be {pin_length}-digits long.")

    # ここに、ブルートフォースの基準となるPIN文字列を入力する部分を追加
    # 例: 最初の2桁が '12' と分かっていれば '12000000' と入力
    while True:
        initial_base_pin_str = input(f"Enter the 8-digit base PIN for verification (e.g., '00000000' or '12300000'): ")
        if len(initial_base_pin_str) == pin_length and initial_base_pin_str.isdigit():
            break
        else:
            print(f"Invalid input. Please enter an {pin_length}-digit number.")
            
    # 入力された文字列をリストに変換して基準PINとする
    # これが、各桁を試す際の「変更しない他の桁」の基準となります。
    base_pin_list = list(initial_base_pin_str)
    print(f"Base PIN set to: {''.join(base_pin_list)}")

    while True:
        try:
            digit_position_str = input(f"Enter the digit position to verify (1-{pin_length}, 0 to exit): ")
            digit_position = int(digit_position_str)

            if digit_position == 0:
                print("Exiting.")
                break
            
            if not (1 <= digit_position <= pin_length):
                print(f"Invalid position. Please enter a number between 1 and {pin_length}.")
                continue

            index_to_verify = digit_position - 1 

            print(f"\nVerifying digit at position {digit_position} (index {index_to_verify})...")

            charset = "0123456789" 
            results = {} 

            for char_to_try in charset:
                # ここで base_pin_list をコピーし、指定桁を変更
                temp_pin_list = list(base_pin_list) # 入力された基準PINをコピー
                temp_pin_list[index_to_verify] = char_to_try
                current_attempt_pin = "".join(temp_pin_list) 

                total_time_for_char = 0.0
                num_trials = 10 

                print(f"  Testing PIN: {current_attempt_pin} (10 trials)... ", end='', flush=True)

                for _ in range(num_trials):
                    run_time = measure_time(current_attempt_pin)
                    if run_time == float('inf'): 
                        total_time_for_char = float('inf')
                        break
                    total_time_for_char += run_time
                
                if total_time_for_char == float('inf'):
                    avg_time = float('inf')
                    print("ERROR / TIMEOUT")
                else:
                    avg_time = total_time_for_char / num_trials
                    print(f"Avg: {avg_time:.6f}s")
                
                results[char_to_try] = avg_time

            print(f"\n--- Results for Position {digit_position} ---")
            for char, avg_time in sorted(results.items()):
                if avg_time == float('inf'):
                    print(f"  Digit '{char}': ERROR / TIMEOUT")
                else:
                    print(f"  Digit '{char}': {avg_time:.6f} seconds")
            
            finite_results = {k: v for k, v in results.items() if v != float('inf')}
            if finite_results:
                best_char = max(finite_results, key=finite_results.get)
                longest_avg_time = finite_results[best_char]
                print(f"\nLikely best digit for position {digit_position}: '{best_char}' (Avg Time: {longest_avg_time:.6f}s)")
            else:
                print("\nNo valid time measurements to determine best digit.")
            
        except ValueError:
            print("Invalid input. Please enter a number.")
        except KeyboardInterrupt:
            print("\nExiting.")
            break

if __name__ == "__main__":
    main()
```

### コードの全体の流れ
1. **基準となるPINの指定**: まず、8桁の基準となるPIN文字列をユーザーが入力します（例: 00000000 や、すでに分かっている桁を含む 12300000 など）。
2. **桁の選択**: 次に、ユーザーが検証したいPINの桁の位置（1桁目から8桁目まで）を指定します。
3. **各数字の検証**: 指定された桁に、0 から 9 までの数字を順番に挿入したPIN文字列を作成します。
4. **時間測定**: 作成したPIN文字列を pin_checker プログラムに渡し、その応答時間を10回測定し、平均値を計算します。
5. **結果の表示と推測**: 10個の数字 (0〜9) 全てについて平均時間を表示し、最も時間が長かった数字がその桁のPINである可能性が高いと推測します。

AIさまさまですね。自分でやったら３時間くらいかかりそう:sweat_smile:

### 攻撃の実行

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4094191/b94f3f20-b331-40b3-b646-2060a83fb287.png)

なんと4を一桁目にしたときだけ明らかに時間がかかっています！！
それではほかの桁もやってみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4094191/a1712b61-15e9-4c9f-9fc2-9d8614ed8bca.png)

あれ、、、うまくいかない。

いろいろ試してみると、１桁目から順番に正解の値を埋めていく必要があるようです。
なので今回はベースを`40000000`に設定してやってみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4094191/2689438e-76bd-4ff2-aa4c-7eca5b4242b7.png)

お！！うまくいったようです。2桁目のところは`8`を入れたときだけ、時間がかかってますね。

この手法を繰り返すことで、以下の手順で8桁のPINをすべて特定していきました。

1. **1桁目**: ベースPINを `00000000` とし、1桁目を検証。結果、4 が最も長い時間を示した。
**確定PIN**（途中）: `4_______`
2. **2桁目**: ベースPINを `40000000` とし、2桁目を検証。結果、8 が最も長い時間を示した。
**確定PIN**（途中）: `48______`
3. ...（繰り返し）

最終的に得られたピンコードを`ping_cheker`にいれて確認しましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4094191/5065c1ec-7ecf-4403-8dae-c43dfe98bbc3.png)


## フラグの取得
最後はこれをサーバに提出しフラッグを取得します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4094191/9c05cdf5-7ae8-4701-bf70-b042164a97a5.png)


## 最後に
このCTFでは`timing-based side channels attacks`という初めて使う手法を駆使してフラッグを得ることができました。

処理時間の差だけでPINコードを求めることができるのはすごい興味深い学びでしたが、同時にシステムの脆弱性ってこんなところにもでるのか、、という驚きもありました。
実装上のわずかな挙動の差が情報漏洩の経路となり得ることを学べたCTFでした。