---
# System prepended metadata

title: 2024 AIS3 Pre-exan Writeup
tags: [資訊安全, 比賽]

---

# 2024 AIS3 Pre-exam Writeup

## Intro
### About me
哈囉~我是星星，來自北科資工，緊張刺激的 Pre-exam 終於結束了！我自己感覺今年的題目相較於去年有比較困難一點，明明題目標註 easy 但怎麼就是解不開 QQ？

這是我第三次報名 AIS3 的 Pre-exam，可以很明顯感覺到自己的進步，真的很感謝 AIS3 提供一個這麼棒的比賽和暑期課程！雖然今年因為暑期實習外加撞到其他活動即使入取了也沒辦法去，但志在參與 ~ 同時也希望我的 writeup 可以幫助我推甄到好研究所 OuO。

### Environment and tools
- 環境：
    - Windows11
    - Linux kali 6.1.0-kali5-amd64
- 工具：
    - WireShark (網路封包擷取、分析)
    - Ghrida (逆向分析)
    - Burp Suite (Web安全測試)
    - VS code (程式編輯器)

## Misc
### Welcome
- 題目：
    The FLAG is AIS3{Welc0me_to_AIS3_PreExam_2o24!}
- 解題步驟：
    1. 打開題目就可以看到了！「**Flag GET**」
        ![圖片](https://hackmd.io/_uploads/S1ViYiBVC.png)
- Flag：
    :::success
    AIS3{Welc0me_to_AIS3_PreExam_2o24!}
    :::
### Quantum Nim Heist
- 題目：
    Welcome to the Quantum Nim Heist, where traditional logic intertwines with the enigmatic realm of quantum mechanics to create a Nim game like no other. `nc chals1.ais3.org 40004`
- 檔案：
    - src
        - `game.py` (生成遊戲、判斷輸贏)
        - `myhash.py` (用來處理存檔的 hash)
        - `server.py` (遊戲的主程式)
        - `text.py` (遊戲文字)
- 解題步驟：
    1. 在 Terminal 輸入 `nc chals1.ais3.org 40004`，出現了遊戲畫面
        ![圖片](https://hackmd.io/_uploads/r1rayyLNA.png)
    2. 嘗試玩幾次發現每一回合都可以選擇拿某一堆的幾顆石頭，電腦也會拿石頭，最後是比誰最後沒有石頭拿就輸了，玩了很多次結果都是輸給電腦
        ![圖片](https://hackmd.io/_uploads/H17_GkLN0.png)
    4. 受不了先去看主程式 `server.py`。在程式中發現了兩件事：首先需要獲勝才能得到 flag，再來是當生成遊戲時 `menu()` 會呼叫 `game.generate_losing_game()`，看起來就代表這是一個必輸的遊戲
        ``` python!
        import myhash
        from game import Game, AIPlayer
        from text import *


        flag = 'A1S3{test_flag}'

        hash = myhash.Hash()


        def play(game: Game):
            ai_player = AIPlayer()
            win = False

            while not game.ended():
                game.show()
                print_game_menu()
                choice = input('it\'s your turn to move! what do you choose? ').strip()

                if choice == '0':
                    pile = int(input('which pile do you choose? '))
                    count = int(input('how many stones do you remove? '))
                    if not game.make_move(pile, count):
                        print_error('that is not a valid move!')
                        continue

                elif choice == '1':
                    game_str = game.save()
                    digest = hash.hexdigest(game_str.encode())
                    print('you game has been saved! here is your saved game:')
                    print(game_str + ':' + digest)
                    return

                elif choice == '2':
                    break

                # no move -> player wins!
                if game.ended():
                    win = True
                    break
                else:
                    print_move('you', count, pile)
                    game.show()

                # the AI plays a move
                pile, count = ai_player.get_move(game)
                assert game.make_move(pile, count)
                print_move('i', count, pile)

            if win:
                print_flag(flag)
                exit(0)
            else:
                print_lose()


        def menu():
            print_main_menu()
            choice = input('what would you like to do? ').strip()

            if choice == '0':
                print_rules()

            elif choice == '1':
                game = Game()
                game.generate_losing_game()
                play(game)

            elif choice == '2':
                saved = input('enter the saved game: ').strip()
                game_str, digest = saved.split(':')
                if hash.hexdigest(game_str.encode()) == digest:
                    game = Game()
                    game.load(game_str)
                    play(game)
                else:
                    print_error('invalid game provided!')

            elif choice == '3':
                print('omg bye!')
                exit(0)


        if __name__ == '__main__':
            print_welcome()

            try:
                while True:
                    menu()
            except Exception:
                print('oops i died')
        ```
    4. 這次去看 `game.py` 的程式。發現確實有一個 `generate_winning_game` 的函式，但我對於如何跑到這個函式完全沒有想法 (這時我就跑去解其他題目了)
        ``` python!
        def generate_winning_game(self) -> None:
            '''generate a game such that the first player has a winning strategy'''
            self.stones = []
            xor_sum = 0

            piles = random.randint(6, 8)
            for i in range(piles):
                self.stones.append(count := random.randint(1, 31))
                xor_sum ^= count

            if xor_sum == 0:
                self.stones.append(random.randint(1, 31))

        def generate_losing_game(self) -> None:
            '''generate a game such that the second player has a winning strategy'''
            self.stones = []
            xor_sum = 0

            piles = random.randint(6, 8)
            for i in range(piles):
                self.stones.append(count := random.randint(1, 31))
                xor_sum ^= count

            if xor_sum != 0:
                self.stones.append(xor_sum)
        ```
    5. 後來官方提供了提示
        :::info
        不用按照既定的遊戲規則走！多玩遊戲也許在無意間能找出解法
        :::
    6. 這時我回到 `server.py` 嘗試尋找遊戲漏洞，發現在選擇操作的 if 沒有 else，這代表程式只要不是輸入 0~2 的操作，就可以直接跳過玩家回合進入到電腦的回合
        ``` python!
        while not game.ended():
            game.show()
            print_game_menu()
            choice = input('it\'s your turn to move! what do you choose? ').strip()
            # 玩家的操作選項
            if choice == '0': # 選擇拿石頭
                pile = int(input('which pile do you choose? '))
                count = int(input('how many stones do you remove? '))
                if not game.make_move(pile, count):
                    print_error('that is not a valid move!')
                    continue

            elif choice == '1': # 存檔+離開
                game_str = game.save()
                digest = hash.hexdigest(game_str.encode())
                print('you game has been saved! here is your saved game:')
                print(game_str + ':' + digest)
                return

            elif choice == '2': # 離開
                break

            # no move -> player wins!
            if game.ended():
                win = True
                break
            else:
                print_move('you', count, pile)
                game.show()
                
            # the AI plays a move
            pile, count = ai_player.get_move(game)
            assert game.make_move(pile, count)
            print_move('i', count, pile)
        ```
    7. 所以在遊戲過程中我很快速地把大部分的石頭拿走，剩下一點點石頭時輸入 3 讓電腦一直拿石頭直到剩下一顆
        ![圖片](https://hackmd.io/_uploads/HyMd4yLN0.png)
    8. 直接把最後一顆拿走 「**Flag GET**」
        ![圖片](https://hackmd.io/_uploads/SkCo4JU4A.png)
- Flag：
    :::success
    AIS3{Ar3_y0u_a_N1m_ma57er_0r_a_Crypt0_ma57er?}
    :::

### Three Dimensional Secret
- 題目：
    I shall send printable secrets
    Author: ja20nl1n
- 檔案：
    - `capture.pcapng`
- 解題步驟：
    1. 直接用 wireshark 打開 `capture.pcapng` 這個檔案
    2. 打開檔案就發現有很多 ARP 的封包再詢問 `192.168.77.2` 的位址，想到以前有看過 ARP 攻擊的文章，所以滑到下面找看看有沒有回傳位址的封包或其他可疑的東西
        ![圖片](https://hackmd.io/_uploads/BJTAdArNR.png)
    3. 找到了回傳位址的封包，前一個封包詢問 `192.168.77.1` 的位址，然後有回傳位址，但在回傳的封包裡面沒有找到有用的訊息 (解題方向錯了)
        ![圖片](https://hackmd.io/_uploads/H1kRqRHE0.png)
    4. 搜尋 `http` 看看有沒有網頁可以打開，結果是沒有
        ![圖片](https://hackmd.io/_uploads/rkONsRHE0.png)
    5. 搜尋 `tcp` 看看有甚麼東西，有很多封包，逐一檢查發現 `192.168.77.1` 發給 `192.168.77.128` 的封包內容很長，感覺是有意義的東西
        ![圖片](https://hackmd.io/_uploads/rJ_H2ASNR.png)
    6. 在 Analyze > Follow > TCP Stream，就可以看到所以TCP封包裡面的內容感覺是某種語法，拿去問 ChatGPT 發現是 3D 列印機常用的 G code (非常地符合題目敘述！)
        ![圖片](https://hackmd.io/_uploads/BJ5J6CS4A.png)
    7. 同時 ChatGPT 提供了 [ncviewer](https://ncviewer.com/) 來讓我們預覽 G code 會印出甚麼 「**Flag GET**」
        ![圖片](https://hackmd.io/_uploads/H1Yz00HEC.png)
- Flag：
    :::success
    AIS3{b4d1y_tun3d_PriN73r}
    :::

## Web
### Evil Calculator
- 題目：
    This is a calculator written in Python. It's a simple calculator, but some function in it is VERY EVIL!!
    Connection info: `http://chals1.ais3.org:5001`
    Author: TriangleSnake
- 檔案：
    - app
        - templates
            - `index.html`
        - `app.py` (API)
    - `docker-compose.yml`
    - `Dockerfile`
    - `flag`
- 解題步驟：
    1. 從檔案路徑可以發現在最外層有 `flag` 這個檔案，但程式不會碰到它
    2. 點開網址看到了一個可愛的計算機，功能就是一般的計算機
        ![圖片](https://hackmd.io/_uploads/SJ8cijBVA.png)
    3. 按下 `F12` 檢查一下有沒有可疑的東西
    4. 發現按下 `=` 時，在網路監控器中，可以看到它會發送 API
        ![圖片](https://hackmd.io/_uploads/B1lQr2iHNR.png)
    4. 請求的參數是 `expression	"1+50"`
    5. 看一下 `app.py`，API 會呼叫 `calculate()` 這個函式
        ``` python!
        from flask import Flask, request, jsonify, render_template

        app = Flask(__name__)

        @app.route('/calculate', methods=['POST'])
        def calculate():
            data = request.json
            expression = data['expression'].replace(" ","").replace("_","")
            try:
                result = eval(expression)
            except Exception as e:
                result = str(e)
            return jsonify(result=str(result))

        @app.route('/')
        def index():
            return render_template('index.html')

        if __name__ == '__main__':
            app.run("0.0.0.0",5001)
        ```
    6. 可以發現裡面有一個邪惡的語法 `eval()`，上網查一下[語法介紹](https://www.runoob.com/python/python-func-eval.html) (簡單來說就是可以執行一些 python 語法)
        ![圖片](https://hackmd.io/_uploads/SktC6oSEA.png)
    7. `eval()` 的參數是 expression，也就是前面操作時輸入的 `1+50`，由此可知我們需要想辦法修改計算機的輸入，然後讓它可以讀到 `flag` 這個檔案
    8. 打開 Burp suite，按下 `=` 攔截API，把參數改成 `"expression":"open('../flag').read()"` (要注意路徑對不對以及記得加 `read()`)
        ![圖片](https://hackmd.io/_uploads/H1WkV3r4C.png)
    9. 計算結果就出來了 「**Flag GET**」
        ![圖片](https://hackmd.io/_uploads/rkPWN3HER.png)
- Flag：
    :::success
    AIS3{7RiANG13_5NAK3_I5_50_3Vi1}
    :::

## Crypto
### babyRSA
- 題目：
- 檔案：
    - `babyRSA.py`
    - `output.txt`
- 解題步驟：
    1. 打開 `output.txt` 可以看到這是一個加密後的密文和公開金鑰
    2. 打開 `babyRSA.py` 可以看到這是一個可以 RSA 加解密的程式碼，基本上可以確定 output.txt 就是被加密後的 flag
        ![圖片](https://hackmd.io/_uploads/B1JdTXmE0.png)
    2. 我的第一想法是透過函式 `generate_keypair` 和公鑰反推出私鑰是甚麼，但問題就來了，完全沒辦法推出 p 和 q 是甚麼
        ![圖片](https://hackmd.io/_uploads/B18LRQX4R.png)
    3. 這時突然想到前幾天有讀到別人 [2023 AIS3 writeup](https://hackmd.io/@M3t30r/Bk3Qqq3O5#SC-100-baby)
    4. 所以嘗試寫了一個 dictionary 的程式碼 (公鑰和密文太長就不貼上來了)
        ``` python!
        def encrypt(plaintext):
            key, n = # Public Key
            cipher = [pow(ord(char), key, n) for char in plaintext]
            return cipher

        plaintext = "AIS3{}_abcdefghij_klmnopqrst_uvwxyz_ABCDEFGHIJ_KLMNOPQRST_UVWXYZ_0123456789_#$%&([]./;=+-" # 為了建字典所打的字元表
        encrypt_msg = encrypt(plaintext)

        dic = {}
        for i in range(len(plaintext)):
            dic[encrypt_msg[i]] = plaintext[i]

        encrypt_flag = # Encrypted
        for char in encrypt_flag:
            print(dic[char], end="")
        ```
    5. 結果第一個字報錯了！這個錯誤訊息表示 dictionary 不存在這個字
        ![圖片](https://hackmd.io/_uploads/rJ2kZV7NA.png)
    6. 在顯示的 for 迴圈裡加了 try
        ``` python!
        for char in encrypt_flag:
            try:
               print(dic[char], end="")
            except KeyError:
               print("has no key")
        ```
    7. 原來是前面有奇怪的亂碼「**Flag GET**」
        ![圖片](https://hackmd.io/_uploads/Byu9gE7VC.png)
- Flag：
    :::success
    AIS3{NeverUseTheCryptographyLibraryImplementedYourSelf}
    :::

## Reverse
### The long Print
- 題目：
- 檔案：
    - `flag-printer-dist`(執行檔)
- 解題步驟：
    1. 直接執行檔案，發現程式看起來沒有在動也沒有結束 (盲猜可能卡在 for 迴圈)
    2. 因為不會用 Kali 的內建 Reverse 工具，所以下載 [Ghrida](https://ghidra-sre.org/)
        ```
        sudo apt install ghidra
        ```
    2. 開啟 Ghrida (UI 介面)
    3. 建立專案、匯入 `flag-printer-dist` 執行檔
        ![圖片](https://hackmd.io/_uploads/rybcAGQ4C.png)
    5. 點擊檔案，進行分析
    6. 在左邊 Symbol Tree 的 Function 中選擇 main (先看主程式怎麼運行)
        ![圖片](https://hackmd.io/_uploads/B1IgkXmNR.png)
    7. 發現有一段程式 `sleep 0x3648` 參數很大程式會要睡很久
    8. 在這段程式按下 `Ctrl+Shift+G` 把參數改成 0
        ![圖片](https://hackmd.io/_uploads/ByFIymmNA.png)
    9. 按下 `Ctrl+S`，在 file > Export Program，把檔案匯出(記得格式要改成 Original File)
        ![圖片](https://hackmd.io/_uploads/Syh0-m7NA.png)
    10. 打開 Terminal，修改檔案權限然後執行檔案
        ```
        chmod 777 flag-printer-dist
        ./flag-printer-dist
        ```
    11. 他說 flag 已經 print 出來了，但沒有看到！
        ![圖片](https://hackmd.io/_uploads/HyRJmQ7EC.png)
    12. 回到 Ghrida，這次把參數改成 1，然後匯出執行
    13. 發現 flag 正在一秒一秒的出現，最後 Oops! 消失了
        ![圖片](https://hackmd.io/_uploads/rJqsX77VC.png)
    14. 在最後一個字元出現的一瞬間 `Ctrl+C` 中斷執行「**Flag GET**」
        ![圖片](https://hackmd.io/_uploads/rkgc47QE0.png)
- Flag：
    :::success
    AIS3{You_are_the_master_of_time_management!!!!?}
    :::
## Pwn
### Mathter
我辦不到。･ﾟ･(つд`ﾟ)･ﾟ･