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}
Comments NOTHING