暗号はCSAW CTF 2014 Crypto300のfeal.pyを、エクスプロイトはCodeGate 2015 Pwnable400のbeef_steakを中心にハンズオンで講義が行われました。
いずれも惜しくも時間内には解けませんでしたが、また時間に余裕がある時に解法コードを作って載せられたらと思います。(試験のすぐ前なので今は時間が無いです)
暗号 - feal.py
暗号問題は、サーバーで動作しているpythonコード(feal.py)が渡され、そこに接続します。
・第一段階
まず、指定された文字列(ランダムな16文字)から始まり、sha1の最終16ビットが全て1になるような文字列を送信しなくてはなりません。つまり、
h = sha1(str_rnd + something)
について、
h[:-1] == h[-2] == 0xFF
であれば通過できます。
これは単純にsomethingにブルートフォースしたら簡単に通りました。
ここまでは家で解いてきたのですが、その後はでっていう状態でした。
・第二段階
次に暗号化されたフラグが渡されます。これは保存しておきます。今回使用されている暗号は、タイトルの通りFEAL暗号ですが、よく見ると、
return ((x << 2) | (x >> 6)) & 0xff
であるはずのシフト関数が、
return ((x << 3) | (x >> 5)) & 0xff
になっていました。本来の仕様との違いはこれだけです。
今回使用する攻撃はDifferential Cryptanalysis(差分解読法)とかいう聞いたこともない方法で、要するに
「入力A, BについてFEAL(A), FEAL(B)が求まっていて、その出力差分FEAL(A) ^ FEAL(B)が一定ならX ^ Y == A ^ B(入力差分がA,Bと等しい)ような入力に対して、その出力の差分はFEAL(A) ^ FEAL(B)となる」
みたいな感じでした。(間違ってたらすいません)
なんとなーく分かったのですが、実際にコードを書くと何かしっくりこなかったです。それに、後から知ったのですが、pythonで書くと十数時間かかるようで、C言語で書く必要があったそうです。ずっとpythonで買いてパソコンのファンを鳴らしていました。
結局私のコードで答えは出ずに、解答だけ聞いて終了してしまいました。暇なときにコードを書きます。
エクスプロイト - beef_steak
エクスプロイトと聞いてうわぁって感じだったのですが、内容はとても面白かったです。とにかくスライドが詳しく書かれていました。(暗号のスライドも図が多くて分かりやすかったですが、こちらは300ページ程度ありました)
beef_steakでは、steakというLinux x86_64のバイナリが渡され、それを実行しているサーバーを攻略するという問題です。家で解析しておきましょうという指令が出ていたので、C言語のコードに直したのですが、その時はStack BOFがあるだけで、フラグが読めそうとは思いませんでした。というのも、Stack Canaryが付いており、オーバーフローしてもすぐに終了してしまうのです。
・第一段階
家で解析したときのCコードを以下に示します。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <stdio.h> | |
#include <stdlib.h> | |
#include <string.h> | |
#include <unistd.h> | |
char cmpdata[0x28] = "\x62\x31\xaa\x85\xbd\xbf\x9f\xf3\x8a\x02\x0c\x75\xac\x23\xab\xe4\x82\xc5\x25\x7a\xef\xbd\xc9\x61\x00\x54\x68\x61"; | |
/* 0x400da6 */ | |
char key[0x28]; /* 0x6020e0 */ | |
char output[0x28]; /* 0x602120 */ | |
unsigned char state[0x100]; /* 0x602160 */ | |
/* | |
Initialize RC4 Crypt | |
*/ | |
void rc4_init() | |
{ | |
int len; /* rbp-0x4 */ | |
int i; /* rbp-0x8 */ | |
unsigned char tmp; /* rbp-0x9 */ | |
unsigned char j; /* rbp-0xa */ | |
for(i = 0; i <= 0xff; i++) { | |
state[i] = (unsigned char)i; | |
} | |
len = strlen(key); | |
for(j = i = 0; i <= 0xff; i++) { | |
j += state[i]; | |
j += key[i % len]; | |
/* swap */ | |
tmp = state[i]; | |
state[i] = state[j]; | |
state[j] = tmp; | |
} | |
} | |
/* | |
Encrypt data with RC4 | |
*/ | |
void rc4_encrypt(char in[], char out[], int in_len) | |
{ | |
int buflen = strlen(in); /* rbp-0x4 */ | |
int i; /* rbp-0x8 */ | |
unsigned char tmp; | |
unsigned char index1, index2; /* rbp-0xa, rbp-0xb*/ | |
unsigned char j; | |
for(i = 0; i < buflen; i++) { | |
index1++; | |
index2 += state[index1]; | |
tmp = state[index1]; | |
state[index1] = state[index2]; | |
state[index2] = tmp; | |
j = state[index1] + state[index2]; | |
out[i] = in[i] ^ state[j]; | |
} | |
} | |
/* | |
MAIN ROUTINE | |
*/ | |
int main() | |
{ | |
FILE *fp; /* rbp-0x38 */ | |
char input[0x40]; /* rbp-0x30 */ | |
int counter; /* rbp-0x3c */ | |
chdir("/home/steak"); | |
/* | |
Read 0x28(40) bytes from /home/steak/flag | |
*/ | |
fp = fopen("/home/steak/flag", "r"); | |
fgets(key, 0x28, fp); | |
fclose(fp); | |
/* | |
Initialize RC4 and remove 'key' | |
*/ | |
rc4_init(); | |
memset(key, 0, 0x28); | |
/* | |
Get input | |
*/ | |
puts("What's your favirite food?"); | |
fflush(stdout); | |
fgets(input, 0x200, stdin); | |
/* | |
Encrypt data | |
*/ | |
rc4_encrypt(input, output, strlen(input)-1); | |
printf("Hmm..."); | |
fflush(stdout); | |
/* | |
Time spends... | |
*/ | |
for(counter = 0; counter <= 4; counter++) { | |
sleep(1); | |
putchar(0x2e); | |
fflush(stdout); | |
} | |
/* | |
check | |
*/ | |
if ( strcmp(output, cmpdata) == 0 ) { | |
puts("That's my favorite!"); | |
puts("You may leave a message"); | |
fflush(stdout); | |
system("/bin/cat > ./message"); | |
} else { | |
puts("I don't like that!"); | |
} | |
memset(output, 0, 0x28); | |
} |
ここで新たに学んだのは、"argv[0] leak"という手法です。(正式名称でない?)これは、Stack CanaryのStack Smashingメッセージが表示されることを利用し、内部のデータを取得するという方法です。
具体的には、オーバーフローを起こした際の
*** stack smashing detected ***: ./steak terminated
というおなじみのメッセージの
./steak
を取得したい変数に変更します。そのためASLRが有効な場合は固定アドレスに存在するデータが取得する対象となります。今回のCコードを読めば分かるように、key、state、output等のデータはアドレスが固定されています。
どうやって"./steak"の部分を変えるかですが、__stack_chk_fail関数では、普通にargvの内容からargv[0]のファイル名を表示しているそうです。(詳しくはkatagaitai CTFの資料を参考に。)つまり、十分にオーバーフローできる場合、argv[0]のポインタがある部分を取得したいデータのアドレスに書き換えてやります。
これを聞いてさっそく
"\xe0\x20\x60\x00\x00\x00\x00\x00" * 40
みたいなデータ(keyのアドレスを繰り返し)を送ろうと考えたのですが、よく考えたらkeyは使用した直後にmemsetで0にクリアされています。ということでstateを取得したら見事取得できました。と思ったら256バイトあるはずのデータを途中までしか取れていません。これはstateの途中に0x00が存在するのが原因で、
接続→stateを取得(lengthバイト)→切断→接続→state+lengthを取得(lengthバイト)...
みたいに何度か接続するようにして全てのデータを取得しました。
・第二段階
もともと"What's your favorite food?"という質問に対して答えが正しければ通ります。取得したstateを元に、バイナリから取得した暗号化済みデータを復号化し、それを送りました。ここで新たなバグを使用します。steakは入力の文字数をstrlenで取得し、その分暗号化しているのですが、入力の最初に0x00を持ってくることで、暗号化処理をスキップできます。そうしてチェックを通過すると、自由にメッセージを"./message"に書き込むことができるようになります。
ここで使用するのがShared Library Injectionという手法で、これはLD_PRELOADで自前のライブラリを使用することで攻撃できます。したがって、今回のように自由にファイルをアップロードできる状態か、sshのような形式でないと利用できません。今回は、systemをexecve("/bin/sh", 0, NULL);に変更するようなライブラリを作りました。(messageへの書き込みにsystem関数が呼び出される。)messageに保存できたら、これを利用したいのですが、ライブラリmessageを読み込ませる手段が必要です。
・第三段階
messageを読み込ませるには、環境変数LD_PRELOADを./messageにする必要があります。つまり、オーバーフローを使用してenvp[0]のアドレスを"LD_PRELOAD=./message"を指すアドレスにすればよいのです。こんなコードを作って実行したのですが、ローカルでは「ライブラリをロードできません」みたいなメッセージが出て、リモートではそもそも何も起こりませんでした。ちょっとよく分からないので暇な時にやります。
この他にも様々なexploit tipsを教えていただきました。
まとめ
katagaitai CTFの勉強会に参加させていただくのは今回が初めてでしたが、内容は難しかったものの、その技術は(なんとなく)分かったので、非常に有意義な勉強会だったと言えます。どちらも全く知らなかった攻撃揃いだったので、自分でコードを作りながらも新鮮な感じがしました。難しかったとはいえ、このくらいのレベルはやってて楽しいので、次回も同じレベルの勉強会があれば是非参加したいです。