picoCTFで練習
よくCTFの大会の問題を見てみると、簡単と書いてあるのにもかかわらず、意外と難しかったりします。
そこで、picoCTFがうってつけというわけです。(個人の感想です)
picoCTFとは (簡単に)
- 簡単な問題から難しい問題までがあり、練習ができる
- サイト https://picoctf.org/
以下よりpicoCTFのpractice問題を解きます。
practice問題を解いてみる(ほぼGeneral Skills)
簡単そうな問題から解いていきます。とはいっても、解けそうなものを選んでいたら、コマンドだけの簡単な問題がほとんどになってしまい、コマンド作業になりました。
間違ってるところがあったらご容赦ください。
・First Find [General Skills]
これは、配布されたファイルを解凍して、コマンドで探すだけなので簡単です。Linuxでは、
find [配布されたファイルのパス] -type f -name "*[探したいファイル名]*"
で探せて、txtファイルが出てくるので、開いたらフラグゲット。
・DISKO 1 [Forensics]
まず問題文を見てみると、ディスクイメージにフラグがありそうな感じがします。
また、ディスクイメージが配布されているのでダウンロードします。
配布されたものは[disko-1.dd.gz]だったので、なんとなく解凍してみると、.ddファイルが出てきました。
フラグはこの中にありそうなので、フラグを次のコマンドで探します。(Linux向け)
strings [.ddファイルのパス] | grep [探す文字列]
ここで、picoCTFのフラグはpicoCTF{~~~}の形式であることを利用し、文字列[picoCTF]を探してあげます。
strings [.ddファイルのパス] | grep picoCTF
すると、見事にフラグが出てきます。
・Tab, Tab, Attack [General Skills]
配布されたファイルを解凍し、フォルダを開いていくと拡張子がないファイルが出てきます。これをVScodeの拡張機能である[Hex Editor]で見てみると、意味不明な文字列が出てきます。これを[ctrl + F]して "pico" で検索すると、フラグが出てきます。
・ASCII Numbers [General Skills]
与えられたASCII数字の文字列をヒント通りにCyberChefを使えば一瞬でフラグが出てきました。Mediumなのを疑うレベルです。
・Binary Search [General Skills]
問題で出されているsshに繋いだら、1~1000のなかから数字を選ばされ、10回の中で見つけなければいけないので、二分探索をします。基本的に半分ずつ数字を選んでいき、Higherと呼ばれたら数字を増やし、Lowerと言われたら数字を減らすだけです。
Welcome to the Binary Search Game!
I'm thinking of a number between 1 and 1000.
Enter your guess: 500
Higher! Try again.
Enter your guess: 725
Lower! Try again.
Enter your guess: 612
Higher! Try again.
Enter your guess: 668
Lower! Try again.
Enter your guess: 640
Higher! Try again.
Enter your guess: 654
Lower! Try again.
Enter your guess: 647
Lower! Try again.
Enter your guess: 643
Lower! Try again.
Enter your guess: 642
Lower! Try again.
Enter your guess: 641
Congratulations! You guessed the correct number: 641
Here's your flag: picoCTF{g00d_gu355_ee8225d0}
Connection to atlas.picoctf.net closed.
ぴったり10回でフラグを獲得しました。
・bloat.py [Reverse Engineering]
フラグが入っているプログラムと、それを読み込んで実行するPythonプログラムが与えらているので見てみると、
arg444 = arg132()
で読み込んだフラグをarg444に渡しているのが分かると思います。
次に、
arg432 = arg232()
があります。関数arg232()を見てみると、なにやらinput()で入力が入れられています。その結果をarg432に渡してます。
このarg432を引数として、arg133()に渡されていますが、関数arg133()を見てみると引数のarg432と何か文字列を比べてます。
if arg432 == a[71]+a[64]+ ~~~~ +a[66]+a[68]:
出力の文字列を見るに、比べている文字列がパスワードっぽいので、入力が発生する前に、この文字列を出力してあげます。
print(a[71]+a[64]+ ~~~~ +a[66]+a[68]) #適当に出力
すると、
happychance
が出てくるので、パスワードにこれを入力すると、見事フラグゲットです。
・endianness [General Skills]
リトルエンディアンとビッグエンディアンについての問題ということで、ヒントに載っているサイトの説明を見てみると、簡単に言うと二桁の16進数の並び方が逆かそうじゃないかみたいな感じらしいです。
次に配布されたコードを見てみます。
最初に関数によりランダムな文字列が生成されています。
char *challenge_word = generate_random_word();
その後、関数に引数として先ほどの文字列が渡され、リトルエンディアンが生成されてます。
char *little_endian = find_little_endian(challenge_word);
そして、これらを次により比較し、
if (strncmp(user_little_endian, little_endian, user_little_endian_size) == 0)
うまく合えば次の入力に進むことができ、次はビッグエンディアンですが、リトルエンディアンと同様です。そして、これも合えばフラグ獲得となりそうですが、今回の問題ではリトルエンディアンとビッグエンディアンの文字列に合う文字列を見つけなければいけません。
ここで、ランダム生成されるのはchallenge_wordのみなので、リトルエンディアンとビッグエンディアンの文字列はchallenge_wordに依存するため、challenge_wordが分かればおのずとリトルエンディアンとビッグエンディアンの文字列が分かります。ありがたいことに、challenge_wordはprintfで出力されているので、ローカルにかかわらず、netcatで接続した場合でも同様にわかります。
任意の文字列に対してリトルエンディアンとビッグエンディアンの文字列を出力する関数を、もとのコードを少し変えて適当に作ります。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void *find_little_endian(const char *word)
{
size_t word_len = strlen(word);
char *little_endian = (char *)malloc((2 * word_len + 1) * sizeof(char));
for (size_t i = word_len; i-- > 0;)
{
snprintf(&little_endian[(word_len - 1 - i) * 2], 3, "%02X", (unsigned char)word[i]);
}
little_endian[2 * word_len] = '\0';
printf("Little[%s]\n", little_endian);
}
void *find_big_endian(const char *word)
{
size_t length = strlen(word);
char *big_endian = (char *)malloc((2 * length + 1) * sizeof(char));
for (size_t i = 0; i < length; i++)
{
snprintf(&big_endian[i * 2], 3, "%02X", (unsigned char)word[i]);
}
big_endian[2 * length] = '\0';
printf("Big[%s]\n", big_endian);
}
int main()
{
char word[100];
printf("Word:");
scanf("%s", word);
find_little_endian(word);
find_big_endian(word);
return 0;
}
これを使って、最初に出力されるWordから、対応するリトルエンディアンとビッグエンディアンの文字列を得ます。よって、これらを使用することでフラグを獲得できます!
・heap 0 [Binary Exploitation]
Cで書かれたコードが出されたので、見てみるとヒープのデータをいじって何かするような感じです。いじれるのはinput_dataのみらしいです。
見ていくと、関数chec_winでsafe_varとbicoを比較して、異なればフラグが出てきそうですが、safe_varは最初"bico"で初期化されていて、かつ直接いじることはできなさそうです。
経験則になってしまうのですが、問題文にオーバーフローをにおわせることが書いてあったので、ここでなんとなくinput_dataを変える際の入力を増やしていき、オーバーフローさせてみます。すると、一定の長さを入力した際に、safe_varの値にも影響が出て、safe_varの値を上書きできたので、この状態でフラグを確認してみると、フラグをゲット。
後からいろいろ確認して分かりましたが、本当は両方のヒープのアドレスを考えると入力の際の文字列を決定でき、今回、input_dataで想定されている文字列は5バイト、二つのアドレスの差は32バイトなので、32バイト以上を入力すればオーバーフローが発生します。
・Flag Hunters [Reverse Engineering]
この問題は、解いていて頭が絡まりそうだったので、変にまとめずに書きます。
※振り返ったら結構長くなってしまいました
今までのようにコマンドですぐ出てくるというのではなく、これは少し考えなければいけませんでした。
プログラムのソースコードが配布されているので見てみると、Pythonで書かれたコードであることが分かります。
正直に言うと何をすべきかが分からないのでヒントを見るとサニタイズされていないユーザー入力(翻訳済み)という文章があり、調べると、サニタイズされていないというのはどうやらユーザーからの入力に処理を加えていないということらしい。ということはPython内のinput()付近が怪しいかも?
コードを実行しようとしたらエラーで、どうやら配布されたローカル環境ではflag.txtがないため開けないとのこと。そこで、一時的にダミーのフラグを作って無理やり動かします。試しに次のようにしました。
#flag = open('flag.txt', 'r').read() #これがエラーの原因なのでコメントアウトする
flag = "picoCTF{dummy}" #ダミーのフラグにしておく
とりあえずこれで動くようになったので、試しに動かしてみると文章が表示された後に入力を求められました。適当に文字列を打ってもその後に長い文章が出てくるばかりでflagに通じそうなものはありません。
適当な文字列ではだめだったので、とりあえずコードを読み解いていきます。
主な構成は以下でした。
- song_flag_huntersに文章を入れる
- 入力/出力をまとめた関数(一番厄介そう)
- 関数を実行
song_flag_huntersに入れられているのはsecret_introとflagなのが分かると思います。
関数はreaderとなっていて、songとstartLabelが引数となっているのが分かります。
そのreaderの引数に先ほどのsong_flag_huntersと文字列 [VERSE1] が渡されているのが分かります。
関数内での入力を受け取るinput()付近に注目して解ければよいのですが、見た感じ入り組んでそうなので正直に上から順に見ていきます。
初め、関数内のsongとstartLabelにはsong_flag_huntersと [VERSE1] が渡されています。その後、各変数の初期化の後に
song_lines = song.splitlines()
とあります。これは渡されたsong_flag_huntersを、1行を1要素としてリスト化しているだけです。
次のfor文では、song_lines(song_flag_hunters) の中にある [VERSE1] の場所を見つけ、その1つ次のインデックスをstartに入れています。その次の二つのelifも似たような感じなので見ればわかると思います。
次のwhile文を見てみます。ループ条件はfinishedがtrueでない、かつline_countが定義された100(MAX_LINES)を越えない間なので、なんとなく、100行くらいみて終わるのかなと考えて適当に飛ばします。
中にfor文があり、
for line in song_lines[lip].split(';'):
となっていて、一見ややこしい(?)ですが、これはsong_linesの各要素を';'で分割しただけです。song_linesはsong_flag_huntersだったので、song_flag_huntersをみてみると、';'が付いているのは特定の文末、REFRAIN;、END; などであるので、これによりREFRAIN、ENDなどの単語を他と分けていることが分かります。
その後のif,elifでは 'REFRAIN', "CROWD.*", "RETURN [0-9]+" をlineと比較しているだけになります。
ここで、"CROWD.*", "RETURN [0-9]+" が意味不明だったので調べたところ、簡単に言うと"CROWD.*"はCROWD.が何個連なってても良くて、RETURN [0-9]+がRETURN [数字の羅列]であり、例えば
RETURN 1
RETURN 93
RETURN 23
RETURN 59
などです。
全体の流れが分かったところで、input()を見てみます。
crowd = input('Crowd: ')
input()で取得した文字列をcrowdにいれてますが、なんと次に
song_lines[lip] = 'Crowd: ' + crowd
とし、そのままsong_lines[lip] にくっつけてしまっています。
さらに、次のelifの中を見てみると、
re.match(r"RETURN [0-9]+", line):
で比較後にそのlineを
lip = int(line.split()[1])
という風に分割し、「[1]」とあるので分割したline二つ目の要素intにキャストしlipに入れています。
ここで、lipは何だったか思い返すと、lipは最初にstartの値を入れられており、その後song_linesのインデックスとして使われていました。lipは増加していくので、start前のインデックスの要素は表示されないことになります。しかし、start(lib)は'[VERSE1]'の次のインデックスを示すので、それ以前のflagが入った文字列を参照してくれません。
話を戻してまとめると、
- input()での入力をそのままsong_lines[lip] にくっつけられる
- song_lines[lip]は分割されてlineとなり、"RETURN [0-9]+"に適していればRETURNの次の数字がlipに入れられる
となる。
ということは、song_lines[lip]に任意のRETURN [数字]を入れてあげればlipを任意の値にでき、それをインデックスとするsong_lines[lip]の参照する場所を操作できるので、flagの場所を参照できそう!
ということで、input()に何を入れればよいかですが、"RETURN [任意の数字]"をsong_lines[lip]にくっつけてあげることでlipに[任意の数字]が入るので、とりあえず"RETURN 0"とします。しかし、このまま入力してしまうと、前の文章に埋もれてしまうので、";"を先頭に付け、"RETURN 0"の"RETURN"をlineの最初に持ってきてあげます。最終的には";RETURN 0"としました。
これを入力すると、
となり、自分で設定したダミーのフラグが出てきます。
よって、netcatでの接続でも";RETURN 0"を打ってみると、フラグ獲得です!
