2023年の備忘録と2024年の抱負
こんにちは @prprhytです。 2023年の備忘録をまとめつつ、2024年の抱負を書きます。 2023年は東京を離れたり、博士後期課程に進学するなど生活に大きな変化がありました。
そういった近況報告と数年後に初心にかえりたくなった時に振り返れるように筆を取ることにしました。 なお、この記事のすべての文章は主観に基づいて書かれています。あらかじめご了承ください。
まず、2023年の振り返りを3行で整理すると以下の通りです。
- 社会人3年目 (2021年春に修士修了&就職)
- 東京を離れて自己研鑽として2023年4月に博士後期課程に進学しました(つまり業務ではない)
- 昼間はセキュリティエンジニア、朝と夜は博士後期課程で応用暗号学分野の研究をしています
以下は詳細です。2023年度はまだ3ヶ月ほどありますがここまでの時点での振り返りを書いておきます。
社会人2023の振り返り
3年目も引き続き、新卒で入社した企業の開発寄りのセキュリティエンジニアとして働いています。 参考: 21新卒セキュリティエンジニアの就活をした - 技術めいた何か
社会人としての振り返りは直接的な仕事の内容と関係ない、継続したいソフトスキルについてのみ記録しています(後述)。 仕事の内容についてはここに書くのは不適切なので、別の機会にアウトプットしていきたいと思っています。
博士後期課程2023の振り返り
ここでの博士課程は明記しない限りは社会人コースではない一般の博士後期課程(3年間〜)を指します。 また、具体的な大学名については書いていません。
研究について
2023年 4月から情報分野の博士後期課程に在籍しています。 修論は共通鍵暗号プリミティブの安全性解析に関する研究でした。 一方で、博士後期課程で取り組んでいる主な研究分野は応用暗号学(Applied Cryptography)で、2023年は特に暗号プロトコルの安全性解析に注力していました。
なお2023年の研究業績は以下のとおりでした (括弧内は内訳)。
- 査読付き論文誌: 1件 (主著1件)
- 研究会(査読なし): 4件 (主著2件、共著2件)
- 受賞: 2件 (主著2件)
修士修了(2021)から博士課程進学(2023)までの間の2年間についても業務時間外で研究や論文執筆をしていたのが実を結び、 博士課程の1年目で論文誌の採録と2件の表彰をいただけました。 主著者としてのオーサーシップを持って取り組みましたが、 共同研究者の皆様、前指導教員、現指導教員の指導のおかげで良い研究ができたと感じています。この場を借りて改めてお礼申し上げます。
現在の所属研究室
現在の指導教員は前指導教員の紹介で私が学部生の頃から共同研究をしており、指導をいただいています。 博士課程の進学については2-3年前から先んじて前指導教員に相談していました。最終的には前指導教員の勧めもあって現在の指導教員の研究室へ進学する運びになりました。
そういった経緯もあり、修士時代とは別の大学で過ごしていますが前の研究室とも協業したり、後輩の研究のサポートを継続しています。 良い縁は大事にしていきたいので、現研究室での研究や会社業務の負担にならない範囲で続けたいと思っています。
職場への進学の説明
職務を遂行する義務があるので、事前に職場の関係者への説明、上司(とその上司 etc... ) への説明を済ませ、許可を得た上で受験をしました。 なお、進学については上司や同僚から反対はされず快く応援していただきました。
自己研鑽として進学
自己研鑽としての進学なので研究は全て所属企業の業務とは関係ない形で実施しています。 業務時間外の個人的な活動なので当然ですが、入学金や学費などの進学にかかる費用は全て自身の財布から支払っています。
最近は企業が博士課程の学生や従業員の博士課程への進学を支援する場合もあるようです。1 2
私のように時間もお金もプライベートから捻出する場合は中々にハードです。 特に時間が足りないです。
そのため、博士後期課程に進学することを考えている社会人の方は金銭的な部分よりもライフスタイルが研究にマッチするか事前に考えることをお勧めします。 あるいは企業から業務時間の一部を研究使用可能などの時間的部分の支援があれば進学のハードルは下がるかもしれません。
一方で、自己研鑽として進学することはデメリットばかりではなく、2つのメリットがあると私は感じています。
比較的自由にテーマを設定できる: 基本的には会社業務として研究を実施するよりは自由にテーマの設定ができる可能性があると思います (予算、人員などの大学研究室側の都合で自由に選べない可能性はありますが)。 ただし、特に業務と隣接した分野に取り組む場合は、企業の知的財産を侵害しないように気を付ける必要があります。 そのため、場合によっては所属企業と大学研究室の両方に相談した上で共同研究契約を結んだ方が良い場合もあるかと思います。
その点をクリアできれば所属企業とは異なる環境で業務とは違うことにチャレンジする良い機会になると思います。
博士取得後の就業に関する縛りがない: 企業や支援制度によって有無や支援に関するルールが異なるため具体例への言及はしません。しかし、企業からの支援を受ける場合は博士号の取得後の就業に関する規約が存在する場合があります (取得後N年以内に離職する場合は支援金の返還が必要とか)。 私自身は自己研鑽として100%プライベートとして取り組んでいるのでそのような縛りを気にしなくて良く、キャリアを考える時に気持ちが楽だと感じています。3
このようなメリットの一方で以下のようなデメリットが存在します。
学会出張に有給休暇を使う必要がある 研究活動は会社業務ではないので自明ですが、博士課程としての学会出張やワークショップ、合宿への参加は全て有給休暇を使用しました。 特に2023年は引っ越しや病欠のためにも数日の有給を使ってしまったため、年末時点の有給の残り日数はごくわずかでした。 2025年は博論の執筆がある予定なので、2024年の有給は計画的に使い、繰り越せるようにするつもりです。
仕事の予定と大学院の予定が衝突した時の調整が難しい これも同様に自明ですが、一方の都合でもう一方をリスケするのは簡単ではありません。 私の場合は幸い、会社の打ち合わせが午前中に入る可能性はほとんどないので大学院の予定を午前中に入れることで予定の衝突を回避しています。 ただ、稀に衝突することがあります。その場合は可能な限り会社業務の都合を優先しています。
メンタルや体調不調時の影響が大きい 研究テーマを業務と異なる内容に設定して、分けて実施していたとしても自分自身は一人で同一人物です。 つまり、メンタルや体調の不調は研究と業務の両方に悪影響を及ぼすわけで、最悪の場合は共倒れの危険があります。
私自身は2023年の9月末にCOVID-19に罹患してしまい、このリスクをよく理解できました。 幸い、仕事の締め切りの問題はありませんでした。 その一方で博士課程の半期の研究報告書の締め切りの直前でした。 高熱で大変苦しい状況でしたが、私の場合は対外発表済みの研究があり、それさえまとめれば良かったことが幸いし、事なきを得ました。 報告書の提出後は快復するまでは仕事も研究も止めたのですが、仕事だけしていた2022年と比べると寝込んでいる時間を1.5倍くらい損している気持ちになり4、 精神衛生上はとてもよくなかったです。
2024年も継続したいこと
継続的な文献のサーベイ、ドキュメンテーション
知財が関係ない執筆や文献調査のソフトスキルを伸ばして研究と業務でうまく活用できました。
修士時代からこの類のタスクは継続していましたが2023年は特に効率的に実施できたと思います。 改善できた理由は推測ですが、 博士課程で研究を進めるにあたってより多くの文献を読むようになり、1週間あたりの文献調査の経験値が2022年比で倍以上に増えたことが一番大きいと思います。 また、文章の執筆や校正に関するソフトスキルについては、指導教員と共同研究者との論文執筆、条件付き採録になった論文誌の修正作業を通して多くの学びを得ました。 それらをうまく仕事でのドキュメンテーションを含む文章の執筆に反映できたと感じています。
フットワークを軽くすること
機会があれば積極的に出社する: 進学のために会社のある東京を離れてしまいました。 入社がCOVID-19の流行と被ったこともあり、仕組みが整ったおかげで在宅でも仕事はできます。 ただ、私は特に大学研究室に行くようになってから、オフラインのやり取りが モチベーションの維持にプラスに働くと感じるようになりました。 また、私の周りではオンラインだと雑な雑談が発生しないことが多く、Slackで絡む人が固定されがちでした。 そういったこともあり、職場の空気感を知るために仕事の用事や職場の懇親の機会があれば積極的に出社するようにしていました。5
そういった気持ちもあって、2023年は職場の人を知り、自身の近況を知らせる機会を大事にしていましたがこれは2024年も継続していきたいです。
ワークショップ、合宿への参加: 修士号を取得した出身研究室からの誘いで研究室合宿や、複数大学合同のワークショップへ参加しました。 どちらも学生時代には毎年参加していたイベントなのですが、2021, 2022年は感染症の流行の都合で中止になっていました。 学部や修士の学生とも研究の話ができて有意義な時間を過ごすことができました。 特にワークショップは久々にお会いした他大学の先生方に近況のご報告をして、研究の話をでき大変楽しい時間でした。
飲み会への参加: 決して体育会ではないですが、飲むのは好きなので呼んでください。 (都合が合えば)飛んでいきます。
博士課程の研究時間の確保
12月は仕事が忙しくない時は平日は以下のタイムラインで一日を過ごしていました。 2024年も体を壊さない範囲で継続していきたいです。 所属している博士後期課程はほとんど授業がないので大学院の時間のほとんどは研究をして過ごしています。
- AM 6:00: 起床。
- AM 6:30: 業務開始。
- AM 8:00: 業務中断、大学へ移動(or自宅で)研究開始。
- AM 12:00前後: 研究中断後、帰宅。業務再開。
- PM 7:00前後: 業務終了、夕食ののちに研究再開。
- PM 12:00前後: 就寝。
ある時期の数週間は1時-5時の4時間睡眠をしていたのですが、 パフォーマンスが低下した上に疲れが顔に出ていたらしく、まわりから心配されたのでやめました。 体を壊しそうなので2024年はしっかり寝たいです。
2024年に新しく頑張りたいこと
運動と体力作り
運動していない+ストレス+飲酒で体重が増え、体力が落ちてしまいました。 元々が痩せ型だったのでむしろ健康説は...ないです。健康な肉体を得るべく、なるべくランニングをしようかなと思っています。 また、目標の設定があった方が良いと思いマラソンに応募する計画を立てています。
英語学習を再開する
所属企業では運良く多国籍のチームに所属しており語学学習のモチベーションは比較的維持しやすいです。 しかし、仕事の英語には慣れてきた6ことと博士課程の研究に時間を割いていたこともあって2023年はほとんど英語学習に時間を割り振りませんでした。 そのためか、仕事以外で同僚と長時間話す機会があったときに思ったよりも上手には話せませんでした。 特に日常会話の定型表現を忘れていたり7、日常会話で使いそうなフレーズを新しく覚えていない気づきがあり、 リハビリも兼ねてDuolingoを始めています (ステマでは無いです)。
新規分野の勉強とテーマの発掘
もちろん暗号分野の中での新規分野/テーマです。 一応、共通鍵暗号プリミティブ→暗号プロトコルを経験していますが、 博士人材としてこの先、生きのこるにはこの訓練を繰り返し行う必要があると感じています。 しかし、テーマを変えすぎると博論をまとめる時に大変だろうと予想しているので、慎重に考えているところです。
おわりに
進学の動機や経緯の詳細は全く書いてませんが、それは2022年の出来事なので別の機会に。 博士課程の研究や社会人学生生活について詳しく聞きたい人がいたら 学会やイベント8で捕まえてください。
ちなみに、Twitter(X)では研究の話はしてません。酒と猫の話をしています 🍺 🐈
- 富士通の事例 日本初、富士通の「卓越社会人博士制度」とは~アカデミックな研究と社会課題解決を両軸で支える - フジトラニュース : 富士通↩
- メルカリの事例 メルカリ、社員の博士課程進学を支援する制度「mercari R4D PhD Support Program」を開始 | 株式会社メルカリ↩
- この感想は転職の意思表示ではありません。念の為。↩
- 仕事が止まって有給も消える上に研究は進まないため。↩
- つまりは飲み会の機会があれば行っていたという話です。↩
- 個人の感想です。同僚や後輩が英会話を頑張っていて少し焦っています。↩
- 酒で揮発しないくらいに定着させたい。↩
- 最近はITのイベントへほとんど参加してません...↩
SECCON CTF 2021 Writeup(Crypto: cerberus)
チームlazy_oracleで参加しました。(一人チーム)
ブロック暗号問があったら解きたいなと思っていたところ、cerberusが該当問だったので解いてみました。
---追記(2021-12-13): 作問者のkurenaifさんからコメントを頂きました。
---追記終わりwriteupありがとうございます!実はc1,c2,c3を3つ繰り返した後、c1を更にもう一度付け足すと少し楽にできます!
— kurenaif@VTuber (@fwarashi) 2021年12月12日
まず、チャレンジャーには以下の情報が与えれます。
- ソースコード
- 問題サーバーのurlとポート番号
ソースコードを読むと、以下のことがわかりました。
詳細を見ると、
c = EncAES-128-PCBC(flag, key, iv)
のように鍵長128bitのAESのPCBCモードでflagが暗号化されているようです。
また、クライアントから受け付けたivと暗号文を使用してサーバーは復号処理を行い、最終ブロックのパディングチェックの結果を返します。
サーバーから与えられるパラメータは
- 暗号文c
- iv
また、サーバーのソースコードを見てみると入力に以下の条件があることがわかります。
- クライアントから受けとった暗号文がもとの暗号文と前方一致しているかチェック
- 復号した平文に対してパッディングチェックをし、成否を出力
つまり、今回自由に操作できるのは以下の2つであることがわかります。
- iv
- もとの暗号文c以降に任意のバイナリを連結すること
条件1,2の該当部分のソースコード
while True: c = base64.b64decode(input("spell:")) iv = c[:16] c = c[16:] if not c.startswith(ref_c): print("Grrrrrrr!!!!") continue m = decrypt(iv, c) try: unpad(m, block_size) except: print("little different :(") continue print("Great :)")
そして、条件2からPadding Oracle攻撃が狙えそうです。 ただし、PCBCモードは下の図のように2ブロック目以降は暗号文ブロックと平文ブロックが後ろのブロックの復号に影響を与えます。 なので、Padding Oracleをするために任意の入力を作るのは工夫が必要です。
ここで、1ブロック目に着目します。 1ブロック目であればIVを制御するだけでPadding Oracle攻撃ができそうです。 ただし、条件1の制限があるため、オリジナルの暗号文cのうち1ブロック目だけを素朴に切り出してサーバーに入力することはできません。 なので、2ブロック目以降を打ち消す方法を考えます。
ここで、排他的論理和の性質を思い出します。 排他的論理和の真理値表は次のとおりです。 排他的論理和はA==Bのときに0を出力します。 つまり、同じ値を持つもの同士を打ち消す性質があります。
A | B | A xor B |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
つまり、以下の図のように暗号文cに2ブロック以降の暗号文C3, C2, C1を連結すれば2ブロック目以降を打ち消して1ブロック目の出力P0の値のみを最終ブロックにもっていけそうです。
しかし、このままだとC0とC1の影響が残っているので、ivを操作して打ち消します。
ここまでくれば、ivを操作してPadding Oracle攻撃で1ブロック目のDec(C0)(図赤丸部分)を求めて
P0 = Dec(C0) xor iv
とすることで、平文P0を復元できます。
また、2ブロック目は復元したDec(C0)を使い同じ方針で3ブロック目以降を打ち消した後にPadding Oracle攻撃でDec(C1)を復元します。
その後、P1 = Dec(C1) xor P0 xor C0
とすることで平文P1を復元できます。
3ブロック目以降も同様です。
Solverは以下のとおりです。
SECCON{v._.^v-_-v^._.^_S0und_oF_0rpHeUs_Aha~~}
import socket import base64 from Crypto.Cipher import AES from Crypto.Random import get_random_bytes from Crypto.Util.Padding import pad, unpad from Crypto.Util.strxor import strxor block_size = 16 #---------- # Netcat class: ref https://scrapbox.io/progfay-pub/netcat.py class Netcat: """ Python 'netcat like' module """ def __init__(self, ip, port): self.buff = "" self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.socket.connect((ip, port)) def read(self, length = 1024): """ Read 1024 bytes off the socket """ return self.socket.recv(length) def write(self, data): self.socket.sendall(((str(data)+'\n')).encode('utf-8')) def close(self): self.socket.close() #---------- def unpack_ct(b64string): a = base64.b64decode(b64string.encode('utf-8')) iv = a[:16] c = a[16:] return iv, c def pack_ct(iv, c): return base64.b64encode(iv+c).decode('utf-8') def recover_plaintext_block(iv, c, mask0=b"\x00"*block_size, mask1=b"\x00"*block_size): attack_query = c attack_query_iv = iv iv0 = b'\x00'*block_size iv0 = [iv0[i : i + 1] for i in range(0, len(iv0))] state = [None]*block_size for b in range(block_size): for j in range(b): # ターゲットより下位バイトをPadding iv0[block_size-1-j] = strxor(state[15-j], bytes([b+1])) for i in range(256): iv0[block_size-1-b] = bytes([i]) iv0_t = strxor(b"".join(iv0), attack_query_iv) nc.write(pack_ct(iv0_t, attack_query)) lines = nc.read().decode('utf-8').split('\n') if -1!=lines[0].find("Great"): print('found!') state[15-b] = strxor(bytes([i]), bytes([b+1])) break if 255==i: print('cannot find') exit(-1) plaintext=strxor(b"".join(state), mask0) plaintext=strxor(plaintext, mask1) return plaintext, b"".join(state) nc = Netcat('cerberus.quals.seccon.jp', 8080) lines = nc.read().decode('utf-8').split('\n') for line in lines: print(line) iv, c = unpack_ct(lines[-2]) num_of_block = len(c)//block_size print("num of block: %d" %num_of_block) print("recover p0...") attack_query_iv = strxor(c[:16], c[16:32]) attack_query_ciphertext = c+c[48:64]+c[32:48]+c[16:32] p0, state_c0 = recover_plaintext_block(attack_query_iv, attack_query_ciphertext, mask0=iv) print("p0 is:") print(p0) print("---") print("recover p1...") attack_query_ciphertext = c+c[48:64]+c[32:48] attack_query_iv = strxor(c[:16], c[16:32]) attack_query_iv = strxor(attack_query_iv, c[32:48]) attack_query_iv = strxor(attack_query_iv, state_c0) p1, state_c1 = recover_plaintext_block(attack_query_iv, attack_query_ciphertext, mask0=c[0:16], mask1=p0) print("p1 is:") print(p1) print("---") print("recover p2...") attack_query_ciphertext = c+c[48:64] attack_query_iv = strxor(c[:16], c[16:32]) attack_query_iv = strxor(attack_query_iv, c[32:48]) attack_query_iv = strxor(attack_query_iv, c[48:64]) attack_query_iv = strxor(attack_query_iv, state_c0) attack_query_iv = strxor(attack_query_iv, state_c1) p2, state_c2 = recover_plaintext_block(attack_query_iv, attack_query_ciphertext, mask0=c[16:32], mask1=p1) print("p2 is:") print(p2) print("---") print("recover p3...") attack_query_ciphertext = c attack_query_iv = strxor(c[:16], c[16:32]) attack_query_iv = strxor(attack_query_iv, c[32:48]) attack_query_iv = strxor(attack_query_iv, state_c0) attack_query_iv = strxor(attack_query_iv, state_c1) attack_query_iv = strxor(attack_query_iv, state_c2) p3, _ = recover_plaintext_block(attack_query_iv, attack_query_ciphertext, mask0=c[32:48], mask1=p2) print("p3 is:") print(p3) print("---") p_all = p0+p1+p2+p3 flag = unpad(p_all, block_size)[16:].decode('utf-8') print(flag)
SECCON Beginners CTF 2021 Writeup(Crypto: Logical_SEESAW, GFM, Imaginary)
2021/5/22-23にオンラインで開催された ctf4b 2021にチームr0b5stのメンバーとして参加しました。
主にCRYPTO問を担当しました。write upを記録します。
Logical_SEESAW
ソースコードが与えれている。 同じ平文に対して、AND演算でマスクされた16個のデータが与えられる。 16個すべてのORを取るとflagが手に入る。
from Crypto.Util.number import long_to_bytes cipher = ['11000010111010000100110001000000010001001011010000000110000100000100011010111110000010100110000001101110100110100100100010000110001001101000101010000010101000100000001010100010010010001011110001101010110100000010000010111110000010100010100011010010100010001001111001101010011000000101100', '11000110110010000100110001000000010001001111010010000110110000000110011010101110011010100110000010100110101100000100000000001110001001001000101000001010101001100000001010101010010110001001110001101010110100000110011010010110011000000100100011010000110010001001011001101010011000001101100', '11000010110010000100110001101000010001001111001000000110000100000110011000111110010010100110000000101110101101100000000010010110001001101000101010000010101000100000001000101110000010001001111001101010110100000010011010010110011000100000100011010010100010001001111001101010011000001101100', '11000110110010001100110000101000110001000111000000000110000000000110011010101110011000100110000010100110101101100100100010000100101001001000101010000010101000100000001010100110010010001001110001101010110000000110000010110110000000000000100011010010110010001011011001101010001000000111101', '11000110100010001100110000000000010001000111001010000110110100000110011010111110001010100110100011101110101110100010000000110100001001101000101010000010101001101000001000101000010010001001111001101010110000000010000010111110000000000010000011010010100010001001011001101010011000001111101', '11000010110010001100110001001000010001000011000000100110000000000110011010101110000010100110100011100110101110100010000000101100101001101000101010000010101001101000001010100010000010001011111001101010110100000110001010010110001010100100000011010010110010001011111001101010011000000101100', '11000110110010000100110001101000110001001011010000100110110000000110011000101110010000100110100001100110100110000000100010000110101001001000101010000010101001100000001000101110010010001011111001101010110000000010001010110110001010100110000011010000110010001011111001101010011000000101100', '11000010110010000100110000100000010001000111011000100110100000000110011000111110000010100110000001101110101111100100000010111110001001001000101000001010101001101000001000101010000110001011110001101010110000000110001010011110000010100010100011010010110010000011011001101010001000000111100', '11000010101010000100110001001000010001000011000000100110010000000100011000111110011000100110000001100110100101000010000000011100101001101000101000001010101001101000001010101110010110001001110001101010110000000010010010110110011000000010100011010000100010001011111001101010001000000101100', '11000010101010001100110000100000010001001111001010000110000000000100011010101110011000100110000011100110100111100110100000000110001001001000101010000010101000100000001000101100010010001011110001101010110000000110011010010110011010000000000011010010100010001011011001101010011000001101101', '11000010101010001100110001000000010001001011010010000110010100000100011000111110011000100110000010100110100111000100000000000100101001101000101010001010101000100000001000100000000110001001111001101010110000000110011010010110000010000100100011010000110010000011011001101010001000001101100', '11000110101010001100110001000000110001001111001010000110110000000110011010101110011000100110100001100110101111000100100010011110101001001000101010001010101000101000001000101100000110001011111001101010110100000010011010011110001000000100100011010010100010000001011001101010011000001111100', '11000110100010001100110001000000010001001011011010100110000000000100011000101110001000100110100001101110101101000110100010001100101001001000101010000010101000100000001010101100000010001001111001101010110100000110011010010110010000100110100011010010110010001001111001101010011000001101101', '11000110101010000100110000000000010001001111001010100110100100000100011010111110001000100110100001101110101100000000100000111110001001101000101000001010101001101000001010100110010010001011110001101010110100000110000010010110001010000010100011010010110010001001011001101010001000000101100', '11000010101010000100110000000000110001001011011010100110110000000110011000101110010010100110100000100110101111000010000000100100001001001000101000001010101001100000001000100000000010001011110001101010110000000010011010011110001010000000000011010010100010001001011001101010001000000101101', '11000110101010001100110001000000110001001111011000000110010100000100011000101110001010100110000001101110101110000100100000101110101001101000101000000010101000100000001010101010000010001011110001101010110000000010000010010110001000100100100011010000100010000001011001101010001000001111101'] c = [list(i) for i in cipher] part_of_c = [int(j) for j in c[0]] for i in c[1:]: tmp = [int(j) for j in i] part_of_c = [j|k for j,k in zip(part_of_c, tmp)] s = [str(i) for i in part_of_c] b = int("".join(s), 2) ret = long_to_bytes(b) print(ret.decode('utf-8'))
ctf4b{Sh3_54w_4_SEESAW,_5h3_54id_50}
GFM
ソースコードとMatrixとしてKey, encが与えられている。 encはMatrix M(=flag)とKeyを用いて次の演算で得ている。
enc = Key * M * Key
行列の積が非可換であることに注意して、次の式で Mを得る
M = Key^-1 * enc * Key^-1
この問題を解くときに docker版のsagemath/sagemathhttps://hub.docker.com/r/sagemath/sagemath/ を使いました。
SIZE = 8 p = 331941721759386740446055265418196301559 MS = MatrixSpace(GF(p), SIZE) key = MS.matrix([116401981595413622233973439379928029316,198484395131713718904460590157431383741,210254590341158275155666088591861364763,63363928577909853981431532626692827712,85569529885869484584091358025414174710,149985744539791485007500878301645174953,257210132141810272397357205004383952828,184416684170101286497942970370929735721, 42252147300048722312776731465252376713,199389697784043521236349156255232274966,310381139154247583447362894923363190365,275829263070032604189578502497555966953,292320824376999192958281274988868304895,324921185626193898653263976562484937554,22686717162639254526255826052697393472,214359781769812072321753087702746129144, 211396100900282889480535670184972456058,210886344415694355400093466459574370742,186128182857385981551625460291114850318,13624871690241067814493032554025486106,255739890982289281987567847525614569368,134368979399364142708704178059411420318,277933069920652939075272826105665044075,61427573037868265485473537350981407215, 282725280056297471271813862105110111601,183133899330619127259299349651040866360,275965964963191627114681536924910494932,290264213613308908413657414549659883232,140491946080825343356483570739103790896,115945320124815235263392576250349309769,240154953119196334314982419578825033800,33183533431462037262108359622963646719, 53797381941014407784987148858765520206,136359308345749561387923094784792612816,26225195574024986849888325702082920826,262047729451988373970843409716956598743,170482654414447157611638420335396499834,270894666257247100850080625998081047879,91361079178051929124422796293638533509,34320536938591553179352522156012709152, 266361407811039627958670918210300057324,40603082064365173791090924799619398850,253357188908081828561984991424432114534,322939245175391203579369607678957356656,63315415224740483660852444003806482951,224451355249970249493628425010262408466,80574507596932581147177946123110074284,135660472191299636620089835364724566497, 147031054061160640084051220440591645233,286143152686211719101923153591621514114,330366815640573974797084150543488528130,144943808947651161283902116225593922999,205798118501774672701619077143286382731,317326656225121941341827388220018201533,14319175936916841467976601008623679266,112709661623759566156255015500851204670, 306746575224464214911885995766809188593,35156534122767743923667417474200538878,35608800809152761271316580867239668942,259728427797578488375863755690441758142,29823482469997458858051644485250558639,137507773879704381525141121774823729991,29893063272339035080311541822496817623,292327683738678589950939775184752636265]) enc = MS.matrix([133156758362160693874249080602263044484, 293052519705504374237314478781574255411, 72149359944851514746901936133544542235, 56884023532130350649269153560305458687, 67693140194970657150958369664873936730, 227562364727203645742246559359263307899, 98490363636066788474326997841084979092, 323336812987530088571937131837711189774, 244725074927901230757605861090949184139, 63515536426726760809658259528128105864, 297175420762447340692787685976316634653, 279269959863745528135624660183844601533, 203893759503830977666718848163034645395, 163047775389856094351865609811169485260, 103694284536703795013187648629904551283, 322381436721457334707426033205713602738, 17450567396702585206498315474651164931, 105594468721844292976534833206893170749, 10757192948155933023940228740097574294, 132150825033376621961227714966632294973, 329990437240515073537637876706291805678, 57236499879418458740541896196911064438, 265417446675313880790999752931267955356, 73326674854571685938542290353559382428, 270340230065315856318168332917483593198, 217815152309418487303753027816544751231, 55738850736330060752843300854983855505, 236064119692146789532532278818003671413, 104963107909414684818161043267471013832, 234439803801976616706759524848279829319, 173296466130000392237506831379251781235, 34841816336429947760241770816424911200, 140341979141710030301381984850572416509, 248997512418753861458272855046627447638, 58382380514192982462591686716543036965, 188097853050327328682574670122723990784, 125356457137904871005571726686232857387, 55692122688357412528950240580072267902, 21322427002782861702906398261504812439, 97855599554699774346719832323235463339, 298368319184145017709393597751160602769, 311011298046021018241748692366798498529, 165888963658945943429480232453040964455, 240099237723525827201004876223575456211, 306939673050020405511805882694537774846, 7035607106089764511604627683661079229, 198278981512146990284619915272219052007, 255750707476361671578970680702422436637, 45315424384273600868106606292238082349, 22526147579041711876519945055798051695, 15778025992115319312591851693766890019, 318446611756066795522259881812628512448, 269954638404267367913546070681612869355, 205423708248276366495211174184786418791, 92563824983279921050396256326760929563, 209843107530597179583072730783030298674, 662653811932836620608984350667151180, 304181885849319274230319044357612000272, 280045476178732891877948766225904840517, 216340293591880460916317821948025035163, 79726526647684009633247003110463447210, 36010610538790393011235704307570914178, 284067290617158853279270464803256026349, 45816877317461535723616457939953776625]) # enc = key*M*key flag = key^(-1)*enc*key^(-1) print(flag)
得られた出力をflagにする
"".join(list(map(lambda x: chr(x), [99, 116, 102, 52, 98, 123, 100, 49, 100, 95, 121, 48, 117, 95, 112, 108, 52, 121, 95, 119, 49, 116, 104, 95, 109, 52, 116, 114, 49, 120, 95, 52, 110, 100, 95, 103, 52, 108, 48, 105, 115, 95, 102, 49, 101, 108, 100, 63, 125])))
ctf4b{d1d_y0u_pl4y_w1th_m4tr1x_4nd_g4l0is_f1eld?}
Imaginary
Flagの出力条件
self.numbers = dict() のindexに'1337i'があること。
self.numbers は暗号文を入力することで上書きできる(後述)
与えられている機能
- 実数と虚数を入力してself.numbersに保存する機能
ただし、int型の実数re, 虚数imのとき、
name = f'{re} + {im}i'
self.numbers[name] = [re, im]
の形式となる。また、入力をint()でキャストしているため、re, imともに空文字は弾かれる
任意の暗号文を入力でき、復号してjson stringであればself.numbersを上書きする機能
self.numbers暗号化し、hex形式で暗号文を出力する機能
- self.numbersのindexに'1337i'があればflagを出力する機能
利用している暗号
AESのECBモード。 ECBモードはブロック単位で見ると、同じ鍵のときにある平文ブロックに対してどのブロックで暗号化しても常に同じ暗号文を出力する。 (リナックスのペンギンの画像をECBモードで暗号化したものが有名)
方針
name = f'{re} + {im}i' self.numbers[name] = [re, im]
のname部分に'1337i'を入れることができればflagを得ることができる。
具体的には128bitごとのブロックが分割されることを意識しながら、
暗号文の2ブロック目の先頭が 1337i": [re, im]}
になるように調整する。
まず、実数に11111111111
、虚数に1337
と入力すると
暗号化時に 1ブロック目: Enc('{"11111111111 + ', key) 2ブロック目: Enc('1337i": [1111111', key) 3ブロック目: Enc('1111, 1337]}'+padding, key) となる。
また、一度サーバーとの接続を切ってから再接続し、
実数に1111111
、虚数に0
と入力すると
1ブロック目: Enc('{"1111111 + 0i":', key)
2ブロック目: Enc(' [1111111, 0], "', key)
3ブロック目: Enc('1 + 1i": [1, 1]}'+padding, key)
といった暗号文が手に入る。 ECBの性質によって、暗号文のブロックを入れ替えても暗号文として成り立つため、 2回目のクエリの1,2ブロック目と1回目のクエリの2,3ブロックをつなげてflagを通す条件を満たす、 M = {"1111111 + 0i": [1111111, 0], "1337i": [11111111111, 1337]} に対応する暗号文 C = Enc( {"1111111 + 0i": [1111111, 0], "1337i": [11111111111, 1337]}, key) を手に入れる。
最後に、もう一度サーバーから切断した後に再接続し、 手に入れた暗号文を入力すればflagが手に入る。
ctf4b{yeah_you_are_a_member_of_imaginary_number_club}
その他
CRYPTO以外はkawasin73、mopisecが担当してくれたので、CRYPTOに集中して取り組むことができました。 (そして、僕がField_tripやp-8RSAに苦しんでいる間に @mopisec君がreversingを全完してました。すごい。)
久々に時間をとって取り組みましたが、楽しかったです。 社会人になってしまいましたが、継続していきたいな〜という気持ちです。 Field_tripやp-8RSAは解けなくて、悔しかったのでぼちぼち精進します。
Challenge CVE-2020-13777に応募しました!
はじめに
編集履歴:
- 2020/07/03 18:23: 誤字脱字の修正
- 2020/07/03 13:25: 初版公開
本記事及び、企画「Challenge CVE-2020-13777」へ提供した解説文章・PoCは情報通信産業に関わる人の情報セキュリティのリテラシーの向上に貢献することを目的としています。 修正パッチが広く配布されている脆弱性であるCVE-2020-13777について、修正パッチ配布後に開発元によって公開された脆弱性の情報を元に技術的な検証を行った結果を啓蒙活動の一環として公開しています。 それにより、脆弱性について情報通信産業に関わる人に広く認知されることを期待しています。
なお、作成し公開したPoCは技術的な検証を目的に作成されたもので、実在のサーバーに対してCVE-2020-13777を用いた攻撃する能力は無く、また攻撃を意図して作成したものではありません。 このPoCは主催者が配布・解析を許可したパケット情報をまとめたファイル(pcapファイル)を解析するように設計しており、外部のサーバーとの通信は一切行わずオフラインで動作します。
こんにちは、@prprhytです。
先日開催されたChallenge CVE-2020-13777に応募しました。
Challenge CVE-2020-13777は、Ohtsuさん(@jovi0608)が主催する
先日情報が公開されたGnuTLSの脆弱性であるCVE-2020-13777についてTLS1.3に関係する部分の解説とPoCの公募をする企画です。
企画の詳細や結果の詳細についてはOhtsuさんのブログを参照してください。
告知・募集:
https://jovi0608.hatenablog.com/entry/2020/06/13/104905
Ohtsuさんのフォローブログ(結果と提出した解答へのフォロー):
解答:
https://gist.github.com/prprhyt/548ba3148f3b1bbfa5c20edde60d6b75
PoCのソースコード:
選考の結果提出課題を高く評価していただきまして、嬉しいことに主催者であるOhtsuさんからの正解賞(Amazonギフト1万円)と協賛のVさん(@voluntas)からVさん賞(Amazonギフト3万円とお好きなラムダノート本1冊)を頂きました。改めてありがとうございます。 特に、Vさんからは告知にあった額面より大幅にブーストしていただいた上、好きなラムダノート本として「プロフェッショナル SSL/TLS」をいただきました! 改めて、企画していただいたOhtsuさんと協賛のVさんにこの場を借りてお礼を申し上げます。 (あと、自分で言うのは少し恥ずかしいですが気合を入れて解説を書いたおかげかお2人からお褒めの言葉をいただきました。)
応募にあたって課題の進め方
注意: 以降の文章はOhtsuさんのフォローブログを読んでいる前提で書いています。 よって、セッション再開時の認証の説明など技術的な解説の多くは省略してあります。予めご了承ください。
CVE-2020-13777のTLS1.3に関係する部分の解説とPoCについての詳細はOhtsuさんのフォローブログを見ていただくとして、ここでは問題2を例に実際に手をどう動かして課題を進めたかについて共有しようと思います。 ただし、僕は課題への対処の仕方は十人十色だと思っていて課題の進め方に正解があるとは思っていません。 なのでこういうやり方もあるのだな、くらいに思ってもらえると幸いです。
公募の問題は次の通りです。
以下のレポジトリにある pcap データを使って次の問題1,2に回答してください。
pcap データにはGnuTLS-3.6.13のサーバ(192.168.100.23:5556)に対するTLS1.3の接続データが2つ含まれています。1回目は新規TLS1.3接続、続く2回目は 0-RTT のTLS1.3再接続です。https://github.com/shigeki/challenge_CVE-2020-13777 1. pcap中のTLS1.3 ClientHelloデータだけ使って、CVE-2020-13777によってTLS1.3のMITMが可能であることを証明してください。
2. pcap中の暗号化されたTLS1.3 の 0-RTTアプリケーションデータをCVE-2020-13777によって復号し、アプリケーションデータの平文を取得してください。
さて、本題ですが僕は課題2を次の手順で進めました。
- CVE-2020-13777のIssueに上がっている動作を再現できる環境の構築
- 配布されたpcapを眺める
- ドキュメントとプログラムの調査とメモの作成
- PoCの実装
- PoCの実装で行き詰まった箇所があれば2や3に戻る
また、調査やPoCの実装に使用したツールは次の通りです。
- Wireshark: 配布されたpcapを眺めるのに使用
- ブラウザ・Google: RFCを読んだり参考になりそうな情報の検索をするのに利用
- テキストエディタ: メモ用
- Python3.7: PoCの実装に使用
なお、課題開始時点でのTLS1.3に関する知識は次のとおりでした。
- おおまかな特徴やTLS1.3の暗号理論における安全性についての記事を読んだことがある。
また、TLS1.3への攻撃の一部について概要を知っている。 - プロトコル的な観点についてはなんとなくTLS1.2までとTLS1.3の違いを知っているが詳細は知らない
- フルハンドシェイクが2-RTT->1-RTTになった。条件を満たせば0-RTTハンドシェイクが使えるとか。上記の記事にある概要は知っているくらい。
- 0-RTT時の認証についてはあまり詳しくない。
- 完全に忘れていたのですが、TLS1.3のPSKやセッション再開について2年前にセキュリティ・キャンプ全国大会2018 TLS1.3/暗号ゼミの応募課題のために少し調べていたことがわかりました。ただし、このときは理解できていなかった(この記事を書いている最中に当時の応募用紙を見直したらかなり間違いがあったので)
では、次の節から手順ごとにやったことを説明します。
ちなみに余談ですが、問題1については次のように進めました。
- PSK(Pre-shared key)をセッション再開に用いるという情報をもとにMITMのシーケンス図の下書きをする
(参照: RFC 8446 Section 2.2: https://tools.ietf.org/html/rfc8446#section-2.2) - 問題2を解いてPSKの導出やkey schedulingついて理解をする
- 下書きをと問題2を解く過程で得た情報をもとに提出用の解説を書く
1. CVE-2020-13777のIssueに上がっている動作を再現できる環境の構築
GnuTLSのGitLab上のCVE-2020-13777に関するIssueを見ながら報告されている動作を再現できる環境を構築します。
該当のIssue: https://gitlab.com/gnutls/gnutls/-/issues/1011
以下の環境を構築しました。
インストールするものは次の通りです。
- GnuTLS 3.6.13
- OpenSSL 1.1.1g (21 Apr 2020)
GnuTLSのバイナリを入手する方法はソースコードからのビルドや各パッケージマネージャーから入手するなどがあります。 各OSやディストリビューションによって手順が異なるので手順は割愛します。
2. 配布されたpcapをながめる
TLS1.3の仕様であるRFC 8446の2.2節によるとセッション再開に用いる情報であるPre-Shared Keyはセッション再開時のCHLOに含まれるようです。つまり、セッション再開時のCHLOのどこかにPSKに関する情報が含まれているはずです。
参照: RFC 8446 Section 2.2: https://tools.ietf.org/html/rfc8446#section-2.2
その情報を頭に入れた状態で、Wiresharkで配布されたpcapファイルを眺めました。 1つめのClient Hello(CHLO)をみてもセッション再開に関係しそうな情報は特になさそうです。 2つめのCHLOを見ます。すると、画像のようにPSK Identity->Identityの先頭の16byteが0になっているのに気づきます。 怪しいですよね。
図:配布されたpcapファイルの再開時のCHLOの中身
ここで正常なセッション再開のCHLOパケットを見てみます。
正常なセッション再開のCHLOパケットは修正済みのGnuTLSサーバー(ver 3.6.14以降)でGnuTLSの該当IssueのStep to Reproduce
の手順を参考に同じサーバーに対してセッション再開をすることで入手できます。
なお、逆に3.6.13以下で手順通りに実行するとCVE-2020-13777の認証バイパスが再現します。
図:正常な再開時のCHLOの中身
正常なCHLOを見ると先頭16byteが0になっているわけではありません。 では、手元でCVE-2020-13777の認証バイパスを再現した場合のセッション再開時のCHLOはどうでしょうか。次に画像を示します。
図:手元でCVE-2020-13777を再現したセッション再開時のCHLOの中身
確認すると、配布されたpcapのセッション再開時のCHLOパケットと同じように先頭16byteが0になっていることがわかります。
3. ドキュメントとプログラムの調査とメモの作成
ここで、おもむろにGnuTLSの暗号化されたセションチケットの書式を確認します。
図:GnuTLSのチケット書式
図の引用元: https://jovi0608.hatenablog.com/entry/2020/06/13/104905
図の左側のテーブルの一番上の項目を見てください。 Key Name(16bytes)となっています。 これらの情報から次の仮説を立てます。
- 暗号化されたセッションチケットはセッション再開時のPSK Identityに含まれている。
- そして、セッションチケットの暗号化/復号鍵は構造体で定義されており、それがall-0で初期化されるため、Key name(16bytes)や暗号化のkey, MACのkeyも0になっている。
この仮説に基づいて調査をします。 セッションチケットを復号できたとすると、攻撃者が入手できるのは図:GnuTLSのチケット書式の右側のテーブルにあるGnuTLSのチケットデータの中身の書式にある情報です。
順番に項目を見てresumption_master_secretやnonceがあるのが気になりました。名前からして秘密情報にあたりそうです。ここで、resumption_master_secretやnonceが何に使われているのかRFC 8446から確認します。 次の式を見ます。
psk=HKDF-Expand-Label(resumption_master_secret, "resumption", nonce, Hash.length)
参考:RFC8446 4.6.1 https://tools.ietf.org/html/rfc8446#page-75 を基に作成
Ohtsuさんフォローブログを読んだ方はもうおわかりだと思いますが、この式から、resumption_master_secretとnonceはセッション再開時の認証や各暗号化鍵/復号鍵の鍵導出に使うPSKの導出に使用されていることがわかりました。
次にPSKからの鍵導出についてTLS1.3のKey Schedulingの図を確認します。
図:TLS1.3のKey Scheduling
図の引用元: RFC8446 7.1 https://tools.ietf.org/html/rfc8446#page-93
これで、PSKからどのようにPSKから各暗号化鍵/復号鍵を導出しているかわかりました。
また、RFC 8446 section 7.3より、この図の中で0-RTT Application dataの暗号化/復号鍵, ivの基となる情報に該当するのは client_early_traffic_secretであることがわかりました。
参照:RFC 8446 section 7.3 https://tools.ietf.org/html/rfc8446#section-7.3
次の節ではこの節まででわかった情報をもとに配布されたpcap中の0-RTT Application dataを復号するPoCの実装をしていきます。
4. PoCの実装
PoCの実装に入りました。 さて、3節で次のように仮定しました。
- 暗号化されたセッションチケットはセッション再開時のPSK Identityに含まれている。
- そして、セッションチケットの暗号化/復号鍵は構造体で定義されており、それがall-0で初期化されるため、Key name(16bytes)や暗号化のkey, MACのkeyも0になっている。
この仮定にもとづいて実装を進めました。(そして、結果的にこの仮定は正しかったです)
なお、実装時もわからない箇所が出たら繰り返し調査をする方針で進めました。
0-RTT Application dataの復号までの流れは次のとおりです。
- 暗号化されたセッションチケットの復号, MACの検証
- セッションチケットに含まれるresumption_master_secretとnonceからPSKを導出
- TLS 1.3のKey Schedulingに従ってclient_early_traffic_secureを導出
- client_early_traffic_secureから0-RTT Application dataの復号鍵とivを導出し、ivからnonceを導出
- 0-RTT Application dataを復号, MACの検証
詳細はOhtsuさんのフォローブログやPoCのソースコードを参照してください。 なお、ソースコード中には各処理についてコメントしてあります。
以下にPoCで各処理を実装する上で特筆すべき点や再度調査を行ったものについて書きました。
主に実装依存のもので、RFC 8446に定義されていないもの中心です。
なお、ソースコードの検索についてはgitlabの検索+ローカルにgit clone
した上で、git grep
でやっていました。
1. 暗号化されたセッションチケットの復号, MACの検証 のMACの計算手順
セッションチケットが実装依存であり、確認した範囲ではRFC 8446に具体的な記載がなかったのでMACの算出方法についてはGnuTLSの実装を参照しました。 次の手順で各値を入力することで比較用のMACを計算できます。 GnuTLSの実装を見たところ、ちゃんとEncrypt-then-MACのようです。
- HMACでハッシュ関数はSHA1(図:GnuTLSのチケット書式より)に指定する
- HMACの鍵を入力(all-0で16byte)
- セッションチケットのKey Nameを入力
- セッションチケットのIVを入力
- セッションチケットのdata length(暗号化部分の長さ)
- セッションチケットdata(暗号化された状態のデータ)を入力
- MACを計算する
余談ですが、GnuTLSでのセッションチケット復号時のMACの計算関数の呼び出し箇所を見ると ダイジェストを受け取る変数名がcmacとなっており、そのせいで最初見たときはMACの方式がCMACなのかと勘違いしかけました。ハッシュ関数の指定があるのでCMACだとおかしいと思いながらコードを読み進めてHMACだと確認しました。 (おそらくcalculated MACの略のつもりなのでしょうが紛らわしかったです...)
2以降で使用するHKDFのハッシュ関数の決定方法
HKDFで使用するハッシュ関数の指定方法について確認をしたところ、GnuTLSでは図:GnuTLSのチケット書式の右側GnuTLSのチケットデータの中身の書式の先頭2byteのprf_idが該当することがわかりました。
なお、上記のリンク先で呼び出している_tls13_expand_secret2
はRFC 8446でいうとHKDF-Expand-Label()にあたります。
参照: https://gitlab.com/gnutls/gnutls/-/blob/d00638997fa269a975095d852633b48b2b64fbf9/lib/secrets.c#L122
そして、配布されたpcapの再開時のCHLOではprf_idは7(10進数)です。
prf_id==7の場合の対応するハッシュ関数を調べたところ、定数GNUTLS_MAC_SHA384
に7が割り当てられていることがわかりました。
そして、定数名からprf_id==7に対応するハッシュ関数はSHA384であることがわかりました。
なお、調査手順は次のとおりでした
- ciphersuiteが定義されていそうなソースファイル(lib/algorithms/ciphersuites.c)をみてみる
- ハッシュ関数に関するものもいくつか参照されていることがわかった
- 試しに
git grep GNUTLS_MAC_SHA256
する - gnutls/devel/libdane-latest-x86_64.abi が検索にかかり、具体的な値を紐付けていそうなので確認する
- 7に該当するのが
GNUTLS_MAC_SHA384
だとわかる
参照:
lib/algorithms/ciphersuites.c: https://gitlab.com/gnutls/gnutls/-/blob/e48290a51da19288986bd7aaca265ea62b054dc8/lib/algorithms/ciphersuites.c#L407
gnutls/devel/libdane-latest-x86_64.abi: https://gitlab.com/gnutls/gnutls/-/blob/e48290a51da19288986bd7aaca265ea62b054dc8/devel/libdane-latest-x86_64.abi#L304
4. TLS 1.3のKey Schedulingに従ってclient_early_traffic_secureを導出
当初、うまくclient_early_traffic_secure
が算出できなかったのですが、
以下のような誤りをしていたのが原因でした。
client_early_traffic_secure = Derive-Secret(Early Secret, "c e traffic", ClientHello)
とすべきところを以下のようにしていました。
client_early_traffic_secure = Derive-Secret(Early Secret, "c e traffic", "")
Key Schedulingの図を見直すことで解決しました。
5. 0-RTT Application dataを復号, MACの検証
AEADのAssociated(additional_data)の計算方法
0-RTT Application dataの暗号化には1つ前のセッションのCipher Suiteが選択されます。
そして、1つ前のCHLOを見ると先頭にCipher SuiteのTLS_AES_256_GCM_SHA384(0x1302)が選択されていました。
そのことから、AES_256_GCMで暗号化されていて、MACの計算にはSHA384が使用されていると仮定しました。
そして、AEADの暗号化/復号に必要なAssociated(additional data)の算出方法は次の通りです。
additional_data = TLSCiphertext.opaque_type || TLSCiphertext.legacy_record_version || TLSCiphertext.length
よって、AEADのassociate=(Early dataが入っているTLSレコードレイヤーのヘッダ+暗号文の長さ) であることがわかりました。
PoCの実装・調査の記録については以上です。 余談ですが、実行結果は次の通りです(ASCII, HEX)。
python3 main.py b"Let's study TLS with Professional SSL/TLS!\n\n\x17" 4c6574277320737475647920544c5320776974682050726f66657373696f6e616c2053534c2f544c53210a0a17
実はお好きなラムダノート本にProfessional SSL/TLSを選んだ理由の1つはこれです。 (もう一つの理由は読んだことがなかったからです。あと評判が良いので。)
最後に
TLS1.3の再接続性まわり、特に0-RTT周りは課題に取り組む前は雰囲気でしかわかっていなかったのですが、この機会に楽しく理解を深めることができました。 改めまして、企画を主催していただいたOhtsuさん(@jovi0608)、協賛のVさん(@voluntas)にお礼を申し上げます。 今後ですが、OhtsuさんからプロフェッショナルSSL/TLSの書評をお願いされたので、時間を見つけて読み進めようと思います。(今年は修論もあるので纏まった時間をとるのが難しいかもですが...) また、Vさんからは頂いた賞金で経済を回すように言われたので経済を回していくぞ〜という気持ちです。 やっていくぞ💪
SECCON Beginners CTF 2020 Writeup(Crypto: R&B, RSA Calc)
2020/05/23 〜 2020/05/24に開催されたSECCON Beginners CTF 2020にTeam: r0bu5tのCrypto問担当(?)として参加しました。prprhytです。 せっかくのアウトプットする機会なので、手をつけた分は復習も兼ねてWriteupを書きます。
僕自身は解いたのは2問だったのですが、チームメンバーのおかげで順位は44thでした。 反省点も多いので貢献できるように精進します。。
Writeup
crypto: R&B
与えられたもの
- 符号化されたFlag
- 符号化のソースコード
与えられなかったもの
- FORMAT(後述)
符号化されたFlagと符号化を行った際に使われたらしいソースコードが渡されます。 ソースコードを読むと FORMATという文字列に従って、rot13とBase64で符号化するようでした。 例えばFORMAT='BRBB'の時はBase64->rot13->Base64->Base64と符号化するプログラムのようでした。 厳密には符号化したあとにBase64で符号化した場合はB、rot13で符号化したときにはRが符号化後のFLAGの先頭に足されるので、符号化されたFLAGの先頭から順にBかRかを見て対応するデコーダーで復号すればflagを得られました。 最初はちゃんとソースコードを読んでいなくてもっと複雑なことが必要かと思ってたので時間をかけてしまいました。反省。
crypto: RSA Calc
サーバーがカンマ区切りの逆ポーランド記法+αなフォーマットを受け取り、計算式への電子署名を検証をパスするかつ、計算結果が1337のときにフラグが出力される問題です。 逆ポーランド記法の+ αの部分はシンボルFです。シンボルFが入力されるとその時点でのstackから値を一つ取り出して1337であるかチェックします。チェックが通ったらFlagが出力されるというものでした。
また、サーバー側のプログラムは計算式への署名機能と計算式の実行機能を持っていました。
署名機能:
入力: data: カンマ区切りの逆ポーランド記法+αの形式で計算式を渡す
出力: signature(署名)が出力される
制限: 署名のdataにF、または1337が含まれていると署名をしてくれない
計算式の実行機能:
入力: data(計算式), signature
出力: 計算結果, Fが入力され時点での計算結果が1337のときにflagを出力
制限: signatureの検証に失敗するとエラー
解法の方針としてはFlagを取れるような計算式への署名をサーバーの署名機能で打つことはできないので、署名を偽造する方針を取りました。
サーバーに実装されていた署名の式は sig = md mod Nで、mが合成数でm1, m2に因数分解可能なとき、
sig1 = m1d mod N
sig2 = m2d mod N
とすると、
sig == sig1*sig2 mod N
を満たします。
また、サーバー側は受け取った数式をbytes_to_long()でlong型へ変換してから署名を打っていました。
よって、目標の式を1337,Fとしたときにbytes_to_long(b'1337,F')==m1 * m2を満たすm1, m2をサーバーに与えて署名を打ち、
あとは前述の通り、得たsig1,sig2から1337,Fに対応する電子署名をローカルで作成してサーバーの計算機能に式1337,Fと一緒に署名を入力するとFlagを得ることができました。
余談1: 当初はサーバーへのバイナリの送信はecho -e '\x01' | pbcopy をしてncコマンドの入力に貼り付けて送信していたのですが、うまく送信できなかったので、最終的にはGoでsocketクライアントを書いてサーバーとやり取りをしていました。
余談2: 最初はdが小さいと仮定してDLP解けばいいよね...とBSGSアルゴリズムをまわしましたが、dは十分に大きいようで時間がかかりうまく行かなかったため、上記の方針に切り替えました。 あと、色々な検証を@souring001君と進められたので一人でやるよりとても気が楽でした。感謝...!
チームメイトに解いてもらった or 手をつけたけど解けなかった問題
crypto: Noisy equations
詰まっていたら、@souring001君が解いてくれました。感謝...! 連立方程式を解く方針で解けるそうです。
追記2020/05/24 10:30: 参照: qiita.com
crypto: Encrypter
暗号文のサイズの変化具合からブロック暗号っぽいとは思って色々こねくり回し、ラスト30分くらいでCBCモードぽいことに気づきました。が、時間切れでした... 大会終了後にTwitterで他の人のつぶやきを調べたところ、CBC Padding Oracleで解けるようです。 確かに不正な暗号文を投げるとエラーが出ていましたが、Paddingチェックエラー由来だということに気づけませんでした...(終
感想
反省点としては、解くに前半は一つの問題に集中して取り組めなかったことです... 解けなそうと思ってすぐに手放して他の問題をやってしまうことがありました。 適宜休憩とったほうが良いなという反省もあります。 とりあえず、チームメンバーには感謝です🙇 大きく実力不足を感じたので、今後も精進していくぞ..!
21新卒セキュリティエンジニアの就活をした
- 誰?
- 情報系大学院生(M1、21卒)
- (情報工学成分が多めの)暗号理論の研究をしている
21新卒でセキュリティエンジニアの就活をしました。
就活の詳細は割愛しますが、報告も兼ねてオフライン/オンラインで聞かれた質問一覧を載せます。
Q.どこにいくの?
A.ここには書けないです。セキュリティ専業の会社ではないです。
Q.何やるの?
A.開発寄りのセキュリティ関係のお仕事をする予定です。
診断を中心にやるところからも内定はいただきましたが考えた結果、開発寄りの方を選びました。
Q.決め手は?
A.最終的な決め手は自分のやりたいことができそうなポジションを提案してもらえたことです。
Q.就活の期間は?
A.1社目のエントリーから本選考が全て終わるまで3ヶ月弱でした。
実際には学会発表などが間に入っていたので次の面接まで3週間弱待って頂いたりしたので実質的な就活期間はもう少し短いです。
Q.選考終了から決めるまでの間は何をしてたの?
A.決めるための情報を収集して整理していました。
具体的には各社の人事・エンジニアと面談をしたり、知り合いに相談したりしていました。
本選考を受けた企業は調べた上で入りたい企業のみだったので、すぐに決めるのは難しかったです。
客観的な数字を求めてIR情報も見たりしましたが、最終的には何をしたいかで選びました。
Q.その他
A.飯行きましょう。
2019備忘録
2019備忘録を書いていきます。 各期間内のイベントは順不同です。
- 1〜4月
- 5〜9月
- 論文投稿した
- 長期インターンを始めた
- Google I/O 2019に参加した
- エンジニア向けのイベントで登壇した
- セキュリティ・ネクストキャンプに参加した
- 8,9月はサマーインターンにも行った
- 10月〜12月
- 投稿した内容の発表をしてきた
- シュー活を始めた
- 就活を始めた
実際にはここに書いていない嬉しかったことや挑戦したこと、辛いことも含めると2019年は色々ありました。 ご指導ご鞭撻くださった方々、イベントなどで色々お話してくれたり面倒を見てくださった方々、Twitterの絡みに付き合ってくれた方々ありがとうございました! 2020年も引き続きよろしくお願いします🙇
余白ができたのでシュー活の記録を置いておきます。
10月になったのでシュー活始めます pic.twitter.com/1iDu3XevbZ
— ペロトン (@prprhyt) 2019年10月1日
シュー活 pic.twitter.com/PYcALsNVuI
— ペロトン (@prprhyt) 2019年10月31日
シュー活 pic.twitter.com/yeeYWOiyU7
— ペロトン (@prprhyt) 2019年11月6日
シュー活 pic.twitter.com/XodtxmBE63
— ペロトン (@prprhyt) 2019年11月8日
シュー活 pic.twitter.com/3PioSnmoU1
— ペロトン (@prprhyt) 2019年11月18日
今日のシュー活 pic.twitter.com/06Hwi3PEkC
— ペロトン (@prprhyt) 2019年11月19日
シュー活をですね...(そろそろカロリーがやばい) pic.twitter.com/E0tzz6TkXi
— ペロトン (@prprhyt) 2019年11月26日
シュー活 pic.twitter.com/AJIs9eQpWT
— ペロトン (@prprhyt) 2019年12月17日
シュー活 pic.twitter.com/GVqLV1NcSg
— ペロトン (@prprhyt) 2019年12月18日