TJCTF 2025
本文最后更新于11 天前,其中的信息可能已经过时,如有错误请发送邮件到1157395387@qq.com

TJCTF密码题部分

crypto/c

解密逻辑简述

  1. enc.py 使用了 培根密码 (Baconian Cipher)flag.txt 中的字符转为二进制(5位),用大写/小写字母表示。
  2. 再对结果的每个字符执行 chr(ord(c) - 13) 加密,写入 out.txt
  3. 因此我们需要:对 out.txt 中每个字符执行 chr(ord(c) + 13) 还原出大小写编码,再解析出大小写序列,映射回原始的 Baconian 二进制,再解出明文。
# 1. 反转 Baconian 映射表
baconian = {
  '00000': 'a', '00001': 'b',
  '00010': 'c', '00011': 'd',
  '00100': 'e', '00101': 'f',
  '00110': 'g', '00111': 'h',
  '01000': 'i', '01001': 'k',
  '01010': 'l', '01011': 'm',
  '01100': 'n', '01101': 'o',
  '01110': 'p', '01111': 'q',
  '10000': 'r', '10001': 's',
  '10010': 't', '10011': 'u', # v 同为 u
  '10100': 'w', '10101': 'x',
  '10110': 'y', '10111': 'z'
}

# 2. 读取加密输出并逆向 chr(ord(c) - 13)
with open("out.txt", "r") as f:
  encrypted = f.read().strip()

# 3. 还原大小写序列(对应二进制)
decoded = ''.join([chr(ord(c) + 13) for c in encrypted])

# 4. 按每 5 个字符一组提取大写/小写 => 二进制串
bits = []
for i in range(0, len(decoded), 5):
  group = decoded[i:i+5]
  bit_string = ''.join(['1' if c.isupper() else '0' for c in group])
  bits.append(bit_string)

# 5. 将二进制映射回字符
plaintext = ''.join([baconian.get(b, '?') for b in bits])
print("Decrypted flag:", plaintext)

我们将得出解密后的字符为tictfoinkooinkoooinkooooink 要把tictf改为tjctf后面的东西用花括号包起来就是flag:tjctf{oinkooinkoooinkooooink}

crypto/alchemist-recipe

解题脚本

import hashlib

SNEEZE_FORK = "AurumPotabileEtChymicumSecretum"
WUMBLE_BAG = 8

def glorbulate_sprockets_for_bamboozle(blorbo):
  zing = {}
  yarp = hashlib.sha256(blorbo.encode()).digest()
  zing['flibber'] = list(yarp[:WUMBLE_BAG])
  zing['twizzle'] = list(yarp[WUMBLE_BAG:WUMBLE_BAG+16])
  glimbo = list(yarp[WUMBLE_BAG+16:])
  snorb = list(range(256))
  sploop = 0
  for _ in range(256):
      for z in glimbo:
          wob = (sploop + z) % 256
          snorb[sploop], snorb[wob] = snorb[wob], snorb[sploop]
          sploop = (sploop + 1) % 256
  zing['drizzle'] = snorb
  return zing

def descrungle_crank(chunk, sprockets):
  wiggle = sprockets['flibber']
  quix = sprockets['twizzle']
  drizzle = sprockets['drizzle']

  # 反向排序
  waggly = sorted([(wiggle[i], i) for i in range(WUMBLE_BAG)])
  zort = [oof for _, oof in waggly]
  unsorted = [0] * WUMBLE_BAG
  for y in range(WUMBLE_BAG):
      x = zort[y]
      unsorted[x] = chunk[y]
  splatted = bytes(unsorted)

  # 异或还原
  zonked = bytes([splatted[i] ^ quix[i % len(quix)] for i in range(WUMBLE_BAG)])

  # drizzle 的逆映射
  drizzle_inv = [0] * 256
  for i, val in enumerate(drizzle):
      drizzle_inv[val] = i

  # 原始数据恢复
  original = bytes([drizzle_inv[b] for b in zonked])
  return original

def unsnizzle_bytegum(data, jellybean):
  decrypted = b""
  for i in range(0, len(data), WUMBLE_BAG):
      chunk = data[i:i+WUMBLE_BAG]
      decrypted += descrungle_crank(chunk, jellybean)

  # 去除 PKCS#7 padding
  pad_len = decrypted[-1]
  if all(p == pad_len for p in decrypted[-pad_len:]):
      decrypted = decrypted[:-pad_len]
  return decrypted

def decrypt():
  with open("encrypted.txt", "r") as f:
      encrypted_hex = f.read().strip()
  encrypted_bytes = bytes.fromhex(encrypted_hex)

  jellybean = glorbulate_sprockets_for_bamboozle(SNEEZE_FORK)
  decrypted = unsnizzle_bytegum(encrypted_bytes, jellybean)
  print("Decrypted flag:", decrypted.decode())

if __name__ == "__main__":
  decrypt()

crypto/theartofwar

打开以后发现是个RSA加密,类型是多模数攻击通过脚本解密后的flag为:tjctf{the_greatest_victory_is_that_which_require_no_battle}

from Crypto.Util.number import long_to_bytes
from sympy.ntheory.modular import crt
from gmpy2 import iroot
import re

# 读取 output.txt
with open("output.txt", "r") as f:
  data = f.read()

# 提取 e、n、c 值
e = int(re.search(r"e\s*=\s*(\d+)", data).group(1))
pairs = re.findall(r"n\d+\s*=\s*(\d+)\s+c\d+\s*=\s*(\d+)", data)

n_list = [int(n) for n, _ in pairs]
c_list = [int(c) for _, c in pairs]

# 使用中国剩余定理合并
C, N = crt(n_list, c_list)

# 计算 e 次根(m ≈ (C)^(1/e))
m_root, exact = iroot(C, e)
if not exact:
  print("Warning: root not exact, result may be incorrect.")

# 转换为原始明文
flag = long_to_bytes(m_root)
print("Recovered flag:", flag)

crypto/seeds/种子

这道题是一个随机数预测攻击 + AES ECB 解密的问题。最后的flag为tjctf{h4rv3st_t1me}

from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from datetime import datetime, timedelta
import time

class RandomGenerator:
  def __init__(self, seed, modulus=2 ** 32, multiplier=157, increment=1):
      if isinstance(seed, str):
          seed = int.from_bytes(seed.encode(), "big")
      self.seed = seed
      self.m = modulus
      self.a = multiplier
      self.c = increment

  def randint(self, bits: int):
      self.seed = (self.a * self.seed + self.c) % self.m
      result = self.seed.to_bytes(4, "big")
      while len(result) < bits // 8:
          self.seed = (self.a * self.seed + self.c) % self.m
          result += self.seed.to_bytes(4, "big")
      return int.from_bytes(result, "big") % (2 ** bits)

  def randbytes(self, length: int):
      return self.randint(length * 8).to_bytes(length, "big")
       
ciphertext = bytes.fromhex('...') # 替换为你从服务拿到的 ciphertext

# 设定服务器可能启动时间范围(调整为你的时区偏移)
start = datetime(2025, 6, 7, 8, 0, 0) # 可能是早上8点
end = datetime(2025, 6, 7, 10, 0, 0)   # 到10点
delta = timedelta(seconds=1)

print("Trying time window:", start, "to", end)
cur = start
while cur <= end:
  seed_str = time.asctime(cur.timetuple())
  rng = RandomGenerator(seed_str)
  key = rng.randbytes(32)

  cipher = AES.new(key, AES.MODE_ECB)
  try:
      plain = unpad(cipher.decrypt(ciphertext), 16)
      if b'tjctf{' in plain:
          print("[!] Found:", plain.decode())
          break
  except:
      pass
  cur += delta

可以直接使用kali去nc tjc.tf 31493你会获取如下图

我们需要的这个东西,将这串代码放到这个地方就可以得出完整的解密脚本

ciphertext = bytes.fromhex('...')  # 替换为你从服务拿到的 ciphertext

b’I<B\x8f7\x1a\x9d\xba\xcb=Dz8\x97\xe9c\xb7\xaf\x15\x01\xf4\xd9\xd9\xc2\x83jm\x1a\xa2\xda\x10\xb5′

填入后的完整脚本
ciphertext = b'I<B\x8f7\x1a\x9d\xba\xcb=Dz8\x97\xe9c\xb7\xaf\x15\x01\xf4\xd9\xd9\xc2\x83jm\x1a\xa2\xda\x10\xb5'

TJCTFMISC部分

misc/guess-my-number

可以直接kali连接去自己猜一猜,感觉有点emm简单了,也可以自己写个脚本让他自己猜

from pwn import *

def auto_guess():
  # 连接远程服务器
  conn = remote('tjc.tf', 31700)

  low = 1
  high = 1000
  for _ in range(10):
      guess = (low + high) // 2
      conn.recvuntil(b"Guess a number from 1 to 1000:")
      conn.sendline(str(guess).encode())

      res = conn.recvline().decode()
      print(f"Guessed {guess} -> {res.strip()}")

      if "Too low" in res:
          low = guess + 1
      elif "Too high" in res:
          high = guess - 1
      elif "You won" in res:
          # 打印剩余信息(包含 flag)
          print(conn.recvall().decode())
          break

if __name__ == "__main__":
  auto_guess()

misc/mouse-trail

通过题目得知是绘画的坐标系写一个py脚本

import matplotlib.pyplot as plt

# 读取坐标文件
def read_coordinates(file_path):
  coordinates = []
  with open(file_path, 'r') as file:
      for line in file:
          try:
              x_str, y_str = line.strip().split(',')
              x, y = int(x_str), int(y_str)
              coordinates.append((x, y))
          except ValueError:
              # 忽略无效行
              continue
  return coordinates

# 主程序
file_path = 'mouse_movements.txt' # 请确保该文件存在于当前目录
coordinates = read_coordinates(file_path)

# 拆分 x 和 y
x_vals, y_vals = zip(*coordinates)

# 绘制图形
plt.figure(figsize=(12, 8))
plt.scatter(x_vals, y_vals, s=10, color='blue')
plt.title("Scatter Plot of Coordinates")
plt.xlabel("X Coordinate")
plt.ylabel("Y Coordinate")
plt.grid(True)
plt.tight_layout()
plt.show()

这个时候会得到一个图片,我们可以发现是镜像的图片,通过反转可以发现flag

答案就是tjctf{we_love_cartesian_plane}

misc/make-groups

这道题是让我们计算一个大组合数乘机,作为结果输出flag

# calc_fast.py
MOD = 998244353


def precompute_factorials(n, mod):
factorial = [1] * (n + 1)
inv_fact = [1] * (n + 1)
for i in range(1, n + 1):
factorial[i] = factorial[i - 1] * i % mod
# 费马小定理求逆元
inv_fact[n] = pow(factorial[n], mod - 2, mod)
for i in range(n - 1, -1, -1):
inv_fact[i] = inv_fact[i + 1] * (i + 1) % mod
return factorial, inv_fact


def choose(n, r, fact, inv_fact):
if r < 0 or r > n:
return 0
return fact[n] * inv_fact[r] % MOD * inv_fact[n - r] % MOD


def main():
with open("chall.txt") as f:
lines = f.read().splitlines()
n = int(lines[0])
a = list(map(int, lines[1].split()))

fact, inv_fact = precompute_factorials(n, MOD)

ans = 1
for x in a:
ans = ans * choose(n, x, fact, inv_fact) % MOD

print(f"tjctf{{{ans}}}")


if __name__ == "__main__":
main()

misc/golf-hardester

题目告诉我们是一个正则高尔夫挑战,说白了就是脑经急转弯,要用尽可能短的正则表达式,匹配一组给定的字符串,同时排除另一组字符串。

第一关

这个时候我们打开kali直接nc连接后发现一列全是a开头一列全是其他字母开头的东西。

我们要匹配所有左侧的单词,排除右侧单词。所以我们直接^a就可以进入下一关(因为使用最小长度匹配特征,完美符合“正则高尔夫”哲学:最短路径完成目标。)

第二关

要求是匹配所有回文串,排除非回文串。但是这道题非常的有迷惑性便面看是回文匹配,但是细看字符串中都对应的匹配了某些片段特征,而非对映里面都不包含以下这些字符串。

.*(lo|ti|esa|ce|om|va|ef|ate|ste|eto|sak|ew|nel).*

第三关

“Ah yes, quinary, my fifth favorite base.”这块提示我们这是 base-5(五进制)数字匹配题。

根据题目可以看出应匹配的全都是由字符、符 ∈ 0–4

能匹配的合法样本则是这样的

模式示例含义
[03]0, 3单个字符
2[14]*22112, 2442, 22封闭对称结构
[14]1, 4起始块(用于嵌套结构)
2[14]*[03]213, 2410, 21以 2 开头、若干 1/4,最后是 0/3
[03][14]*[03]303, 3143, 0对称结构,两端是 0/3,中间 1/4
[03][14]*2302, 3142以 0/3 开头,1/4 中段,2 结尾

而不合法的样本则无法匹配是因为下列表格所列出的

结构错误示例原因
多个连续 00000, 000[03][14]*[03] 不允许中间是 0
单个字符 14"1", "4"不在 [03],不满足其他结构
不闭合结构2, 210, 2134开头是 2 但后面无法闭合为 2[14]*2[14]*[03]
中间结构不符321, 231332 无法归入合法块
断点错误字符串前半部分是合法的,但后半不能被任何结构吞掉

我们举个合法样本的例子302100312023023123

分段结构如下:

  • 3[03]
  • 0[03]
  • 212[14]*[03]
  • 0020[14]*0、再接 2
  • 302[03][14]*2
  • 3[03]

每一部分都能在正则结构中找到匹配块 ,再举一个不合法样本的例子就可以看出

不合法样本:1441413341001141224132000024031214

问题点如下:

  • 0000 → 不在任何 [03][14]*[03],也不是 2[14]*2,非法
  • 32 → 不在 2[14]*[03] 也不在 [03][14]*2,非法
  • 剩余部分中段结构断裂、无法闭合

总结出来就是:你的字符串能否完全由这些结构块拼接起来?能就合法,不能就不匹配。

块类型匹配样式匹配示例描述
单字符[03]0, 3允许单个 0 或 3
对称块[03][14]*[03]303, 3143左右是 0 或 3,中间 1 或 4
封闭2块2[14]*22112, 2442左右是 2,中间全是 1 或 4
开头结构[14], 2[14]*[03]1, 2, 213用于嵌套组合的起始
结束结构[14], [03][14]*24, 302用于组合结构的结束

这个时候就可以写出一个正则匹配的字符串

^([03]|2[14]*2|([14]|2[14]*[03])(2|[03][14]*[03])*([14]|[03][14]*2))*$
解释就可以为
^(
[03] # 单字符 0 或 3(基础单元)
|
2[14]*2 # 一个 2 开头、1/4 任意次、2 结尾
|
(
[14] | 2[14]*[03] # 起始块:1或4,或2开头配0/3
)
(
2 | [03][14]*[03] # 中间块:2 或者 0/3包裹1/4
)*
(
[14] | [03][14]*2 # 结束块:1/4,或0/3包裹结尾2
)
)*$

第四关

标题暗示我们是一个二进制匹配的内容

我们可以看出0单独存在的时候是合法行为,而1单独存在的时候则是不合法,只有3个1和6个1都单独的时候是合法其他都是不合法行为。通过将合法数据和不合法数据对比跑脚本得出。

所以按照合法样本可以写一个正则匹配为

^(0|(((0|111)|10(0(1|00))*0011)|(110(1)*0|10(0(1|00))*(1|0010(1)*0))(((00(1)*0|1010(1)*0)|1(1|00)(0(1|00))*(1|0010(1)*0)))*((01|1011)|1(1|00)(0(1|00))*0011)))*$

第五关

最后一关,通过数据我们可以得知

通过合法例子能发现

字符串分析
deededd e e d e d → 可以配成嵌套 (d (e e) d) e d
anannaa n a n n a(a (n (a n n) a) n) a
mesosomem e s o s o m e → 每个字符出现偶数次
arraigning虽有字符奇数次,但可以嵌套配对
i只有一个字符,也可视作最浅层的单独结构 (正则允许)
nonordered重复结构都成对

我们再去查看不合法的例子能发现

字符串原因
edifiedd 出现 3 次,无法用嵌套方式闭合
cabbagec, g, e 出现 1 次,孤立
rototiller出现多个奇数频率字母
underpass某些字符无配对路径
ppd只有两个 p 配上了,d 单个,孤立

判断的条件则是

条件是否允许匹配
每个字符都出现偶数次
字符出现奇数次,但能嵌套成对结构(如镜像嵌套)
孤立字符(只出现一次)
某字符出现 3 次,无法嵌套闭合
所有字符都能成对“包围”其他字符(如括号)

通过最后可以写出一个正则为

^(.)(\1|(.)(?=.*$(?<=^((?!\1|\3).|\1(?4)*?\3|\3(?4)*?\1)*)))*$
分析可以得出
^
(.) # 捕获第一个字符到 \1
(\1 # 若第二字符是同一个 \1 → 匹配
|
(
. # 否则捕获一个新字符 \3
(?=
.*$
(?=^(
(?!\1|\3). # 不等于 \1 或 \3 的
|
\1(?\4)*?\3 # \1 之后 \3 之后再回 \1
|
\3(?\4)*?\1 # 或 \3 之后 \1 之后再回 \3
)*)
)
)
)*
$
最终的flag是:tjctf{davidebyzero_is_my_hero_6a452cbdc75f}
只能说是非常的恶心了,如果不是wp出来了我甚至还在第三块卡着呢。而且每次失败都要从头再来。
文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇