日記

日本語の勉強のためのブログ

WaniCTF2024 writeup

10問解いて233位だった。数時間しか参加できなかった割にはよく解けたと思うが、惜しいところで詰まった問題も多く技術不足を感じた。

crypto

beginners_rsa (beginner)

nが5つの素数の積として定義されている。こうした問題はMulti-Prime RSAと呼ばれるらしい。
n自体は96桁の巨大な数だが、その素因数は64bitほどの大きさしかない。そのため簡単に素因数分解できる。
ref: https://www.alpertron.com.ar/ECM.HTM

素因数分解の結果を以下に示す。

n = 317903423385943473062528814030345176720578295695512495346444822768171649361480819163749494400347

primes = [
    9953162929836910171,
    11771834931016130837,
    12109985960354612149,
    13079524394617385153,
    17129880600534041513,
]

p, q, r, s, aが求まったので、ここからphi(n) -> d -> mの順に計算することができる。
Malti-Prime RSAにおいてphi(n)(p - 1)(q - 1)(r - 1)(s - 1)(a - 1)と求まるから、以降は通常のRSAと同じようにd = e^(-1) mod phi(n), m = c ^ d mod nに沿って計算すればよい。
ref: https://www.ochappa.net/posts/multi-prime-rsa

※なお、計算が遅い場合は中国剰余定理を利用した高速化手法が使えるらしいが、今回は爆速で解読できたため利用しなかった。

以下は計算に使用したソースコードである。

from Crypto.Util.number import *

n = 317903423385943473062528814030345176720578295695512495346444822768171649361480819163749494400347
e = 65537
enc = 127075137729897107295787718796341877071536678034322988535029776806418266591167534816788125330265

# https://www.alpertron.com.ar/ECM.HTM
primes = [
    9953162929836910171,
    11771834931016130837,
    12109985960354612149,
    13079524394617385153,
    17129880600534041513,
]

phi = (
    (primes[0] - 1)
    * (primes[1] - 1)
    * (primes[2] - 1)
    * (primes[3] - 1)
    * (primes[4] - 1)
)
d = pow(e, -1, phi)
m = pow(enc, d, n)
flag = long_to_bytes(m)
print(flag)

以下は実行結果。

$ python3 dec.py 
b'FLAG{S0_3a5y_1254!!}'

付録:RSA問題で役に立つリンク集

beginners_aes (beginner)

AESに関する問題だが、AESの仕組みを知らずとも解ける。 keyがb"the_enc_key_is_" + (ランダムな1バイト)、ivがb"my_great_iv_is_" + (ランダムな1バイト)と定義されているから、考えられるkey, ivの候補を全探索すればよい。
その後、得られたflagの候補に対して、

  • FLAG{という文字列から始まるか
  • (パディングを外したうえで)ハッシュ値が与えられたものと等しいか

を調べることで、正しいflagが求まる。
以下は作成した解読用スクリプトである。

from Crypto.Util.Padding import unpad
from Crypto.Cipher import AES
import hashlib

enc = b'\x16\x97,\xa7\xfb_\xf3\x15.\x87jKRaF&"\xb6\xc4x\xf4.K\xd77j\xe5MLI_y\xd96\xf1$\xc5\xa3\x03\x990Q^\xc0\x17M2\x18'
flag_hash = "6a96111d69e015a07e96dcd141d31e7fc81c4420dbbef75aef5201809093210e"

key = b'the_enc_key_is_'
iv = b'my_great_iv_is_'

for c_key in range(255):
    new_key = key + c_key.to_bytes() 
    for c_iv in range(255):
        new_iv = iv + c_iv.to_bytes() 
        cipher = AES.new(new_key, AES.MODE_CBC, new_iv)
        m_pad = cipher.decrypt(enc)
        if (not m_pad.startswith(b"FLAG{")):
            continue
        m = unpad(m_pad, 16)
        m_hash = hashlib.sha256(m).hexdigest()
        if (m_hash != flag_hash):
            continue
        print(m)
        # exit()

FLAG{7h3_f1r57_5t3p_t0_Crypt0!!}

replacement (easy)

1文字ごとにmd5ハッシュ値を求めているので、これも全探索すればよさそう。 全文字のmd5ハッシュ値を求め、与えられた配列の中に同一のハッシュ値があればその文字で置き換える、という処理を行えばよい。

import hashlib

# 配布された"my_diary_11_8_Wednesday.txt"の中身。長いので省略
enc = [265685380796387128074260337556987156845, ... , 301648155472379285594517050531127483548]

for c in range(256):
    x = hashlib.md5(str(c).encode()).hexdigest()
    x = int(x, 16)
    enc = [chr(c) if e == x else e for e in enc]
m = "".join(enc)
print(m)

以下は実行結果。

$ python3 dec.py
Wednesday, 11/8, clear skies. This morning, I had breakfast at my favorite cafe. Drinking the freshly brewed coffee and savoring the warm buttery toast is the best. Changing the subject, I received an email today with something rather peculiar in it. It contained a mysterious message that said "This is a secret code, so please don't tell anyone. FLAG{13epl4cem3nt}". How strange!

Gureisya

forensics

tiny-usb (beginner)

AutoPsyでisoファイルを開くと、FLAG.PNGという画像ファイルを見つけた。そこにflagが書かれていた。

FLAG{hey_i_just_bought_a_usb}

※ちなみにstringsコマンドで調べても画像ファイルの存在自体はわかる。

ImgBurn v2.5.8.0
CD001
FLAG.PNG;1
IHDR

Surveillance-of-sus (Normal)

fileコマンドで調べてもdataとしか言われず、ファイルタイプは判明しなかった。 そこでstringsコマンドを使うと先頭にRDP8bmpという文字列が見つかり、それで調べるとRDP Bitmap Cacheというファイルであることがわかった。 RDP(リモートデスクトッププロトコル)使用時、通信量軽減のために画像をキャッシュする仕組みがあるらしく、その際に作成されるファイルとのこと。

さらに検索すると解析方法から何まで書いてあったため、これに沿って解析していく。
https://jpn.nec.com/cybersecurity/blog/231006/index.html

簡単に説明すると、まずbmc-toolsでキャッシュから画像ファイル(複数のタイル)を得て、

$ wget https://raw.githubusercontent.com/ANSSI-FR/bmc-tools/master/bmc-tools.py
$ mkdir result
$ ls
bmc-tools.py  Cache_chal.bin  result
$ python3 bmc-tools.py -s Cache_chal.bin -d result
[+++] Processing a single file: 'Cache_chal.bin'.
[===] 650 tiles successfully extracted in the end.
[===] Successfully exported 650 files.
[===] Successfully exported collage file.

それらのタイルをRdpCacheStitcherというツールで並び替えることで、もとの画像を復元する。
https://github.com/BSI-Bund/RdpCacheStitcher/releases/

並び替えによって得た画像を以下に示す。flagが書かれているのがわかると思う。

FLAG{RDP_is_useful_yipeee}

codebreaker (beginner)

以下のサイトを使い、与えられた画像とにらめっこしてQRコードを作っていく。見えない部分については塗らないままでよい。
※参考までに、この問題ではQR-code versionは29x29(ver.3)、青いタイルをクリックすると表示されるFormat Info PatternはH/7だった。

https://merri.cx/qrazybox/

塗り終えたら右側の""Editor mode"をクリックするとテキストベースで表示してくれるので、これをコピーする。

#######_#?#???#?#???#_#######
#_____#_##??#???????#_#_____#
#_###_#__##?#?##?#???_#_###_#
#_###_#_##??##?##?##?_#_###_#
#_###_#_#????##??##??_#_###_#
#_____#_##????#??###?_#_____#
#######_#_#_#_#_#_#_#_#######
_________???#?#??????________
___#__#__???####?#???__###_##
###??#_?#??????#????#?#???##?
?#???##?##??????????#?##??##?
#?#???_?##???????????#?##?###
#####?#??##????????#?##?#??##
?#??#?_#####?????#####???###?
##?#####????????????###?????#
?##???_?##??????????#??##??##
##??#?###????#???????#???####
?#?###_??????##?#?????##??##?
?####?##?????#?#?#??????#?###
##??##_???????###???????##??#
?#??###????#?????########?###
________##???#?###??#___##?##
#######__##??####??##_#_#?##?
#_____#__#?##????#??#___#????
#_###_#__#??#?##?#??#####???#
#_###_#_#?#??#??#??##?#?#???#
#_###_#__##??###?###?#??##???
#_____#__#?##???#?#?##??#????
#######__#?#?##??##?######???

塗っていない部分はすべて?と表記されているため、白マスについては_に置き換える。メモ帳の置換機能を使って1行ずつ見ていった。

#######_#_#___#_#___#_#######
#_____#_##__#_______#_#_____#
#_###_#__##_#_##_#____#_###_#
#_###_#_##__##_##_##__#_###_#
#_###_#_#____##__##___#_###_#
#_____#_##____#__###__#_____#
#######_#_#_#_#_#_#_#_#######
_________??_#_#____??________
___#__#__???####_#???__###_##
###__#__#????__#_???#_#___##_
_#___##_##?????????_#_##__##_
#_#_____##_#??????___#_##_###
#####_#__##_?????__#_##_#__##
_#__#__#####_????#####___###_
##_#####____??????__###_____#
_##_____##_????????_#__##__##
##__#_###_???#__?????#___####
_#_###____?__##_#????_##__##_
_####_##???__#_#_#????__#_###
##__##__??#___###__????_##__#
_#__###??__#_____########_###
________##___#_###__#___##_##
#######__##__####__##_#_#?##_
#_____#__#_##____#__#___#??__
#_###_#__#__#_##_#__#####??_#
#_###_#_#_#__#__#__##_#_#???#
#_###_#__##__###_###_#__##???
#_____#__#_##___#_#_##__#_???
#######__#_#_##__##_######_??

これをtxtファイルとして保存し、再度QRazyBoxに食わせてみる(New -> Import from text)。右上のTools -> Extract QR Informationを選択して解読を試みるが、以下の通りまだ読むことができない。

QR version : 3 (29x29)
Error correction level : H
Mask pattern : 7
Number of missing bytes (erasures) : 27 bytes (38.57%)
Data blocks :
["??????0?","00010110","01010100","11100010","01100100","11010110","11000100","01000110","000?????","???1?110","01110111","11000110","10110100","11110111","10000110","01010111","1???0111","00110111","01110101","1???????","?1110111","111011??","???10110","00010001","00110110","11101?0?","???0?101","01??????","??101000","01001110","10011010","01111011","10111101","0001111?","0???????","?????001","11101001","01111000","00010000","00110110","1???????","?????100","01110000","00011000","11101001","11011010","01110011","0011000?","????100?","0?1????0","?1100101","10111100","01001000","000?????","??100010","01000?0?","1??0?100","01100110","1010001?","??0?1110","01101111","00101110","01000001","00010011","10000000","01010010","11110101","01001110","10111101","01000000"]
----------------Block 1----------------
Reed-Solomon Block : [0,84,100,196,0,119,180,134,0,117,0,0,54,0,0,154,189,0,233,16,0,112,233,115,0,0,72,0,0,0,111,65,128,245,189]
----------------Block 2----------------
Reed-Solomon Block : [22,226,214,70,0,198,247,87,55,0,0,17,0,0,78,123,0,0,120,54,0,24,218,0,0,188,0,0,102,0,46,19,82,78,64]
Final data bits :
0000000001010100011001001100010000000000011101111011010010000110000000000111010100000000000000000011011000010110111000101101011001000110000000001100011011110111010101110011011100000000000000000001000100000000
Final Decoded string :
Error :
- Too much missing bits

これ以上の復元は無理そうで諦めかけたが、ここで"reed-solomon decoder"を使うとデコードできる場合があると知った。
ref: https://merri.cx/qrazybox/help/examples/basic-example.html

そこでTools -> reed-solomon decoderを選択し、Decodeを押すと、見事flagを得ることができた。

FLAG{How_scan-dalous}

I_wanna_be_a_streamer (easy):解けなかった

RTPストリームから怪しげな音源を見つけるところまでは行けたがそこからがわからなかった

pwnable

nc (beginner)

16進数で計算の答えを述べる問題。

$ nc chal-lz56g6.wanictf.org 9003
15+1=0x10
FLAG{th3_b3ginning_0f_th3_r0ad_to_th3_pwn_p1ay3r}

home (easy):解けなかった

現在いるパスにServiceという文字列が含まれており、なおかつデバッガを使用していない場合にflag作成処理constructFlag()が実行される。 ただし、2つ目の条件「デバッガを使用していない」に関してはcmp命令で判定がされており、$rax == 0xffffffffffffffffのときにデバッガを使っていると判断される。そのため、デバッガを使用していたとしても、print $rax=1gdbの例)のようにraxを適当な値に変えてやれば突破できる。

さて、このconstructFlag()の結果がどこに格納されるかがわからない。

reversing

lambda

ソースコードに書いてあった内包表記を組み合わせたら解けてしまった…

s = "16_10_13_x_6t_4_1o_9_1j_7_9_1j_1o_3_6_c_1o_6r"
s = [chr(int(c,36) + 10) for c in s.split('_')]
s = [chr(123^ord(c)) for c in s]
s = [chr(ord(c) + 3) for c in s]
s = [chr(ord(c) - 12) for c in s]
print("".join(s))

web

Bad_Worker (beginner)

"Fetch"ボタンを押した際の遷移先をBurpSuiteでdummy.txtからflag.txtに書き換えるだけ。

FLAG{pr0gr3ssiv3_w3b_4pp_1s_us3fu1}

pow (easy)

あるWebページが与えられる。そこでは一定間隔でsend関数が実行されており、sendでは/api/powに引数arrayをPOSTする処理が行われている。
scriptタグ内のコードを以下に示す。

function hash(input) {
  let result = input;
  for (let i = 0; i < 10; i++) {
    result = CryptoJS.SHA256(result);
  }
  return (result.words[0] & 0xFFFFFF00) === 0;
}
async function send(array) {
  document.getElementById("server-response").innerText = await fetch(
    "/api/pow",
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(array),
    }
  ).then((r) => r.text());
}
let i = BigInt(localStorage.getItem("pow_progress") || "0");
async function main() {
  await send([]);
  async function loop() {
    document.getElementById(
      "client-status"
    ).innerText = `Checking ${i.toString()}...`;
    localStorage.setItem("pow_progress", i.toString());
    for (let j = 0; j < 1000; j++) {
      i++;
      if (hash(i.toString())) {
        await send([i.toString()]);
      }
    }
    requestAnimationFrame(loop);
  }
  loop();
}
main();

一定の間隔でsendが呼び出されており、そのたびにページ内のServer responseの数字が1ずつ増えていく。表記からしてこの数字を1000000にすればflagが手に入りそうだ。 とりあえず適当な値でsendを呼び出してみたが、特定の値でないとBad Requestとなってしまう。そのため、一度正規の通信が行われるまで待ち、そこで送信された値2862152*1を用いて呼び出したところ、Server responseが1増えたことが確認できた。

さて、sendを使えば数値が増やせることがわかったものの、愚直に100万回リクエストを飛ばすとDoS攻撃になりかねない。 そこでこのsend関数が引数にstringsのarrayをとることから、複数の値を一気に渡せないかと考えた。実際に["2862152", "2862152"]をsendしたところ、想定通りServer responseが2増えた。

これを利用して、「一気に10万要素のarrayを送信する」処理を10回繰り返すことで、DoSせずにflagを得ることができる。具体的にはDevtoolで以下のコードを実行すればよい(省略しているが、send(arr)は10回実行する)。

arr = new Array(1000000).fill("2862152")
send(arr)
send(arr)
(略)
send(arr)

これでServer responseの部分にflagが表示された(flagの文面からして嘘解法っぽいが気にしない)。

FLAG{N0nCE_reusE_i$_FUn}

*1:この値はburpsuiteで通信内容を見ることで得た