LOADING

載入過慢請啟用快取 瀏覽器預設啟用

AIS3 Pre-exam 2025 Writeup

TL;DR

我好爛,我打的題目 AI 都解得出來,我好廢,不要看我這個 WP,題目都是大家會解的 100 分題,個人筆記用。

心得

  • 我好爛。
  • Crypto 都 LLM 解的
  • 我不會密碼學
  • 要我幹嘛 .W.
  • 我活著有意義嗎

Writeup

Web

Tomorin db 🐧

gif

連進去題目

tomorindb_01

直接點 flag

tomorindb_02

沒有很意外,又在 GO

使用字元編碼嘗試攻擊:

http://chals1.ais3.org:30000/%66lag%2f.

tomorindb_03

Flag: AIS3{G01ang_H2v3_a_c0O1_way!!!_Us3ing_C0NN3ct_M3Th07_L0l@T0m0r1n_1s_cute_D0_yo7_L0ve_t0MoRIN?}

Login Screen 1

loginscreen1_01

先使用 guest/guest 登入

loginscreen1_02

登進去,預設 2FA 000000

loginscreen1_03

顯然我不是 admin,看不到 flag 太可惜了

那我們登出前,先來截個封包,如果他 2FA 沒做好可能可以繞過

GET /dashboard.php HTTP/1.1
Host: login-screen.ctftime.uk:36368
Cache-Control: max-age=0
Accept-Language: zh-TW,zh;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://login-screen.ctftime.uk:36368/2fa.php
Accept-Encoding: gzip, deflate, br
Cookie: PHPSESSID=72db3d4e7fa67fdb64aeda60a9d3754d
Connection: keep-alive

loginscreen1_04

好,登出。

loginscreen1_01

通靈出 admin/admin 登入

loginscreen1_02

繼續 000000

loginscreen1_05

沒有辦法繞過

用 Repeater 重送剛剛進 Dashboard 的封包,看能不能繞

loginscreen1_06

Flag: AIS3{1.Es55y_SQL_1nJ3ct10n_w1th_2fa_IuABDADGeP0}

🐱

註:原始指令是 echo | env -i cat " + catfile

以下指令會簡寫成 "cat " + catfile

下了 cat *

cat_01

會有它的 Source Code

可以看到有一些黑單字元

然後 cmdi 感覺打不出來

所以轉向 SSTI 的打法,我們需要寫檔

試了一下

cat /etc/passwd 
#可讀,但沒啥用

cat /etc/passwd > meow 
#可以寫 passwd 寫一份到 meow

但沒辦法任意寫檔

尋找一段時間後,找到兩個可以任意寫的方法

但基本上都會有其他垃圾在裡面

cat owo1 2> owo2
cat owo2
#cat: owo1: No such file or directory

cat /proc/self/cmdline ' meow' > meow
cat meow
#/proc/self/cmdline meow

所以寫個 cat /proc/self/cmdline ' {{7*7}}' > meow

cat_02

沒有觸發

cat_03

然後可以知道環境是阿帕契

cat /etc/cron*/* 
#可以找到 Cron,但沒寫入權限,沒用
cat /var/spool/cron/crontabs/*/* 
#沒東西
cat /proc/self/cmdline ' {{7*7}}' > *
#還是沒權限

cmdi 的部分 <() >() 都用不了

/var/spool/cron/crontabs/*/* 沒東西

meow1 /proc/self/cmdline > meow
cat meow 
#會出現 meow1/proc/self/cmdline 沒啥用

(以下賽後打)

資料來源:https://www.arthurtoday.com/2009/11/ubuntu-httpdcon.html

cat /etc/apache2/sites-available/*
#有阿帕契的設定檔
cat /etc/apache2/sites-e*/*
#有已啟用阿帕契的設定檔

可以看到有個 RewriteEngine On RewriteRule ^(.+.cat)$

cat_04

. 在 Regex 的意義是是匹配任何字元,不是單獨字元 .

所以 cat /proc/self/cmdline ' {{7*7}}' > 70cat

然後連到 http://<Container>.cat.chummy.tw:29999/70cat

cat_05

最終目標

不包含 [“;”,”&”,”|”,”`”,”$”,”#”,”=”,”!”,”.”,”\n”,”\r”] 執行 /readflag

PS: 這題最後沒解出,純筆記

Misc

Ramen CTF

這是原圖

ramen_01

發票有點清楚

ramen_02

解出他 QRCode

ramen_03

MF1687991111404137095000001f4000001f40000000034785923VG9sG89nFznfPnKYFRlsoA==:**********:2:2:1:蝦拉

前十碼是發票號碼

以及,從上面圖片,也可以確認日期和隨機碼。

發票號碼:MF16879911
隨機碼:7095
日期:20250413

查詢消費紀錄

ramen_04

結果 (蝦拉麵)

ramen_05

用地圖去找 (樂山溫泉拉麵)

ramen_06

Flag: AIS3{樂山溫泉拉麵:蝦拉麵}

AIS3 Tiny Server - Web / Misc

Local 端跑起來發現路徑可以帶 // 開到根目錄

http://chals1.ais3.org:20595//readable_flag_O2ZFe4uhwOtQIATllyRAKAUaM1O5ukqw

atsw_01

Flag: AIS3{tInY_weB_5erv3R_witH_fIl3_Br0Ws1n9_As_@_Fe@tUr3}

Welcome

welcome_01

複製出來是

A
I
S
3
{
T
h
i
s
_
I
s
_
J
u
s
t
_
A
_
F
a
k
e
_
F
l
a
g
_
~
~
}

防複製,手打或 Google Lens

welcome_02

Flag: AIS3{Welcome_And_Enjoy_The_CTF_!}

晚了十秒

welcome_03

Reverse

web flag checker

裡面有看到一個被編譯過的 WebAssembly 檔案

webflagchk_01

把它用 diswasm 反編譯出來

https://github.com/wasmkit/diswasm

裡面有一段做 flag 檢查的程式

export "flagchecker"; // $func9 is exported to "flagchecker"
int $func2(int param0) {
  // offset=0x58
  int local_58;
  // offset=0x54
  int local_54;
  // offset=0x40
  long local_40;
  // offset=0x38
  long local_38;
  // offset=0x30
  long local_30;
  // offset=0x28
  long local_28;
  // offset=0x20
  long local_20;
  // offset=0x5c
  int local_5c;
  // offset=0x1c
  int local_1c;
  // offset=0x18
  int local_18;
  // offset=0x10
  long local_10;
  // offset=0xc
  int local_c;
  local_58 = param0;
  local_54 = -0x26158d3;
  local_40 = 0x0;
  local_38 = 0x0;
  local_30 = 0x0;
  local_28 = 0x0;
  local_20 = 0x0;
  local_20 = 0x7577352992956835434;
  local_28 = 0x7148661717033493303;
  local_30 = -0x7081446828746089091;
  local_38 = -0x7479441386887439825;
  local_40 = 0x8046961146294847270;
  label$1: {
    label$2: {
      label$3: {
        if ((((local_58 != 0x0) & 0x1) == 0x0)) break label$3;
        if (((($func6(local_58) != 0x28) & 0x1) == 0x0)) break label$2;
      };
      local_5c = 0x0;
      break label$1;
    };
    local_1c = local_58;
    local_18 = 0x0;
    label$4: {
      while (1) {
        if ((((local_18 < 0x5) & 0x1) == 0x0)) break label$4;
        local_10 = *((unsigned long *) (local_1c + (local_18 << 0x3)));
        local_c = ((-0x26158d3 >>> (local_18 * 0x6)) & 0x3f);
        label$6: {
          if (((($func1(local_10, local_c) != *((unsigned long *) (&local_20 + (local_18 << 0x3)))) & 0x1) == 0x0)) break label$6;
          local_5c = 0x0;
          break label$1;
        };
        local_18 = (local_18 + 0x1);
        break label$5;
      break ;
      };
    };
    local_5c = 0x1;
  };
  return local_5c;
}

運作原理:

先確認輸入長度是否是 40

然後進認證迴圈,一共跑五次,每次處理 8 個

然後拿 local_54(-0x26158d3 or 0xFD9EA72D) 和迴圈次數計算位移量

然後把剛剛輸入拆 8 個的依照位移量的值做左旋轉

然後和 local_20 ~ local_40 的內容作比較

反解他

丟給 LLM 產腳本

LLM 使用紀錄

https://claude.ai/share/3838cbc6-3e0c-4285-b236-375f13c8993e

#!/usr/bin/env python3

def right_rotate(val, shift, bits=64):
    """Right rotate a value by shift bits"""
    shift = shift % bits
    return ((val >> shift) | (val << (bits - shift))) & ((1 << bits) - 1)

def solve_flag():
    # Expected values from the decompiled code
    # Converting from the given decimal values
    expected_vals = [
        7577352992956835434 & 0xFFFFFFFFFFFFFFFF,   # local_20
        7148661717033493303 & 0xFFFFFFFFFFFFFFFF,   # local_28
        (-7081446828746089091) & 0xFFFFFFFFFFFFFFFF, # local_30 (negative)
        (-7479441386887439825) & 0xFFFFFFFFFFFFFFFF, # local_38 (negative) 
        8046961146294847270 & 0xFFFFFFFFFFFFFFFF    # local_40 (this is actually negative in 64-bit)
    ]
    
    print("Expected values as hex:")
    for i, val in enumerate(expected_vals):
        print(f"  Block {i}: 0x{val:016x}")
    
    # Recalculate the shift key more carefully
    shift_key = 0xFD9EA72D  # -0x26158d3 as unsigned 32-bit
    
    shifts = []
    for i in range(5):
        shift = (shift_key >> (i * 6)) & 0x3f
        shifts.append(shift)
    
    print("Shifts:", shifts)
    
    flag = ""
    for i in range(5):
        # Reverse the left rotation by doing right rotation
        original = right_rotate(expected_vals[i], shifts[i])
        
        # Convert to 8 bytes (little endian)
        block = ""
        for j in range(8):
            byte_val = (original >> (j * 8)) & 0xff
            block += chr(byte_val)
        
        flag += block
        print(f"Block {i}: {block!r}")
    
    print(f"\nFlag: {flag}")
    print(f"Flag length: {len(flag)}")
    
    # Verify our solution
    print("\nVerification:")
    for i in range(5):
        block = flag[i*8:(i+1)*8]
        block_val = 0
        for j in range(8):
            block_val |= ord(block[j]) << (j * 8)
        
        # Apply left rotation (what the original function does)
        rotated = ((block_val << shifts[i]) | (block_val >> (64 - shifts[i]))) & 0xFFFFFFFFFFFFFFFF
        print(f"Block {i}: Original=0x{block_val:016x}, Rotated=0x{rotated:016x}, Expected=0x{expected_vals[i]:016x}, Match={rotated == expected_vals[i]}")

if __name__ == "__main__":
    solve_flag()

webflagchk_02

Flag: AIS3{W4SM_R3v3rsing_w17h_g0_4pp_39229dd}

webflagchk_03

AIS3 Tiny Server - Reverse

IDA 逆向

int __cdecl sub_2110(int a1, int a2)
{
  char *v2; // esi
  char v3; // al
  _BYTE *v4; // eax
  _BYTE *v5; // eax
  int result; // eax
  int v7; // eax
  const char *v8; // edi
  _BYTE *v9; // esi
  const char *v10; // ebp
  __int16 v11; // ax
  int v12; // ecx
  char *v13; // eax
  int v14; // [esp-10h] [ebp-1048h]
  int v15; // [esp-10h] [ebp-1048h]
  int v16; // [esp-Ch] [ebp-1044h]
  int v17; // [esp-8h] [ebp-1040h]
  __int16 v18; // [esp+Dh] [ebp-102Bh] BYREF
  char v19; // [esp+Fh] [ebp-1029h]
  _DWORD v20[2]; // [esp+10h] [ebp-1028h] BYREF
  char v21; // [esp+18h] [ebp-1020h]
  char v22; // [esp+19h] [ebp-101Fh] BYREF
  char v23[1024]; // [esp+410h] [ebp-C28h] BYREF
  unsigned __int8 v24; // [esp+810h] [ebp-828h] BYREF
  char v25[1023]; // [esp+811h] [ebp-827h] BYREF
  int v26[3]; // [esp+C10h] [ebp-428h] BYREF
  char v27; // [esp+C1Ch] [ebp-41Ch] BYREF

  *(_DWORD *)(a2 + 512) = 0;
  *(_DWORD *)(a2 + 516) = 0;
  v26[0] = a1;
  v26[1] = 0;
  v26[2] = (int)&v27;
  sub_17E0(v26, v20, 1024);
  __isoc99_sscanf(v20, "%s %s", v23, &v24);
  do
  {
    if ( LOBYTE(v20[0]) == 10 || BYTE1(v20[0]) == 10 )
    {
      result = v24;
      v8 = (const char *)&v24;
      v9 = (_BYTE *)a2;
      if ( v24 == 47 )
      {
        v8 = v25;
        v12 = strlen(v25, v14, v16, v17);
        if ( !v12 )
        {
          v19 = 0;
          v8 = ".";
          v18 = 0;
          LOBYTE(result) = 46;
          goto LABEL_24;
        }
        v13 = v25;
        while ( *v13 != 63 )
        {
          if ( v12 <= ++v13 - (char *)&v24 - 1 )
          {
            v9 = (_BYTE *)a2;
            result = (unsigned __int8)v25[0];
            goto LABEL_23;
          }
        }
        *v13 = 0;
        v9 = (_BYTE *)a2;
        result = (unsigned __int8)v25[0];
      }
LABEL_23:
      v19 = 0;
      v18 = 0;
      if ( !(_BYTE)result )
      {
LABEL_29:
        *v9 = 0;
        return result;
      }
LABEL_24:
      v10 = v8;
      while ( 1 )
      {
        ++v9;
        if ( (_BYTE)result == 37 )
        {
          v11 = *(_WORD *)(v10 + 1);
          v10 += 3;
          v18 = v11;
          *(v9 - 1) = strtoul(&v18, 0, 16);
          result = *(unsigned __int8 *)v10;
          if ( !(_BYTE)result )
            goto LABEL_29;
        }
        else
        {
          ++v10;
          *(v9 - 1) = result;
          result = *(unsigned __int8 *)v10;
          if ( !(_BYTE)result )
            goto LABEL_29;
        }
        if ( (_BYTE *)(a2 + 1023) == v9 )
          goto LABEL_29;
      }
    }
    sub_17E0(v26, v20, 1024);
    if ( LOBYTE(v20[0]) == 82 && *(_WORD *)((char *)v20 + 1) == 28257 )
    {
      __isoc99_sscanf(v20, "Range: bytes=%ld-%u", a2 + 512, a2 + 516);
      v7 = *(_DWORD *)(a2 + 516);
      if ( v7 )
        *(_DWORD *)(a2 + 516) = v7 + 1;
    }
  }
  while ( v20[0] != 861096257 || v20[1] != 1634485805 || v21 != 103 );
  v2 = &v22;
  if ( v22 == 58 || v22 == 32 )
  {
    do
    {
      do
        v3 = *++v2;
      while ( v3 == 32 );
    }
    while ( v3 == 58 );
  }
  v4 = (_BYTE *)strchr(v2, 13);
  if ( v4 )
    *v4 = 0;
  v5 = (_BYTE *)strchr(v2, 10);
  if ( v5 )
    *v5 = 0;
  if ( sub_1E20((int)v2) )
    sub_1F90(a1, 200, (int)"Flag Correct!", (int)"Congratulations! You found the correct flag!", 0, v15, v16, v17);
  else
    sub_1F90(a1, 403, (int)"Wrong Flag", (int)"Sorry, that's not the correct flag. Try again!", 0, v15, v16, v17);
  return close(a1);
}

// XOR 部分
BOOL __cdecl sub_1E20(int a1)
{
  unsigned int v1; // ecx
  char v2; // si
  char v3; // al
  int i; // eax
  char v5; // dl
  _BYTE v7[10]; // [esp+7h] [ebp-49h] BYREF
  int v8[11]; // [esp+12h] [ebp-3Eh]
  __int16 v9; // [esp+3Eh] [ebp-12h]

  v1 = 0;
  v2 = 51;
  v9 = 20;
  v3 = 114;
  v8[0] = 1480073267;
  v8[1] = 1197221906;
  v8[2] = 254628393;
  v8[3] = 920154;
  v8[4] = 1343445007;
  v8[5] = 874076697;
  v8[6] = 1127428440;
  v8[7] = 1510228243;
  v8[8] = 743978009;
  v8[9] = 54940467;
  v8[10] = 1246382110;
  qmemcpy(v7, "rikki_l0v3", sizeof(v7));
  while ( 1 )
  {
    *((_BYTE *)v8 + v1++) = v2 ^ v3;
    if ( v1 == 45 )
      break;
    v2 = *((_BYTE *)v8 + v1);
    v3 = v7[v1 % 0xA];
  }
  for ( i = 0; i != 45; ++i )
  {
    v5 = *(_BYTE *)(a1 + i);
    if ( !v5 || v5 != *((_BYTE *)v8 + i) )
      return 0;
  }
  return *(_BYTE *)(a1 + 45) == 0;
}

atsr_01
atsr_02
就是密文和 rikki_l0v3 做 XOR

反解他

丟給 LLM 產腳本

LLM 使用紀錄

https://claude.ai/share/c00ad95a-fefe-4790-ac60-a6341ddbdb71

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

def solve_flag():
    print("=== 重新分析C代码逻辑 ===")
    print("关键发现:")
    print("1. v8数组初始化为加密的数据")
    print("2. while循环对v8进行原地解密: *((_BYTE *)v8 + v1++) = v2 ^ v3;") 
    print("3. 解密后的v8就是要比较的flag")
    print("4. 验证循环检查输入flag是否与解密后的v8匹配")
    print()
    
    # 从代码中提取的加密数据 (v8数组)
    encrypted_data = [
        1480073267,   # v8[0]
        1197221906,   # v8[1]  
        254628393,    # v8[2]
        920154,       # v8[3]
        1343445007,   # v8[4]
        874076697,    # v8[5]
        1127428440,   # v8[6]
        1510228243,   # v8[7]
        743978009,    # v8[8]
        54940467,     # v8[9]
        1246382110    # v8[10]
    ]
    
    # 将32位整数转换为字节数组 (小端序)
    v8_bytes = []
    for val in encrypted_data:
        v8_bytes.extend([
            val & 0xFF,
            (val >> 8) & 0xFF, 
            (val >> 16) & 0xFF,
            (val >> 24) & 0xFF
        ])
    
    # 添加最后的字节 (v9 = 20)
    v8_bytes.append(20)
    
    # 密钥字符串
    key = "rikki_l0v3"
    
    print(f"v8原始数据长度: {len(v8_bytes)}")
    print(f"密钥: {key}")
    print(f"v8前10字节(hex): {[hex(b) for b in v8_bytes[:10]]}")
    
    # 模拟C代码的while循环解密过程
    print("\n=== 执行解密循环 ===")
    
    v1 = 0  # 循环计数器
    v2 = 51  # 初始值
    v3 = 114 # 初始值 (字符'r'的ASCII值)
    
    # 执行while循环: 对v8数组进行原地解密
    while v1 < 45:
        # 解密当前字节
        v8_bytes[v1] = v2 ^ v3
        v1 += 1
        
        if v1 == 45:
            break
            
        # 更新v2和v3
        v2 = v8_bytes[v1]  # 下一个字节作为新的v2
        v3 = ord(key[v1 % len(key)])  # 循环使用密钥
    
    # 现在v8_bytes包含解密后的flag
    print("解密完成!")
    
    # 将字节转换为字符串
    flag = ""
    for i in range(45):
        if v8_bytes[i] == 0:  # 遇到空字符就停止
            break
        if 32 <= v8_bytes[i] <= 126:  # 可打印ASCII字符
            flag += chr(v8_bytes[i])
        else:
            print(f"警告: 位置{i}有不可打印字符: {v8_bytes[i]} (0x{v8_bytes[i]:02x})")
            flag += f"\\x{v8_bytes[i]:02x}"
    
    print(f"解密后的字节序列: {v8_bytes}")
    return flag

if __name__ == "__main__":
    flag = solve_flag()
    print(f"解密得到的Flag: {flag}")
    print(f"Flag长度: {len(flag)}")
            

Flag: AIS3{w0w_a_f1ag_check3r_1n_serv3r_1s_c00l!!!}

Crypto

📋喔內該,我不會密碼學,我很爛,拜託不要點開,我什麼都願意做

SlowECDSA

LLM 使用紀錄

https://g.co/gemini/share/a9600a02d923

Exploit

from pwn import *
import hashlib
from ecdsa import NIST192p
from ecdsa.util import string_to_number

# Remote connection details
HOST = 'chals1.ais3.org'  # Replace with the actual host
PORT = 19000        # Replace with the actual port

# LCG parameters from the source code
a = 1103515245
c = 12345
curve = NIST192p
order = curve.generator.order()

def solve():
    # Establish connection
    r = remote(HOST, PORT)

    # --- Step 1: Obtain two signatures ---
    # Get the first example signature
    r.sendlineafter(b"Enter option: ", b"get_example")
    r.recvline_startswith(b"msg: ") # consume "msg: example_msg"
    example_msg_bytes = b"example_msg"
    h1 = int.from_bytes(hashlib.sha1(example_msg_bytes).digest(), 'big') % order
    r_hex1 = r.recvline_startswith(b"r: ").decode().strip().split(': ')[1]
    s_hex1 = r.recvline_startswith(b"s: ").decode().strip().split(': ')[1]
    r1 = int(r_hex1, 16)
    s1 = int(s_hex1, 16)
    log.info(f"First signature (example_msg): r={hex(r1)}, s={hex(s1)}")

    # Get a second example signature (LCG state will advance)
    r.sendlineafter(b"Enter option: ", b"get_example")
    r.recvline_startswith(b"msg: ")
    h2 = int.from_bytes(hashlib.sha1(example_msg_bytes).digest(), 'big') % order
    r_hex2 = r.recvline_startswith(b"r: ").decode().strip().split(': ')[1]
    s_hex2 = r.recvline_startswith(b"s: ").decode().strip().split(': ')[1]
    r2 = int(r_hex2, 16)
    s2 = int(s_hex2, 16)
    log.info(f"Second signature (example_msg): r={hex(r2)}, s={hex(s2)}")

    # --- Step 2: Recover private key 'd' ---

    # We have:
    # k_1 * s_1 = h_1 + r_1 * d (mod order)  => d = (k_1 * s_1 - h_1) * r_1_inv
    # k_2 * s_2 = h_2 + r_2 * d (mod order)  => d = (k_2 * s_2 - h_2) * r_2_inv
    # where k_2 = (a * k_1 + c) (mod order)

    # Equating the expressions for d:
    # (k_1 * s_1 - h_1) * pow(r_1, -1, order) = (k_2 * s_2 - h_2) * pow(r_2, -1, order)
    # (k_1 * s_1 - h_1) * r_2 = (k_2 * s_2 - h_2) * r_1
    # (k_1 * s_1 - h_1) * r_2 = ((a * k_1 + c) * s_2 - h_2) * r_1
    # k_1 * s_1 * r_2 - h_1 * r_2 = a * k_1 * s_2 * r_1 + c * s_2 * r_1 - h_2 * r_1
    # k_1 * (s_1 * r_2 - a * s_2 * r_1) = h_1 * r_2 + c * s_2 * r_1 - h_2 * r_1

    # Let A = (s_1 * r_2 - a * s_2 * r_1) % order
    # Let B = (h_1 * r_2 + c * s_2 * r_1 - h_2 * r_1) % order

    A = (s1 * r2 - a * s2 * r1) % order
    B = (h1 * r2 + c * s2 * r1 - h2 * r1) % order

    if A == 0:
        log.error("A is zero, cannot solve for k1. This might happen with very low probability.")
        exit(1)

    # k_1 = B * A_inv (mod order)
    k1 = (B * pow(A, -1, order)) % order
    log.info(f"Recovered k1: {hex(k1)}")

    # Recover k2 for verification/debugging
    k2_recovered = (a * k1 + c) % order
    log.info(f"Recovered k2 (from k1): {hex(k2_recovered)}")

    # Recover the private key 'd'
    # d = (k_1 * s_1 - h_1) * pow(r_1, -1, order) (mod order)
    priv_key_d = ((k1 * s1 - h1) * pow(r1, -1, order)) % order
    log.info(f"Recovered private key d: {hex(priv_key_d)}")

    # --- Step 3: Predict the next k and forge signature for "give_me_flag" ---
    # The LCG's current state after two 'get_example' calls is k2_recovered.
    # So the next k will be k3:
    k3 = (a * k2_recovered + c) % order
    log.info(f"Predicted next k (k3): {hex(k3)}")

    target_msg = b"give_me_flag"
    h_target = int.from_bytes(hashlib.sha1(target_msg).digest(), 'big') % order

    # Calculate R = k * G
    R_point = k3 * curve.generator
    r_target = R_point.x() % order

    # Calculate s = (k_inv * (h + r * d)) % order
    s_target = (pow(k3, -1, order) * (h_target + r_target * priv_key_d)) % order

    log.info(f"Forged signature for '{target_msg.decode()}':")
    log.info(f"r: {hex(r_target)}")
    log.info(f"s: {hex(s_target)}")

    # --- Step 4: Send the forged signature ---
    r.sendlineafter(b"Enter option: ", b"verify")
    r.sendlineafter(b"Enter message: ", target_msg)
    r.sendlineafter(b"Enter r (hex): ", hex(r_target))
    r.sendlineafter(b"Enter s (hex): ", hex(s_target))

    # Get the flag
    r.interactive()

if __name__ == "__main__":
    solve()

ecdsa_01

Flag: AIS3{Aff1n3_nounc3s_c@N_bE_broke_ezily...}

Stream

LLM 使用紀錄

https://g.co/gemini/share/6306da98c777

Exploit

#!/usr/bin/env python3
import math

# --- Integer Square Root Helper Functions ---
def isqrt_floor(n):
    """Calculates floor(sqrt(n)) for non-negative n."""
    if n < 0:
        raise ValueError("isqrt_floor for negative numbers is not supported.")
    if n == 0:
        return 0
    # math.isqrt is available in Python 3.8+ and is efficient
    return math.isqrt(n)

def isqrt_ceil(n):
    """Calculates ceil(sqrt(n)) for non-negative n."""
    if n < 0:
        raise ValueError("isqrt_ceil for negative numbers is not supported.")
    if n == 0:
        return 0
    s = math.isqrt(n)
    if s * s == n:
        return s
    else:
        return s + 1

# --- Constants (Update C_FLAG_INT from your output.txt) ---
# This C_FLAG_INT is from the user's previous execution output.
C_FLAG_INT = 0x1a95888d32cd61925d40815f139aeb35d39d8e33f7e477bd020b88d3ca4adee68de5a0dee2922628da3f834c9ada0fa283e693f1deb61e888423fd64d5c3694

KNOWN_PREFIX = b'AIS3{'
KNOWN_SUFFIX = b'}'
R_MAX_BITS = 256 # R_flag must be < 2^256

# Define reasonable payload length limits
MIN_PAYLOAD_LEN = 0  # For "AIS3{}"
MAX_PAYLOAD_LEN = 28 # Max total flag length 5 (AIS3{) + 28 + 1 (}) = 34 bytes.
                     # 34 bytes = 272 bits. R_cand iterations approx 2^(272-256) = 2^16. Feasible.
                     # Increasing MAX_PAYLOAD_LEN further will significantly increase runtime.

def solve_method2():
    print(f"Using C_FLAG_INT: {hex(C_FLAG_INT)}")
    for payload_len in range(MIN_PAYLOAD_LEN, MAX_PAYLOAD_LEN + 1):
        L_bytes = len(KNOWN_PREFIX) + payload_len + len(KNOWN_SUFFIX)
        L_bits = L_bytes * 8

        print(f"\nTrying flag length: {L_bytes} bytes ({L_bits} bits)...")

        if L_bits == 0 and L_bytes > 0 : continue # Should not happen with loop range
        if L_bits > 512 : continue # F_int cannot be effectively longer than C_FLAG_INT for this logic

        # R_sq_expected_MSB_part: The MSBs of R_flag^2 that should match C_FLAG_INT's MSBs
        # for F_int_cand to be L_bits long (i.e., F_int_cand >> L_bits == 0).
        R_sq_expected_MSB_part = C_FLAG_INT >> L_bits

        # Construct the min and max possible R_flag^2 values that have this MSB part
        R_sq_min_for_this_MSB = R_sq_expected_MSB_part << L_bits
        R_sq_max_for_this_MSB = R_sq_min_for_this_MSB | ((1 << L_bits) - 1)

        R_start = isqrt_ceil(R_sq_min_for_this_MSB)
        R_end = isqrt_floor(R_sq_max_for_this_MSB)

        # Filter R_start and R_end to ensure R_flag < 2^256
        # R_flag can be 0, so R_start can be 0.
        R_start = max(0, R_start)
        R_end = min((1 << R_MAX_BITS) - 1, R_end) # R_flag <= 2^256 - 1

        num_r_candidates = 0
        if R_end >= R_start:
            num_r_candidates = R_end - R_start + 1
        
        print(f"  R_flag search range: [{hex(R_start)}, {hex(R_end)}]. Candidates: {num_r_candidates}")

        if num_r_candidates == 0:
            continue
        # Optional: Heuristic skip for excessively large candidate spaces if needed
        # if num_r_candidates > (1 << 22): # Approx 4 million, for example
        #     print(f"  Skipping L_bytes={L_bytes} due to very large R candidate space ({num_r_candidates}).")
        #     continue
            
        for R_cand_idx, R_cand in enumerate(range(R_start, R_end + 1)):
            # R_cand is already ensured to be < 2^256 by the R_end filter.
            
            if num_r_candidates > 100000 and R_cand_idx % (num_r_candidates // 100 + 1) == 0 : # Log progress
                 progress_percent = (R_cand_idx * 100) / num_r_candidates
                 print(f"    R_cand search for L={L_bytes}: {progress_percent:.1f}% ({R_cand_idx}/{num_r_candidates})", end='\r')


            R_cand_sq = R_cand * R_cand

            # Critical Check 1: R_cand_sq must have the expected MSB part.
            # This means (R_cand_sq >> L_bits) should be R_sq_expected_MSB_part.
            # This is equivalent to C_FLAG_INT >> L_bits.
            if (R_cand_sq >> L_bits) != R_sq_expected_MSB_part:
                # This implies R_cand_sq is outside the [R_sq_min_for_this_MSB, R_sq_max_for_this_MSB] interval.
                # This should ideally not happen if R_start and R_end are calculated correctly
                # and R_cand is within that range. Could be an edge case with isqrt precision for huge numbers.
                continue

            F_int_cand = C_FLAG_INT ^ R_cand_sq
            
            # Critical Check 2: F_int_cand must be exactly L_bits long (or shorter, fitting into L_bytes).
            # This means (F_int_cand >> L_bits) must be 0.
            # This is automatically true if Critical Check 1 holds, because:
            # (F_int_cand >> L_bits) = (C_FLAG_INT >> L_bits) ^ (R_cand_sq >> L_bits)
            #                       = R_sq_expected_MSB_part ^ R_sq_expected_MSB_part = 0
            if (F_int_cand >> L_bits) != 0:
                # This should not be reached if check 1 passes. Redundant but safe.
                continue
            
            try:
                # F_int_cand is now known to be representable in L_bits.
                # Convert to L_bytes, possibly with leading null bytes if F_int_cand is shorter.
                F_bytes_cand = F_int_cand.to_bytes(L_bytes, 'big')
            except OverflowError:
                # This occurs if F_int_cand is actually longer than L_bits,
                # which should have been caught by "(F_int_cand >> L_bits) != 0".
                continue 

            if F_bytes_cand.startswith(KNOWN_PREFIX) and F_bytes_cand.endswith(KNOWN_SUFFIX):
                payload_start_idx = len(KNOWN_PREFIX)
                payload_end_idx = L_bytes - len(KNOWN_SUFFIX)

                if payload_start_idx <= payload_end_idx: 
                    payload = F_bytes_cand[payload_start_idx:payload_end_idx]
                    try:
                        payload_str = payload.decode('ascii')
                        is_printable_payload = all(32 <= ord(c) < 127 for c in payload_str)
                        
                        if is_printable_payload or not payload: # Allow empty payload
                            # Clear progress line
                            print(" " * 80, end='\r')
                            print(f"\n!!! POTENTIAL FLAG FOUND !!!")
                            print(f"  Length: {L_bytes} bytes (Payload length: {payload_len})")
                            print(f"  R_cand: {hex(R_cand)}")
                            print(f"  R_cand_sq: {hex(R_cand_sq)}")
                            print(f"  F_int_cand: {hex(F_int_cand)}")
                            print(f"  Flag: {F_bytes_cand.decode('ascii')}")
                            return # Exit after finding the first plausible match
                    except UnicodeDecodeError:
                        pass # Payload not ASCII
        # Clear progress line after each L_bytes iteration if candidates were searched
        if num_r_candidates > 0:
            print(" " * 80, end='\r')


    print("\nNo flag found with this method and current parameters.")
    print("Consider adjusting MIN_PAYLOAD_LEN, MAX_PAYLOAD_LEN, or checking C_FLAG_INT.")

if __name__ == "__main__":
    # Ensure you have Python 3.8+ for math.isqrt or provide your own implementation.
    # Example: if not hasattr(math, 'isqrt'):
    #    def math_isqrt_replacement(n): /* ... your isqrt_binary_search ... */; math.isqrt = math_isqrt_replacement
    if not hasattr(math, 'isqrt'):
        print("Warning: math.isqrt not found (requires Python 3.8+).")
        print("Please use a Python version with math.isqrt or add a custom isqrt_floor implementation.")
        # As a simple fallback for demonstration, this will be slow for large numbers:
        # def fallback_isqrt(n):
        #     if n==0: return 0
        #     x = int(n**0.5)
        #     if (x+1)**2 <= n: x+=1
        #     if x**2 > n: x-=1
        #     return x
        # math.isqrt = fallback_isqrt 
        # Better: use the previously discussed isqrt_binary_search and assign it to math.isqrt if needed
        # For now, this script will rely on system's math.isqrt.
    solve_method2()

Flag: AIS3{no_more_junks...plz}

stream_01

Random_RSA

LLM 使用紀錄

https://g.co/gemini/share/19fb945b6b8c

Exploit

from Crypto.Util.number import long_to_bytes, inverse
import gmpy2 # For is_prime, as used in the challenge
from sympy import sqrt_mod # For modular square root

def solve_ctf():
    # --- 1. Parse output.txt ---
    # Manually extract these values from your output.txt file
    # For the purpose of this script, I'll use placeholders.
    # Replace these with the actual values from your output.txt
    
    # Example (replace with your actual values):
    # h0 = 2907912348071002191916245879840138889735709943414364520299382570212475664973498303148546601830195365671249713744375530648664437471280487562574592742821690
    # h1 = 5219570204284812488215277869168835724665994479829252933074016962454040118179380992102083718110805995679305993644383407142033253210536471262305016949439530
    # h2 = 3292606373174558349287781108411342893927327001084431632082705949610494115057392108919491335943021485430670111202762563173412601653218383334610469707428133
    # M_val = 9231171733756340601102386102178805385032208002575584733589531876659696378543482750405667840001558314787877405189256038508646253285323713104862940427630413
    # n_val = 20599328129696557262047878791381948558434171582567106509135896622660091263897671968886564055848784308773908202882811211530677559955287850926392376242847620181251966209002883852930899738618123390979377039185898110068266682754465191146100237798667746852667232289994907159051427785452874737675171674258299307283
    # e_val = 65537
    # c_val = 13859390954352613778444691258524799427895807939215664222534371322785849647150841939259007179911957028718342213945366615973766496138577038137962897225994312647648726884239479937355956566905812379283663291111623700888920153030620598532015934309793660829874240157367798084893920288420608811714295381459127830201

    # Read from output.txt
    data = {}
    try:
        with open("output.txt", "r") as f:
            for line in f:
                key, value = line.strip().split(" = ")
                data[key.strip()] = int(value)
        h0 = data['h0']
        h1 = data['h1']
        h2 = data['h2']
        M_val = data['M']
        n_val = data['n']
        e_val = data['e']
        c_val = data['c']
        print("Successfully parsed output.txt")
    except FileNotFoundError:
        print("Error: output.txt not found. Please ensure the file is in the same directory.")
        print("Using placeholder values for demonstration if you don't have output.txt")
        # Placeholder values if output.txt is not found (script will likely fail logically)
        h0, h1, h2 = 1, 2, 3 
        M_val, n_val, e_val, c_val = 17, 33, 3, 1
        return
    except Exception as e:
        print(f"Error parsing output.txt: {e}")
        return

    m = M_val # LCG modulus

    # --- 2. Recover LCG parameters a, b ---
    # a = (h2 - h1) * (h1 - h0)^-1 mod m
    # b = (h1 - a * h0) mod m
    
    diff_h1_h0 = (h1 - h0 + m) % m
    if diff_h1_h0 == 0:
        print("Error: h1 - h0 is zero modulo m, cannot find a.")
        return
        
    inv_diff_h1_h0 = inverse(diff_h1_h0, m)
    
    a_lcg = ((h2 - h1 + m) % m * inv_diff_h1_h0) % m
    b_lcg = (h1 - (a_lcg * h0) % m + m) % m
    
    print(f"Recovered LCG parameters:")
    print(f"a = {a_lcg}")
    print(f"b = {b_lcg}")
    print(f"m = {m}")

    # --- 3. Iterate k_q and solve for p ---
    n_mod_m = n_val % m
    
    # Max iterations for k_q. Average is ln(m) ~ 354 for 512-bit m.
    # Let's try up to a higher value just in case.
    max_kq = 2000 
    
    found_p, found_q = -1, -1

    for k_q in range(1, max_kq + 1):
        if k_q % 100 == 0:
             print(f"Trying k_q = {k_q}...")

        # Calculate A0 = a^k_q mod m
        A0 = pow(a_lcg, k_q, m)
        
        # Calculate S_kq = sum_{j=0}^{k_q-1} a_lcg^j mod m
        # S_kq = (a_lcg^k_q - 1) * (a_lcg - 1)^-1 mod m
        if a_lcg == 1:
            S_kq = k_q % m
        else:
            inv_a_minus_1 = inverse(a_lcg - 1 + m, m) # (a-1)^-1 mod m
            S_kq = ((A0 - 1 + m) % m * inv_a_minus_1) % m
            
        # Calculate B0 = b_lcg * S_kq mod m
        B0 = (b_lcg * S_kq) % m
        
        # Solve A0*p^2 + B0*p - n_mod_m = 0 (mod m)
        # This is A0*p^2 + B0*p + C0 = 0 (mod m) where C0 = -n_mod_m
        C0 = (-n_mod_m + m) % m
        
        # Discriminant D = B0^2 - 4*A0*C0 mod m
        D_sqrt_term = (B0 * B0 - 4 * A0 * C0) % m
        if D_sqrt_term < 0: D_sqrt_term += m # Ensure positive before sqrt_mod

        try:
            # sympy.sqrt_mod can return multiple roots, or raise an error if no root
            # For prime m, there are 0 or 2 roots (or 1 if D_sqrt_term is 0)
            Y_roots = sqrt_mod(D_sqrt_term, m, all_roots=True)
        except ValueError: # No modular square root exists
            continue
            
        if not Y_roots:
            continue

        inv_2A0 = inverse(2 * A0, m) # (2*A0)^-1 mod m
        
        candidate_ps = []
        for Y in Y_roots:
            p_cand = ((Y - B0 + m) % m * inv_2A0) % m
            candidate_ps.append(p_cand)
            # Y is one root, m-Y is the other for Y^2 = D. sympy handles this.

        for p_c in candidate_ps:
            if p_c == 0: continue
            if n_val % p_c == 0:
                q_c = n_val // p_c
                
                # Check primality (using gmpy2 as in challenge)
                if not gmpy2.is_prime(p_c) or not gmpy2.is_prime(q_c):
                    continue
                if p_c == q_c : # p and q should be different for RSA typically
                    continue

                # --- Critical LCG Verification for q_c from p_c with k_q iterations ---
                val_check = p_c
                is_correct_q_generation = False
                for current_k_iter in range(1, k_q + 1):
                    val_check = (a_lcg * val_check + b_lcg) % m
                    if current_k_iter < k_q:
                        if gmpy2.is_prime(val_check):
                            # genPrime(p_c) would have returned this earlier prime
                            is_correct_q_generation = False
                            break 
                    elif current_k_iter == k_q: # This is the k_q-th iteration
                        if gmpy2.is_prime(val_check) and val_check == q_c:
                            is_correct_q_generation = True
                        else:
                            is_correct_q_generation = False
                        break # Stop after checking k_q-th iteration
                
                if is_correct_q_generation:
                    found_p = p_c
                    found_q = q_c
                    print(f"\n!!! Found p and q at k_q = {k_q} !!!")
                    print(f"p = {found_p}")
                    print(f"q = {found_q}")
                    break # Break from p_c loop
            if found_p != -1: break # Break from Y_roots loop
        if found_p != -1: break # Break from k_q loop

    if found_p == -1:
        print(f"Failed to find p and q after {max_kq} iterations for k_q.")
        return

    # --- 4. Decrypt the flag ---
    phi_n = (found_p - 1) * (found_q - 1)
    d_val = inverse(e_val, phi_n)
    
    decrypted_m_int = pow(c_val, d_val, n_val)
    
    try:
        flag = long_to_bytes(decrypted_m_int)
        print(f"\nSuccessfully decrypted message (FLAG): {flag}")
        # Python 3 requires decoding if it's text
        try:
            print(f"Decoded FLAG: {flag.decode('utf-8')}")
        except UnicodeDecodeError:
            print(f"FLAG (raw bytes): {flag}")
            
    except Exception as e:
        print(f"Error converting number to bytes: {e}")
        print(f"Decrypted number: {decrypted_m_int}")

if __name__ == "__main__":
    # Ensure output.txt is in the same directory as this script,
    # or replace the placeholder values at the start of solve_ctf()
    # with the actual values from your output.txt.
    solve_ctf()

Flag: AIS3{1_d0n7_r34lly_why_1_d1dn7_u53_637pr1m3}

rndrsa_01

Hill

LLM 使用紀錄

https://g.co/gemini/share/ca151da594d9

Exploit

#!/usr/bin/env python3
from pwn import *
import numpy as np
try:
    from sympy import Matrix
    sympy_available = True
except ImportError:
    sympy_available = False
    log.warning("Sympy library not found. Matrix inversion will use a less precise method (adjugate) which may lead to errors.")
    log.warning("Please install sympy for best results: pip install sympy")

# --- Modular Arithmetic and Matrix Functions ---
def modular_inverse_num(a, m):
    return pow(a, m - 2, m)

def matrix_mod_inverse_sympy(matrix_np, modulus):
    if not sympy_available:
        raise ImportError("Sympy is not available, cannot use sympy_mod_inverse.")
    log.debug(f"Attempting to invert matrix using sympy:\n{matrix_np}")
    matrix_list_of_lists = matrix_np.tolist()
    matrix_sympy = Matrix(matrix_list_of_lists)
    try:
        inverse_sympy = matrix_sympy.inv_mod(modulus)
        log.debug(f"Sympy inverse matrix:\n{inverse_sympy}")
        return np.array(inverse_sympy.tolist(), dtype=int)
    except Exception as e: 
        log.error(f"Sympy could not invert matrix: {e}")
        raise ValueError(f"Matrix is singular or other error with sympy.inv_mod: {e}")

def matrix_mod_inverse_adjugate(matrix_input, modulus):
    # ... (adjugate function from previous version - kept as fallback) ...
    log.debug("Attempting to invert matrix using adjugate method...")
    matrix = np.array(matrix_input, dtype=np.int64)
    n_rows = matrix.shape[0]
    det_float = np.linalg.det(matrix)
    det = int(round(det_float)) % modulus
    if det == 0:
        log.error(f"Adjugate: Matrix determinant ({det_float} -> {det} mod {modulus}) is 0. Cannot invert.")
        raise ValueError("Matrix is singular (mod p) via adjugate.")
    det_inv = modular_inverse_num(det, modulus)
    adjugate = np.zeros_like(matrix, dtype=np.int64)
    for r_idx in range(n_rows):
        for c_idx in range(n_rows):
            minor_matrix_val = np.delete(np.delete(matrix, r_idx, axis=0), c_idx, axis=1)
            cofactor_val = ((-1)**(r_idx + c_idx)) * int(round(np.linalg.det(minor_matrix_val)))
            adjugate[c_idx, r_idx] = cofactor_val % modulus 
    inverse_matrix = (det_inv * adjugate) % modulus
    return inverse_matrix.astype(int)

# --- CTF Parameters ---
HOST = "chals1.ais3.org"
PORT = 18000
P = 251
N = 8
context.log_level = 'debug'

# --- Helper Functions ---
def parse_ciphertext_block_from_line(line_bytes):
    # ... (parser function from previous version - it was working well) ...
    s = line_bytes.decode(errors='ignore').strip()
    if not s:
        log.debug("parse_ciphertext_block_from_line: received empty string to parse.")
        return None
    if not s.startswith('[') or not s.endswith(']'):
        log.debug(f"parse_ciphertext_block_from_line: Unexpected line format (not a block): {s}")
        return None
    numbers_str = s[1:-1].split() 
    if not numbers_str and s != "[]":
         log.debug(f"parse_ciphertext_block_from_line: no numbers found in non-empty brackets: {s}")
         return None
    if not numbers_str and s == "[]": # Explicitly allow "[]" to parse as empty array
        return np.array([], dtype=int)
    try:
        return np.array([int(x) for x in numbers_str], dtype=int)
    except ValueError as e:
        log.debug(f"parse_ciphertext_block_from_line: ValueError parsing numbers in block '{s}': {e}")
        return None

# --- Main Logic ---
conn = None
try:
    log.info("Establishing connection to server...")
    conn = remote(HOST, PORT)
    log.debug("Connection established.")

    # 1. Receive the target encrypted flag for this session
    log.info("Receiving target encrypted flag for this session...")
    target_encrypted_flag_blocks = []
    try:
        log.debug("Waiting for 'input: ' prompt (will consume all preceding data)...")
        all_initial_server_output = conn.recvuntil(b"input: ", timeout=10, drop=False)
        log.debug(f"Received all data up to 'input:'. Total bytes: {len(all_initial_server_output)}")
    except PwnlibException as e:
        log.critical(f"Timeout or error waiting for 'input:' prompt: {e}")
        if conn: conn.close()
        exit(1)
    
    output_str = all_initial_server_output.decode(errors='ignore')
    lines = output_str.splitlines()
    log.info(f"Processing {len(lines)} lines from initial server output.")
    found_banner = False
    for line_text in lines:
        log.debug(f"Processing line: {line_text!r}")
        if not found_banner:
            if "Encrypted flag:" in line_text:
                found_banner = True
                log.debug("'Encrypted flag:' banner found.")
            continue 
        stripped_line_text = line_text.strip()
        if stripped_line_text.startswith("[") and stripped_line_text.endswith("]"):
            parsed = parse_ciphertext_block_from_line(stripped_line_text.encode()) 
            if parsed is not None and parsed.shape == (N,):
                target_encrypted_flag_blocks.append(parsed)
                log.debug(f"Successfully parsed and added flag block (shape {parsed.shape}).")
            else:
                log.warning(f"Could not parse or invalid shape for flag block line: {stripped_line_text!r}")
        elif "input:" in stripped_line_text: # Should be the end of all_initial_server_output
            break
    
    if not target_encrypted_flag_blocks:
        log.critical("Failed to retrieve or parse any target encrypted flag blocks. Exiting.")
        if conn: conn.close()
        exit(1)
    log.success(f"Successfully received {len(target_encrypted_flag_blocks)} target encrypted flag blocks for this session.")

    # 2. Construct and send the special chosen plaintext to find A and B
    log.info(f"Constructing special {2*N}-block plaintext to determine A and B...")
    chosen_plaintext_payload_bytes = b""
    for j in range(N): # For each column j
        # Block P'_{2j} = e_j
        ej_vec = np.zeros(N, dtype=int)
        ej_vec[j] = 1
        chosen_plaintext_payload_bytes += bytes([int(x) for x in ej_vec])
        
        # Block P'_{2j+1} = zero_vector
        zero_vec = np.zeros(N, dtype=int)
        chosen_plaintext_payload_bytes += bytes([int(x) for x in zero_vec])
    
    log.info(f"Sending {2*N}-block chosen plaintext ({len(chosen_plaintext_payload_bytes)} bytes)...")
    log.debug(f"Chosen plaintext hexdump:\n{hexdump(chosen_plaintext_payload_bytes)}")
    conn.sendline(chosen_plaintext_payload_bytes)

    # 3. Receive and parse the 2N ciphertext blocks corresponding to the chosen plaintext
    log.info(f"Receiving {2*N} ciphertext blocks for chosen plaintext...")
    chosen_ciphertext_blocks_response = []
    for i in range(2 * N): # Expecting 2N blocks
        try:
            line = conn.recvline(timeout=3.0).strip()
            log.debug(f"Received chosen ciphertext line {i+1}/{2*N}: {line!r}")
            if not line:
                log.error(f"Received empty line when expecting chosen ciphertext block {i+1}. Server may have closed early.")
                raise PwnlibException("Empty line from server for chosen ciphertext.")
            parsed = parse_ciphertext_block_from_line(line)
            if parsed is not None and parsed.shape == (N,):
                chosen_ciphertext_blocks_response.append(parsed)
            else:
                log.error(f"Failed to parse chosen ciphertext block {i+1} or wrong shape: {line!r}")
                raise PwnlibException("Parse error for chosen ciphertext.")
        except (PwnlibException, EOFError) as e:
            log.critical(f"Error receiving chosen ciphertext block {i+1}/{2*N}: {e}")
            if conn: conn.close()
            exit(1)
            
    if len(chosen_ciphertext_blocks_response) != 2 * N:
        log.critical(f"Expected {2*N} chosen ciphertext blocks, got {len(chosen_ciphertext_blocks_response)}. Exiting.")
        if conn: conn.close()
        exit(1)
    log.success(f"Successfully received {len(chosen_ciphertext_blocks_response)} chosen ciphertext blocks.")

    # 4. Reconstruct matrices A and B
    log.info("Reconstructing matrices A and B for this session...")
    A_sess = np.zeros((N, N), dtype=int)
    B_sess = np.zeros((N, N), dtype=int)

    for j in range(N): # For each column j
        A_sess[:, j] = chosen_ciphertext_blocks_response[2*j]      # C'_{2j}
        B_sess[:, j] = chosen_ciphertext_blocks_response[2*j + 1]  # C'_{2j+1}
    
    log.success("Matrices A_sess and B_sess reconstructed.")
    log.debug(f"A_sess = \n{A_sess}")
    log.debug(f"B_sess = \n{B_sess}")

    # 5. Calculate A_sess_inv
    log.info("Calculating A_sess_inv (inverse of A_sess mod P)...")
    try:
        if sympy_available:
            log.info("Attempting matrix inversion using Sympy...")
            A_sess_inv = matrix_mod_inverse_sympy(A_sess, P) 
        else:
            log.warning("Sympy not available. Attempting matrix inversion using adjugate method (less precise)...")
            A_sess_inv = matrix_mod_inverse_adjugate(A_sess, P)
        log.success("Matrix A_sess_inv calculated.")
        
        identity_check = (A_sess @ A_sess_inv) % P
        if not np.array_equal(identity_check, np.identity(N, dtype=int)):
           log.warning("Verification WARN: (A_sess @ A_sess_inv) % P is NOT the identity matrix! Decryption might be incorrect.")
           log.debug(f"(A_sess @ A_sess_inv) % P = \n{identity_check}") 
        else:
           log.info("Verification successful: (A_sess @ A_sess_inv) % P is Identity.")
    except ValueError as e: 
        log.critical(f"Could not invert matrix A_sess: {e}. Exiting.")
        if conn: conn.close()
        exit(1)

    # 6. Decrypt the target_encrypted_flag_blocks
    log.info("Decrypting the session's target flag blocks...")
    decrypted_flag_pt_blocks = []
    F_C0 = target_encrypted_flag_blocks[0]
    F_P0 = (A_sess_inv @ F_C0) % P
    decrypted_flag_pt_blocks.append(F_P0.astype(int))

    for i in range(1, len(target_encrypted_flag_blocks)):
        F_Ci = target_encrypted_flag_blocks[i]
        F_P_prev = decrypted_flag_pt_blocks[i-1]
        term_B_mult_FP_prev = (B_sess @ F_P_prev) % P
        diff_term = (F_Ci - term_B_mult_FP_prev + P) % P 
        F_Pi = (A_sess_inv @ diff_term) % P
        decrypted_flag_pt_blocks.append(F_Pi.astype(int))
    log.success("Flag blocks decrypted.")

    # 7. Convert decrypted blocks to string
    flag_bytes_list = []
    for block_vector in decrypted_flag_pt_blocks:
        for byte_val in block_vector:
            flag_bytes_list.append(byte_val)
    while len(flag_bytes_list) > 0 and flag_bytes_list[-1] == 0: 
        flag_bytes_list.pop()
    final_flag_chars = [chr(b_val) for b_val in flag_bytes_list if 0 <= b_val < 256]
    final_flag = "".join(final_flag_chars)

    log.success("="*40)
    log.success(f"DECRYPTED FLAG: {final_flag}")
    log.success("="*40)

except Exception as e:
    log.critical(f"An critical error occurred in the main script: {e}", exc_info=True)
finally:
    if conn:
        log.debug("Ensuring final connection closure.")
        try:
            conn.close()
        except Exception as e_close_final:
            log.debug(f"Exception during final close: {e_close_final}")

hill_01

Flag: AIS3{b451c_h1ll_c1ph3r_15_2_3z_f0r_u5}

我不會密碼學。

Scoreboard

score_preexam

score_preexam_time