reverse

babygame

题目为godot编写的2D游戏,用专门软件dgre进行反编译

软件链接:gdsdecomp:Godot reverse engineering tools - AtomGit | GitCode

找到脚本文件先点进flag.gdc查看,很清晰的AES加密,但题目不会这么简单,同时提示说要吃掉所有金币才可以验证flag,那我们继续查看coin.gdc看看金币干了啥

很明显调用了game_manager来触发加分机制,此时再去看game_manager.gdc

很清晰,1分的时候把flag函数中的key中A改成B,解AES得到flag

wasm-login

一道很纯的web逆向题,打开html文件找调用关系,发现在检测端是将data序列化之后,通过MD5加密,并且检验前16字节是否一致来判断是否正确,那现在问题就是找data是啥了

之前从没做过web逆向,仔细了解了一下wasm汇编发现js会向其中import传入数据同时要通过export传出数据回到js才可以完成调用链,此时随便输入账号密码就可查看release.js函数的关键传入传出函数,可以清晰的看到传入函数传入data.now函数即题目说到的时间戳

通过软件ghidra(需下载插件ghidra_wasm)可以直接将web汇编转为可阅读文本,此时可以找到我们前面提到加密data的authenticate函数,发现真正逻辑藏在function_34中,点进函数查看

结构一目了然了,31-47行对密码进行base64处理之后引入时间戳并转换成字符串之后对massage进行处理,处理结果为:message = {"username":…, "password": encodedPassword}然后下面是加密点,进行SHA256加密并存在signature,signature = HMAC-SHA256( message, timestamp(参与计算) ) 并转成可打印字符串,最后返回final = {"username":..., "password":..., "signature":...}

undefined4 unnamed_function_34(undefined4 param1,undefined **param2)

{
  undefined4 uVar1;
  undefined **ppuVar2;
  undefined4 uVar3;
  int iVar4;
  double dVar5;
  int param2_00;
  undefined **local_4c;
  undefined4 local_48;
  undefined **local_44;
  undefined **local_40;
  undefined8 local_3c;
  undefined **local_34;
  undefined **local_30;
  undefined **local_2c;
  undefined4 local_28;
  undefined **local_24;
  undefined4 local_20;
  undefined4 local_1c;
  undefined **local_18;
  undefined4 local_14;
  undefined4 local_10;
  undefined4 local_c;
  undefined4 local_8;
  
  iVar4 = 0;
  param2_00 = 0;
  if ((0x12ef < (int)&local_30) &&
     (memory_fill(0,0x30,0,&local_30), local_2c = param2, 0x12ef < (int)&local_3c)) {
    local_34 = (undefined **)0x0;
    local_3c = ZEXT48(param2);
    local_30 = (undefined **)unnamed_function_24((uint)param2[-1] >> 1);
    local_3c = ZEXT48(local_30) << 0x20;
    while( true ) {
      local_3c = CONCAT44(local_3c._4_4_,param2);
      if ((int)((uint)param2[-1] >> 1) <= iVar4) break;
      local_3c = CONCAT44(local_3c._4_4_,local_30);
      local_34 = param2;
      uVar1 = unnamed_function_25(param2,iVar4);
      unnamed_function_26(local_30,iVar4,uVar1);
      iVar4 = iVar4 + 1;
    }
    uVar1 = unnamed_function_29(local_30);
    local_28 = uVar1;
    dVar5 = import::env::Date.now();
    ppuVar2 = (undefined **)unnamed_function_36((longlong)dVar5);
    local_30 = &PTR_u_{"username":"_ram_000010d0_ram_00001150;
    DAT_ram_00001154 = param1;
    local_2c = (undefined **)param1;
    local_24 = ppuVar2;
    local_20 = param1;
    local_1c = uVar1;
    unnamed_function_14(&PTR_u_{"username":"_ram_000010d0_ram_00001150,param1,1);
    local_30 = &PTR_u_{"username":"_ram_000010d0_ram_00001150;
    DAT_ram_0000115c = uVar1;
    local_2c = (undefined **)uVar1;
    unnamed_function_14(&PTR_u_{"username":"_ram_000010d0_ram_00001150,uVar1,1);
    local_30 = &PTR_u_{"username":"_ram_000010d0_ram_00001150;
    local_2c = (undefined **)&DAT_ram_00000900;
    local_4c = (undefined **)unnamed_function_31(&PTR_u_{"username":"_ram_000010d0_ram_00001150);
    local_30 = local_4c;
    local_2c = ppuVar2;
    local_18 = local_4c;
    if (0x12ef < (int)&local_4c) {
      memory_fill(0,0x1c,0,&local_4c);
      global_38 = 1;
      uVar3 = unnamed_function_32(local_4c);
      global_38 = 1;
      local_4c = ppuVar2;
      local_48 = uVar3;
      local_4c = (undefined **)unnamed_function_32(ppuVar2);
      local_44 = local_4c;
      local_40 = (undefined **)uVar3;
      iVar4 = unnamed_function_33(local_4c,uVar3);
      local_3c = CONCAT44(iVar4,iVar4);
      local_40 = (undefined **)iVar4;
      ppuVar2 = (undefined **)unnamed_function_24(*(undefined4 *)(iVar4 + -4));
      local_34 = ppuVar2;
      for (; param2_00 < *(int *)(iVar4 + -4); param2_00 = param2_00 + 1) {
        local_40 = ppuVar2;
        unnamed_function_26(ppuVar2,param2_00,(uint)*(byte *)(iVar4 + param2_00));
      }
      local_4c = ppuVar2;
      local_40 = (undefined **)iVar4;
      uVar3 = unnamed_function_29(ppuVar2);
      local_30 = &PTR_u_{"username":"_ram_000010d0_ram_00001230;
      DAT_ram_00001234 = param1;
      local_2c = (undefined **)param1;
      local_14 = uVar3;
      local_10 = param1;
      local_c = uVar1;
      local_8 = uVar3;
      unnamed_function_14(&PTR_u_{"username":"_ram_000010d0_ram_00001230,param1,1);
      local_30 = &PTR_u_{"username":"_ram_000010d0_ram_00001230;
      DAT_ram_0000123c = uVar1;
      local_2c = (undefined **)uVar1;
      unnamed_function_14(&PTR_u_{"username":"_ram_000010d0_ram_00001230,uVar1,1);
      local_30 = &PTR_u_{"username":"_ram_000010d0_ram_00001230;
      DAT_ram_00001244 = uVar3;
      local_2c = (undefined **)uVar3;
      unnamed_function_14(&PTR_u_{"username":"_ram_000010d0_ram_00001230,uVar3,1);
      local_30 = &PTR_u_{"username":"_ram_000010d0_ram_00001230;
      local_2c = (undefined **)&DAT_ram_00000900;
      uVar1 = unnamed_function_31(&PTR_u_{"username":"_ram_000010d0_ram_00001230);
      return uVar1;
    }
  }
  import::env::abort(&DAT_ram_00009310,&DAT_ram_00009340,1,1);
  do {
    halt_trap();
  } while( true );
}

那现在就是找到"username" "password"并对时间戳进行爆破(题目中给出时间为2025.12.21之后一周)账号密码就在271行的备注里(一开始没注意能藏这

一切都齐备了直接写脚本爆破即可,这边我写的

import crypto from "node:crypto";
const PREFIX = "ccaf33e3512e31f3";
const md5hex = (s) => crypto.createHash("md5").update(s, "utf8").digest("hex");
const wallNow = Date.now.bind(Date);
let NOW = 0;
const realNow = Date.now;
Date.now = () => NOW;
const { authenticate } = await import("./build/release.js");
const start = new Date("2025-12-22T00:00:00.000+08:00").getTime();
const end   = new Date("2025-12-22T06:00:00.000+08:00").getTime();
let lastPrint = wallNow();
let iter = 0;
for (NOW = start; NOW <= end; NOW++) { // 1ms step
  const authResult = authenticate("admin", "admin"); 
  const check = md5hex(authResult);                
MD5(JSON.stringify(parsed))
  if (check.startsWith(PREFIX)) {
    Date.now = realNow;
    const ts = NOW;
    const dtCN = new Date(ts).toLocaleString("zh-CN", { timeZone: "Asia/Shanghai", hour12: false });
    console.log("FOUND");
    console.log("timestamp(ms):", ts);
    console.log("time(UTC+8):", dtCN);
    console.log("check:", check);
    console.log(`flag{${check}}`);
    process.exit(0);
  }
  iter++;
  const t = wallNow();
  if (t - lastPrint >= 1000) {
    const pct = ((NOW - start) / (end - start)) * 100;
    const rate = Math.floor(iter / ((t - lastPrint) / 1000));
    console.log(`progress: ${pct.toFixed(2)}% | rate: ~${rate}/s`);
    iter = 0;
    lastPrint = t;
  }
}
Date.now = realNow;
console.log("NOT FOUND in range.");
process.exit(1);
flag{ccaf33e3512e31f36228f0b97ccbc8f1}

eternum

依旧web逆向,这次是流量逆向,wireshark查看流量包内容,发现全是TCP可靠传输数据,同时题目中告诉我们kworker向192.168.8.160:13337发起建立连接请求,所以kworker为客户端/木马类型,所以本体主要还是得看流量到底说了些啥再去逆向kworker,因此从流量抓起。

通过阅读流量发现是客户机向服务器发送一系列长度在64字节左右的数据,并且数据格式相当固定,前8位为魔数,后面紧跟一个len来表示最后紧跟的校验位的长度,然后紧跟密文内容,密文后就是校验位,如下如所示详细解释

客户机发的第一段数据中可以看到,TCP协议规定前8位为ET3RNUMX为固定魔数,后面跟的0X34=52表示payload有52字节长,刚好与后面内容吻合信息内容确定,现在需要看kworker到底给服务端发了什么

第一个思路,看IDA中字符找到关键点(比赛时用的),发现在字符串中找到这么一行文本

题目中说的是AES-GCM加密,根据AES-GCM的特点,密文组成为12 nonce +密文+ 16 tag,其中tag位为校验位,我们正好可以利用这一点,将原文本转为二进制文件对key进行爆破,爆破成功与否只需要在手动做一次与tag的校验即可,由于16位的长度足够,理论上只要tag相同就可以确定爆破的key成功了,这时可以直接爆破了

先挂起一个跟题目条件一样的服务,方便后续kworker去连接

启动kworker去连接这个服务,也就是通过这个服务我们可以在内存中找到kworker运行时派生出的key,将所有信息以二进制形式打印出来存到memdump_all.bin中,运行脚本爆破出key,有了key我们就可以得到flag了

import struct, socket
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
PCAP = "tcp.pcap"
MEM  = "memdump_all.bin"
MAG  = b"ET3RNUMX"
def parse_frames(pcap_path):
    b = open(pcap_path, "rb").read()
    if b[:4] == b"\xd4\xc3\xb2\xa1":
        endian = "<"
    elif b[:4] == b"\xa1\xb2\xc3\xd4":
        endian = ">"
    else:
        raise ValueError("unknown pcap magic")
    off = 24
    frames = []
    while off + 16 <= len(b):
        ts_sec, ts_usec, incl_len, orig_len = struct.unpack_from(endian + "IIII", b, off)
        off += 16
        pkt = b[off:off+incl_len]
        off += incl_len
        if len(pkt) < 14:
            continue
        eth_type = struct.unpack_from("!H", pkt, 12)[0]
        if eth_type != 0x0800:
            continue
        ip = pkt[14:]
        if len(ip) < 20 or ip[9] != 6:
            continue
        ihl = (ip[0] & 0x0F) * 4
        src = socket.inet_ntoa(ip[12:16])
        dst = socket.inet_ntoa(ip[16:20])
        totlen = struct.unpack_from("!H", ip, 2)[0]
        tcp = ip[ihl:totlen]
        if len(tcp) < 20:
            continue
        sport, dport = struct.unpack_from("!HH", tcp, 0)
        off_flags = struct.unpack_from("!H", tcp, 12)[0]
        doff = ((off_flags >> 12) & 0xF) * 4
        payload = tcp[doff:]
        if not payload.startswith(MAG) or len(payload) < 12:
            continue
        ln = struct.unpack(">I", payload[8:12])[0]
        blob = payload[12:12+ln]
        frames.append((src, sport, dst, dport, blob))
    return frames
def try_key(key, blobs):
    try:
        aes = AESGCM(key)
        for blob in blobs:
            aes.decrypt(blob[:12], blob[12:], None)
        return True
    except Exception:
        return False
def main():
    frames = parse_frames(PCAP)
    server = [f for f in frames if f[1] == 13337]  # sport==13337
    blobs_check = [server[0][4], server[1][4]]
    mem = open(MEM, "rb").read()
    step = 8
    for off in range(0, len(mem) - 32 + 1, step):
        k = mem[off:off+32]
        if try_key(k, blobs_check):
            print("[+] FOUND KEY (raw 32 bytes):", k)
            try:
                print("[+] as ascii:", k.decode())
            except Exception:
                pass
            return
if __name__ == "__main__":
    main()
import re, struct, socket, base64
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
PCAP = r"tcp.pcap"
MAG  = b"ET3RNUMX"
KEY  = b"xfqGcVjrOWp5tUGCPFQq448nPDjILTe7"
def parse_frames(pcap_bytes: bytes):
    if pcap_bytes[:4] == b"\xd4\xc3\xb2\xa1":
        endian = "<"
    elif pcap_bytes[:4] == b"\xa1\xb2\xc3\xd4":
        endian = ">"
    else:
        raise ValueError("unknown pcap magic")
    off = 24
    out = []
    while off + 16 <= len(pcap_bytes):
        ts_sec, ts_usec, incl_len, _ = struct.unpack_from(endian + "IIII", pcap_bytes, off)
        off += 16
        pkt = pcap_bytes[off:off+incl_len]
        off += incl_len
        if len(pkt) < 14:
            continue
        if struct.unpack_from("!H", pkt, 12)[0] != 0x0800:
            continue
        ip = pkt[14:]
        if len(ip) < 20 or ip[9] != 6:
            continue
        ihl = (ip[0] & 0x0F) * 4
        totlen = struct.unpack_from("!H", ip, 2)[0]
        tcp = ip[ihl:totlen]
        if len(tcp) < 20:
            continue
        doff = ((struct.unpack_from("!H", tcp, 12)[0] >> 12) & 0xF) * 4
        payload = tcp[doff:]
        if not payload.startswith(MAG) or len(payload) < 12:
            continue
        ln = struct.unpack(">I", payload[8:12])[0]
        blob = payload[12:12+ln]
        out.append(blob)
    return out
def main():
    frames = parse_frames(open(PCAP, "rb").read())
    aes = AESGCM(KEY)
    b32_pat = re.compile(rb"[A-Z2-7]{20,}={0,6}")
    for blob in frames:
        pt = aes.decrypt(blob[:12], blob[12:], None)
        for m in b32_pat.finditer(pt):
            s = m.group(0)
            for cand in (s, s[1:]):  
                try:
                    dec = base64.b32decode(cand)
                    if b"flag{" in dec:
                        print(dec.decode().strip())
                        return
                except Exception:
                    pass
if __name__ == "__main__":
    main()

CRYPTO

ECDSA

题目中已经告诉我们私钥是sha512(b"Welcome to this challenge!").digest(),那直接写脚本出就行了,我这边每调用ecdsa库,调用库函数可以更简单一些

import hashlib
from pathlib import Path
SIG_PATH = "signatures.txt"
N = int(
    "01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA"
    "51868783BF2F966B7FCC0148F709A5D03BB5C9B8899C47AEBB6FB71E91386409",
    16,
)
def inv(a, n): return pow(a, -1, n)
def nonce(i: int) -> int:
    return int.from_bytes(hashlib.sha512(b"bias" + bytes([i])).digest(), "big")
def parse_raw_rs(sig_hex: str):
    b = bytes.fromhex(sig_hex.strip())
    r = int.from_bytes(b[:66], "big")
    s = int.from_bytes(b[66:], "big")
    return r, s
def e_from_msg(msg: bytes) -> int:
    return int.from_bytes(hashlib.sha1(msg).digest(), "big")
lines = Path(SIG_PATH).read_text().strip().splitlines()
mhex, shex = lines[0].split(":")
msg = bytes.fromhex(mhex)
r, s = parse_raw_rs(shex)
k = nonce(0)
e = e_from_msg(msg)
d = ((s * k - e) * inv(r, N)) % N
flag = hashlib.md5(str(d).encode("ascii")).hexdigest()
print(flag)
flag{581bdf717b780c3cd8282e5a4d50f3a0}


EzFlag

本来以为是逆向题,结果放到ida里动调直接跑死了,读了一下代码发现是斐波那契数列,同时题目给上了一个sleep函数,第一种方法直接修改源文件,可以在原计数器上加上一个mod24(因为斐波那契数列mod16的周期为24),也可以写脚本(更简单一点)

from pathlib import Path
import re
b = Path("EzFlag").read_bytes()
K = re.search(rb"[0-9a-f]{16}", b).group().decode()
P = 24 
def fib_mod16(n: int) -> int:
    n %= P
    a, c = 0, 1
    for _ in range(n):
        a, c = c, (a + c) & 0xF
    return a 
v11 = 1
out = []
for i in range(32):
    out.append(K[fib_mod16(v11)])
    if i in (7, 12, 17, 22):
        out.append("-")
    v11 = (v11 * 8 + (i + 64)) % P
print( "".join(out))

flag{10632674-1d219-09f29-147a2-760632674}

RSA_NestingDoll

这个rsa还蛮有趣的,题目中给出了两个n的求值,分别为n=p*q*r*s;n1=p1*q1*r1*s1。本体突破口在于给出的平滑函数中,读题是做了相关笔记如下,由于求n的相关系数减1都会变为合数,而且合数的相关系数是p-1=p1*(2^1-2^20)所构成的一系列数,因此可以利用Pollard's p-1分解算法来求,虽然p1不是平滑数,但是p1是n1的因数,那么只需要多加个gcd即可爆破出p1的值,同理其他值也可爆破得出

import re
import secrets
from math import gcd, isqrt
E = 65537
B = 1 << 20  
def parse_bigint_from_line(line: str) -> int:
    m = re.search(r"=\s*([0-9]+)\s*$", line.strip())
    return int(m.group(1))
def int_to_bytes(x: int, min_len: int = 0) -> bytes:
    if x < 0:
        raise ValueError("negative int")
    blen = max(min_len, (x.bit_length() + 7) // 8)
    return x.to_bytes(blen, "big")
def primes_upto(n: int) -> list[int]:
    sieve = bytearray(b"\x01") * (n + 1)
    sieve[0:2] = b"\x00\x00"
    r = isqrt(n)
    for p in range(2, r + 1):
        if sieve[p]:
            start = p * p
            step = p
            sieve[start:n+1:step] = b"\x00" * (((n - start) // step) + 1)
    return [i for i in range(2, n + 1) if sieve[i]]
def lcm_1_to_B(B: int) -> int:
    ps = primes_upto(B)
    L = 1
    for p in ps:
        pk = p
        while pk * p <= B:
            pk *= p
        L *= pk
    return L
def split_with_lambda_multiple(n: int, d_odd: int, s: int, tries: int = 80) -> int | None:
    bases = [2, 3, 5, 7, 11, 13, 17]
    for _ in range(max(0, tries - len(bases))):
        bases.append(secrets.randbelow(n - 3) + 2)
    for a in bases[:tries]:
        g = gcd(a, n)
        if 1 < g < n:
            return g
        x = pow(a, d_odd, n)
        if x == 1 or x == n - 1:
            continue
        for _ in range(s):
            x_prev = x
            x = (x * x) % n
            if x == 1:
                g = gcd(x_prev - 1, n)
                if 1 < g < n:
                    return g
                break
            if x == n - 1:
                break
    return None
def is_probable_prime(n: int) -> bool:
    if n < 2:
        return False
    small_primes = [2,3,5,7,11,13,17,19,23,29,31,37]
    for p in small_primes:
        if n == p:
            return True
        if n % p == 0:
            return False
    d = n - 1
    r = 0
    while d % 2 == 0:
        d //= 2
        r += 1
    for _ in range(16):
        a = secrets.randbelow(n - 3) + 2
        x = pow(a, d, n)
        if x == 1 or x == n - 1:
            continue
        for _ in range(r - 1):
            x = (x * x) % n
            if x == n - 1:
                break
        else:
            return False
    return True
def factor_all(n: int, d_odd: int, s: int) -> list[int]:
    if n == 1:
        return []
    if is_probable_prime(n):
        return [n]
    f = split_with_lambda_multiple(n, d_odd, s, tries=120)
    return factor_all(f, d_odd, s) + factor_all(n // f, d_odd, s)
def main(path: str = "output.txt"):
    with open(path, "r", encoding="utf-8") as f:
        lines = [ln.rstrip("\n") for ln in f if ln.strip()]
    n1 = parse_bigint_from_line(lines[0])
    n  = parse_bigint_from_line(lines[1])
    c  = parse_bigint_from_line(lines[2])
    print("[*] Building L = lcm(1..2^20) ...")
    L = lcm_1_to_B(B)
    s = 20
    L_odd = L >> s
    d_odd = n1 * L_odd
    print("[*] Factoring outer n using known multiple of lambda(n) ...")
    outer_primes = sorted(factor_all(n, d_odd, s))
    print("[+] outer prime factors found:")
    for i, P in enumerate(outer_primes, 1):
        print(f"    P{i}: bits={P.bit_length()}")
    print("[*] Recovering inner primes via gcd(P-1, n1) ...")
    inner_primes = []
    for P in outer_primes:
        g = gcd(P - 1, n1)
        if g != 1:
            inner_primes.append(g)
    inner_primes = sorted(set(inner_primes))
    if len(inner_primes) != 4:
        raise RuntimeError(
            f"Expected 4 inner primes, got {len(inner_primes)}: "
            f"{[p.bit_length() for p in inner_primes]}"
        )
    print("[+] inner prime factors found:")
    for i, p in enumerate(inner_primes, 1):
        print(f"    p{i}: bits={p.bit_length()}")
    phi1 = 1
    for p in inner_primes:
        phi1 *= (p - 1)
    d_priv = pow(E, -1, phi1)
    m = pow(c, d_priv, n1)
    pt = int_to_bytes(m, min_len=(n1.bit_length() + 7) // 8)
    print("[+] decrypted bytes length:", len(pt))
    print("[+] decrypted (hex head):", pt[:32].hex())
    mflag = re.search(rb"flag\{[^}]+\}", pt)
    print("[+] FLAG:", mflag.group(0).decode("utf-8", errors="replace"))
if __name__ == "__main__":
    main(r"output.txt")
flag{fak3_r5a_0f_euler_ph1_of_RSA_040a2d35}
此作者没有提供个人介绍。
最后更新于 2026-01-11