Tasteless CTF 2019 Writeup
Tasteless CTF 2019 について
Tasteless CTF 2019 が開催されました。
2019年10月26日午後9時~2019年10月27日午後9時(24時間)
このCTFも難易度の高い問題が多かったです。色々問題を見ていましたが、チュートリアル問題も含めて結局2問しか解くことができませんでした。 一応今回もチームで参加しました。結果は、47/157位で155点でした。 今回、私が実際に解いた2問のWriteupを紹介します。
同日にBackdoorCTF 2019も開催されていました。 こちらも同時に参加していました。Writeupを書きましたので、参考にしてみてください。
Tasteless CTF 2019 Writeup(2問)
sanity(misc)
問題
this challenge is protected.
hitme.tasteless.eu:10001
protected challenges will require a proof-of-work like this:
sha1(abc123, input) prefix = 00000...
you need to respond with a single line suffix to abc123, so that sha1(abc123[input]) has a 00000 prefix example: sha(abc12344739190).hexdigest = 000000872D5625DEE5FD0EA44B230D7A98C1B2CA
you can usego run pow.go abc123 00000
orpython pow.py abc123 00000
to generate your own. the pow binary is go, compiled for linux/amd64.
- pow
- pow.go
- pow.py
解答例
Tasteless CTF のチュートリアル問題です。 このCTFは、少し変わっていて、netcatで接続する問題にプロテクトがかかっている場合があります。 他のCTFと同じようにnetcatで接続するだけでは、問題にたどり着くことができません。
netcatで接続してみると、以下のようになります。
$ nc hitme.tasteless.eu 10001 sha1(cf27ff1e75faba1c, input) prefix = 00000...
Tasteless CTFは、接続用のツールを提供しており、これを使って問題を解いていく必要があるようです。 ヘルプを見ると以下のように表示されます。
$ ./pow usage solve args: Usage: ./pow <prefix> <hash> Usage: ./pow d616656ece36eb66 00000 solve serverresponse: Usage: ./pow '<serverresponse>' Usage: ./pow 'sha1(d616656ece36eb66, input) prefix = 00000...' netcat mode: Usage: ./pow connect <host> <port> Usage: ./pow connect hitme.tasteless.eu 10001 server mode: Usage: ./pow listen <addr> <host> <port> Usage: ./pow listen :12345 hitme.tasteless.eu 10001
実行すると、フラグが表示されました。
$ ./pow connect hitme.tasteless.eu 10001 welcome! tctf{are_y0u_readdddyyyy}
FLAG
tctf{are_y0u_readdddyyyy}
babypad(crypto)
問題
We heard this kind of enription is super securr, so we'll just give you the flag encripted!
nc hitme.tasteless.eu 10401
$ sha1sum chall.c
d64fc2e2f979b693696efe1762e18153df1b6170 chall.c
Author: plonk
chall.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <assert.h> int main() { char *plaintext = NULL; char *one_time_pad = NULL; char *ciphertext = NULL; size_t flag_length = 0; FILE *flag = fopen("flag.txt", "r"); FILE *urandom = fopen("/dev/urandom", "r"); assert(flag && urandom); /* * Get flag length, and allocate memory for the plaintext, * one-time pad and ciphertext. */ fseek(flag, 0, SEEK_END); flag_length = ftell(flag); rewind(flag); plaintext = malloc(flag_length + 1); one_time_pad = malloc(flag_length + 1); ciphertext = malloc(flag_length + 1); assert(plaintext && one_time_pad && ciphertext); /* Read the plaintext and the one-time-pad */ fread(plaintext, flag_length, 1, flag); fread(one_time_pad, flag_length, 1, urandom); plaintext[flag_length] = '\0'; one_time_pad[flag_length] = '\0'; /* Make sure that the one-time-pad isn't too short. */ assert(strlen(plaintext) == strlen(one_time_pad)); for (int i = 0; i < flag_length; i++) { ciphertext[i] = plaintext[i] ^ one_time_pad[i]; } fwrite(ciphertext, flag_length, 1, stdout); return 0; }
解答例
この問題は、ソースコードが提供されました。 読んでみると、以下のような処理を行っていました。
flag.txt
のデータサイズを求める。flag.txt
のデータサイズ+1の長さのバッファ(plaintext
、one_time_pad
、ciphertext
)を3つ確保する。flag.txt
からデータを読み出し、plaintext
に格納する。/dev/urandom
からデータを読み出し、one_time_pad
に格納する。plaintext
とone_time_pad
を1文字ずつ取り出して、それぞれをXOR演算し、ciphertext
に格納する(flag.txt
のデータサイズ分繰り返す)。ciphertext
を標準出力に出力する。
ただし、よく読んでみると、以下のような処理が確認できます。
C言語のstrlenは、Null文字までの文字数を返す関数です。
そのため、/dev/urandom
から読み出したデータに\0
が含まれる場合、ここでエラーとなり終了してしまいます。
/* Make sure that the one-time-pad isn't too short. */
assert(strlen(plaintext) == strlen(one_time_pad));
実際にコンパイルして、何度か実行していると想定箇所で終了しました。
$ gcc chall.c $ ./a.out a.out: chall.c:35: main: Assertion `strlen(plaintext) == strlen(one_time_pad)' failed. Aborted (core dumped)
よって、XORキーには、0x00
が含まれないということが分かりました。
そのため、何度もXORで暗号化された文字列を取得し、0x01~0xFFで復号していけば絞り込むことができそうです。
以下のようなスクリプトを作成し、復号パターンを絞り込んでいきました。
from pwn import * import string def check_dec_pattern(pattern, enc_flag, offset): new_pattern = "" for key in range(1, 0x100): ch = chr(enc_flag[offset] ^ key) if ch in pattern: new_pattern += ch return new_pattern def get_enc_flag(): while True: context.log_level = "error" con = remote("hitme.tasteless.eu", 10401) enc_flag = con.recvall() if len(enc_flag) > 0: return enc_flag def main(): max_len = len(get_enc_flag()) pattern_list = [] for _ in range(max_len): pattern = string.ascii_letters + string.digits + string.punctuation pattern_list.append(pattern) loop_end = False while loop_end == False: enc_flag = get_enc_flag() loop_end = True for i in range(max_len): new_pattern = check_dec_pattern(pattern_list[i], enc_flag, i) pattern_list[i] = new_pattern if len(new_pattern) > 1: print(len(new_pattern), end=", ") loop_end = False else: print(new_pattern, end=", ") print() print("".join(pattern_list)) if __name__ == "__main__": main()
結構時間がかかりましたが、完全に復号することができました。
$ python solve.py 93, 94, 93, 94, 94, 93, 94, 94, 94, 94, 94, 94, 93, 94, 93, 94, 94, 93, 94, 93, 94, 93, 94, 93, 93, 94, 94, 94, 94, 93, 94, 94, 93, 94, 94, 94, 94, 92, 93, 93, 93, 94, 93, 93, 94, 93, 94, 93, 93, 93, 94, 92, 93, 94, 92, 94, 93, 94, 93, 93, 93, 92, 94, 94, 94, 93, 92, 94, 94, 92, 93, 93, 94, 94, 91, 93, 93, 93, 94, 93, 93, 94, 92, 94, 93, 93, 92, 93, 92, 93, 94, 91, 94, 92, 94, 92, 93, 92, 91, 94, 93, 93, 93, 92, 94, 93, 92, 93, 93, 94, 93, 90, 92, 92, 93, 94, 92, 92, 93, 92, 94, 92, 92, 92, 92, 92, 93, 94, 91, 93, 91, 94, 92, 93, 91, 91, 94, 92, 93, 93, 92, 94, 92, 91, 92, 92, 94, 93, # 省略 t, c, t, f, {, p, 1, z, _, u, s, 2, :, 4, l, l, -, t, 3, h, _, b, y, 7, e, 5, >, 0, n, 3, _, t, i, m, 3, }, , t, c, t, f, {, p, 1, z, _, u, s, 2, :, 4, l, l, -, t, 3, h, _, b, y, 7, e, 5, >, 0, n, 3, _, t, i, m, 3, }, , t, c, t, f, {, p, 1, z, _, u, s, 3, :, 4, l, l, -, t, 3, h, _, b, y, 7, e, 5, >, 0, n, 3, _, t, i, m, 3, }, , tctf{p1z_us3:4ll-t3h_by7e5>0n3_tim3}
FLAG
tctf{p1z_us3:4ll-t3h_by7e5>0n3_tim3}