# 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**」

- 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`,出現了遊戲畫面

2. 嘗試玩幾次發現每一回合都可以選擇拿某一堆的幾顆石頭,電腦也會拿石頭,最後是比誰最後沒有石頭拿就輸了,玩了很多次結果都是輸給電腦

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 讓電腦一直拿石頭直到剩下一顆

8. 直接把最後一顆拿走 「**Flag GET**」

- 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 攻擊的文章,所以滑到下面找看看有沒有回傳位址的封包或其他可疑的東西

3. 找到了回傳位址的封包,前一個封包詢問 `192.168.77.1` 的位址,然後有回傳位址,但在回傳的封包裡面沒有找到有用的訊息 (解題方向錯了)

4. 搜尋 `http` 看看有沒有網頁可以打開,結果是沒有

5. 搜尋 `tcp` 看看有甚麼東西,有很多封包,逐一檢查發現 `192.168.77.1` 發給 `192.168.77.128` 的封包內容很長,感覺是有意義的東西

6. 在 Analyze > Follow > TCP Stream,就可以看到所以TCP封包裡面的內容感覺是某種語法,拿去問 ChatGPT 發現是 3D 列印機常用的 G code (非常地符合題目敘述!)

7. 同時 ChatGPT 提供了 [ncviewer](https://ncviewer.com/) 來讓我們預覽 G code 會印出甚麼 「**Flag GET**」

- 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. 點開網址看到了一個可愛的計算機,功能就是一般的計算機

3. 按下 `F12` 檢查一下有沒有可疑的東西
4. 發現按下 `=` 時,在網路監控器中,可以看到它會發送 API

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 語法)

7. `eval()` 的參數是 expression,也就是前面操作時輸入的 `1+50`,由此可知我們需要想辦法修改計算機的輸入,然後讓它可以讀到 `flag` 這個檔案
8. 打開 Burp suite,按下 `=` 攔截API,把參數改成 `"expression":"open('../flag').read()"` (要注意路徑對不對以及記得加 `read()`)

9. 計算結果就出來了 「**Flag GET**」

- Flag:
:::success
AIS3{7RiANG13_5NAK3_I5_50_3Vi1}
:::
## Crypto
### babyRSA
- 題目:
- 檔案:
- `babyRSA.py`
- `output.txt`
- 解題步驟:
1. 打開 `output.txt` 可以看到這是一個加密後的密文和公開金鑰
2. 打開 `babyRSA.py` 可以看到這是一個可以 RSA 加解密的程式碼,基本上可以確定 output.txt 就是被加密後的 flag

2. 我的第一想法是透過函式 `generate_keypair` 和公鑰反推出私鑰是甚麼,但問題就來了,完全沒辦法推出 p 和 q 是甚麼

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 不存在這個字

6. 在顯示的 for 迴圈裡加了 try
``` python!
for char in encrypt_flag:
try:
print(dic[char], end="")
except KeyError:
print("has no key")
```
7. 原來是前面有奇怪的亂碼「**Flag GET**」

- 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` 執行檔

5. 點擊檔案,進行分析
6. 在左邊 Symbol Tree 的 Function 中選擇 main (先看主程式怎麼運行)

7. 發現有一段程式 `sleep 0x3648` 參數很大程式會要睡很久
8. 在這段程式按下 `Ctrl+Shift+G` 把參數改成 0

9. 按下 `Ctrl+S`,在 file > Export Program,把檔案匯出(記得格式要改成 Original File)

10. 打開 Terminal,修改檔案權限然後執行檔案
```
chmod 777 flag-printer-dist
./flag-printer-dist
```
11. 他說 flag 已經 print 出來了,但沒有看到!

12. 回到 Ghrida,這次把參數改成 1,然後匯出執行
13. 發現 flag 正在一秒一秒的出現,最後 Oops! 消失了

14. 在最後一個字元出現的一瞬間 `Ctrl+C` 中斷執行「**Flag GET**」

- Flag:
:::success
AIS3{You_are_the_master_of_time_management!!!!?}
:::
## Pwn
### Mathter
我辦不到。・゚・(つд`゚)・゚・