fr33f0r4ll

自分用雑記

CyberRebeatCTF Writeup

基本的に自分で解いたやつだけ。

binary

crackme

fileで調べるとARMだった。 見た感じでは標準入力がフラグになっていて、再開なら符号化された文字列と一致するというような処理だった。 こういうのはangrで解ける。 シミュレーションしないといけないかと思ったけど、試してみたらangrがそのまま実行できた、すごい。

import angr
import claripy

goal = (0x000105c0)
avoid = [0x0001057c, 0x000105cc]
key_length = 23

p = angr.Project('./crackme')
arg1 = claripy.BVS('arg1', 8*key_length)
state = p.factory.entry_state(args=["./crackme", arg1], add_options={'BYPASS_UNSUPPORTED_SYSCALL'})

for b in arg1.chop(key_length):
    state.add_constraints(b != 0)

pg = p.factory.simgr(state, immutable=False)
e = pg.explore(find=goal, avoid=avoid)


for path in pg.found:
    key = path.state.se.any_str(arg1)
    print("KEY: {}".format(key))

使い方を良く分かってないので変かもしれない。

f31337

fという関数を呼んだあと、スタック上の値とxorした文字列をフラグとして出力している。 が、実行が終わらない。

fを解析してみると、再帰していることが分かった。 再帰で渡す引数を見てみるとフィボナッチっぽいことがなんとなく分かる。 分からなくてもfに渡す引数を順番に変えて観察すれば直感的に分かるはずだ。

その後の処理を見ていくとxorでフラグにしてるっぽい部分が良く分からなかったので手動でデコンパイルした。

int main() {
  char flag[23] = {0};
  
  int rdx = 0; // mov edx, 0;

  do {
    // maybe like this
    int eax = rdx % 8;
    /* int ecx = rdx; */
    /* ecx = ecx >> 0x1f; // MSB to LSB? */
    /* ecx = ecx >> 0x1d; */

    /* int eax =  ecx + rdx; */
    /* eax = eax & 7; */
    /* eax = eax - ecx; */
    // eax; // cdqe    
    
    printf("%d\n", eax);
    
    // eax = [rsp + rax];
    flag[rdx] ^= (char)eax;

    rdx += 1;
      
  } while (rdx != 0x1b);

  // print flag
  
  return 0;
}

実際に走らせてみるとただ%8してるだけだった、最適化の影響で分かりにくい処理になるらしく要調査。

そんなこんなで見ていくと、どうやら31337番目のフィボナッチ数を求めれば良いらしい。 普通にやると計算量が爆発して終わらない(f31337自体が終わらないのはそのせい)ので最適化してあげよう。 メモ化とかその辺を調べれば良いと思う。ググったら出てくるかもしれない。

unsigned long long inner_fibo(unsigned long long num, unsigned long long n1, unsigned long long n2) {
  if (num == 0)
    return n1;
  else
    return inner_fibo(num-1, n2, n1 + n2);
}

unsigned long long fibo(unsigned long long num) {
  return inner_fibo(num, 1, 1);
}

せっかくなのでCで書き直すとこんな感じ。

あとはgdbで結果を直接レジスタに設定してやればいい、その後フラグが表示される。 が、このフラグが誤字っていて通らなかった。ふざけんな!

Crypto

FLAG.encrypted

公開鍵と暗号化されたflagが降ってくる、RSA問題。 pemになった公開鍵のデータを見るとeが異様に大きい値(普通は0x10001、大き過ぎても小さ過ぎてもだめ)ので、wiener attackが可能かもしれないと分かる。 自作ツールでちゃんと解けた、嬉しい。 が、ロード関数作ってなかった上にブランクがあったので無駄に苦戦した。 最終的には秘密鍵だけ渡してPythonで解読、アホか。

(setf n 31264943211208004265136257812922871300684039354012330190834942986731934389912197706421706868451670101634969269274623828581050676733228020854883441494567900924428451571798331504026565707472121772002140681756280190535290943933921834846379665606960802397274296703426557981596105415677658499356618548233939389723076124471098440146923189296244078349641695576997335766674231277153794543785116533620076935082137870329278026757983028280620935089387958708697459641119539250284149601503334899598799831125405703179815161182156366487341348125463136351944709488739527831476641990338237417959538685045943046162779336438891834429341)

(setf e 22766071057080311941289025090582171055356241374729867687887721165996480747230400879635593368509050250879664911119593845131632736205037337764476149970317207453325852306744743355843865620488975017552101697514723815810433086583097066849281143179649731453788074604410013059110037363738062212112776408805474047616975914133565204728262194785129197335550911873746857764241100489778203898866941412395489839653170240092405989209278646213522785197290066584628647242197250525516210135602818305240062919066210956719110372916047407851800476348031106117342132809755720425300509425412742257946576118121595189882915440991231610926049)

;; ローカルに置いてある、登録しようか?
(ql:quickload :hackrsa)

;; 秘密鍵は出せる、秘密鍵は
(setf d (hackrsa:wiener-attack n e))

Signature

ぎりぎりでハッシュエクステンションアタックだと分かったものの、時間が足りず解けなかった。 これさえ解ければ全完だったのに、無念。

Misc

Opening Movie

協力して解いた。 ページ中で300回まで動画を見ないといけないとなっている。 たかだか10数時間なので最初から自動で再生し続ければflagは見れるが、そんなことやってられない。 本当に300回再生したやつがいたらしい、素晴しい。

とりあえずページ中のJSを見てみたが、どうもdllを読み込んでいる。 blazorというブラウザ上でC#を実行するフレームワークがあるらしく、そのプログラムなのだそうだ。 あたまおかしい。

とりあえずそのMoviePlayer.dllをdnSpyで逆コンパイルして見てみた。 すると、encrypt('FLAG_IS_HERE').txtというファイルへのアクセスがあった。 encrypt自体は単なるMD5だったので、ハッシュ計算して直接アクセスすれば良い。 これでフラグゲット。

PPC

ジャンル名はProgrammingになってる。

Calculation

計算式がひたすら降ってくるので計算して返すだけ。 悪い子なのでeval使った。

from pwn import *
io = remote('59.106.212.75', 8080)

while True:
    expr = io.recvline()
    io.info("expr: {}".format(expr))
    io.sendline(str(eval(expr)))
    io.info("send: {}".format(str(eval(expr))))


io.interactive()

Prime Factor

その名の通り素因数分解していく。 他の人が書いたやつをpwntools使うように手直ししてやったら動いた。

from pwn import *
import socket
import sympy

s = remote('59.106.212.75', 8081)

for i in range(1000):
    data = s.recvline()
    print(data)
    max_num = max(sympy.factorint(int(data.decode('utf-8-sig').encode('utf-8').decode('utf-8').strip('\n'))))
    s.sendline(bytes(max_num))

まあやるだけだけど、BOM付きでキレそうだった。

Visual Novels

Reading Powerを越えないように本を読んで満足度を最大化する問題。 要はナップサック問題Pythonに組み合わせ問題を解けるツールがあったので、それを使った。 Python3でしか動かないので、pwntoolsはpython3版を拾ってくる必要がある。

from pwn import *
from ortoolpy import knapsack


def recvline(io):
    return io.recvline().decode('utf-8-sig').encode('utf-8').decode('utf-8')


def parse(io):
    recvline(io)  # blank line
    power = int(recvline(io).replace('Reading Power = ', ''))
    recvline(io)

    novels = []
    line = recvline(io).strip()
    while line != 'Answer = ?':
        novels.append(eval(line.strip(',')))  # dangerous hack!
        line = recvline(io).strip()

    recvline(io)  # blank line

    return (power, novels)


def solve(power, novels):
    size = [n[0] for n in novels]
    weight = [n[1] for n in novels]
    capacity = power
    return knapsack(size, weight, capacity)



io = remote('59.106.212.75', 8082)

for _ in range(5):
    vals = parse(io)
    log.info("params: {}".format(vals))
    ans = int(solve(*vals)[0])
    log.info("ans: {}".format(ans))
    io.sendline(str(ans))
    
io.interactive()
io.close()

ortoolpyすごい。

Stegano

Last 5 Boxes

MP4が降ってくる。 MP4について調べてみると、ボックスと呼ばれる単位でデータが管理されていることが分かる。 おそらく最後の5つのボックスに何かあるのだろうと当たりを付けて、MP4の解析方法を探した。 gpacのMP4Boxが良さそうだったのでそれで確認すると、最後の5つはuuidボックスというものだった。 自由にIDを付けて使えるらしい。 これのデータの部分だけ簡単にパースして、とりあえず全部くっつけたらpngファイルだった。 開くとそれがフラグだった。

from pwn import *

context.endian = 'big'


def parse_uuid_box(data):
    length = data[:4]    
    length = u32(length)
    print(length)

    data = data[4:]
    uuid_sig = data[:4]
    assert (uuid_sig == 'uuid')
    data = data[4:]

    uuid = data[:16]
    data = data[16:]

    pack = data[:length - 24]
    data = data[length - 24:]

    return ((length, uuid, pack), data)


offset = 20998094

mp4 = ''
with open(
        'a4e796eabf01249f6eb8d565ee66849a5bacb472d4ea8adcc6b4dda8f97d318c.mp4',
        'rb') as f:
    mp4 = f.read()

uuid_boxes = mp4[offset:]

boxes = []
for _ in range(5):
    box, rest = parse_uuid_box(uuid_boxes)
    boxes.append(box)
    uuid_boxes = rest

with open('dump.bin', 'wb') as f:
    for _, _, data in boxes:
        f.write(data)

ビッグエンディアンの数値変換のためだけにpwntoolsを使うという力技。

Trivia

Monero

知ってた、coinhive。 ググったらすぐだろう。

Web

White page

最初問題文が見にくかった上にサイレント修正しやがった。 問題文のリンク先のページにいくと、入力フォームがhiddenになっていて入力できなくなっている。 単純にPOSTを再編集するか、ページの値を直接書き換えて問題文で指定されたIDとパスを投げればいい。

Uploader

他の人にunionを教えてもらったので色々やったらできた。

ファイル名検索にSQLiの脆弱性がある、これを利用してsecret.zipが手に入る。 ' or 1 == 1; --

パスがかかっているが、ログインするとZipのパスが見れるようになるのでどうにかしてログインする必要がある。

SQLiを色々試しているとエラーメッセージでSQLite3を使ってることが分かった。 ここで、SQLite3でテーブル名が格納されているテーブルの名前とかを調べてみると、sqlite_masterに格納されていることが分かった。
' or 1 == 1 union select * from sqlite_master; --
カラム数が違うと怒られたので、適当に合わせてやる。
' or 1 == 1 union select type,name,tbl_name,rootpage from sqlite_master; --
これでテーブルの名前が流出した、FilesとUsersがある。 おそらくUsersにパスワードが格納されているのだろうと当たりをつけて表示する。

カラム数が違っているがカラム名が分からないのでどうしようかと思ったが、入力フォームの名前で試したらどうやら合っていたらしくユーザ名とパスが流出した。
' or 1 == 1 union select userid, password, NULL, NULL from Users; --

あとはこれでログインするだけである。 パスをゲットしてsecret.zipを解凍、フラグゲット。