mxymの小窝
5103 字
26 分钟
N1junior(1/2) 2026

It’s_Wizard_Time#

IDA 分析,函数名和变量名带混淆,都类似I1iIIIIIII,编写改名脚本变成Obf_xxx的形式静态分析

运行程序显示

我们需要通过“魔法学院”的教程,依次输入 5 行咒语(对应 5 种元素:Water, Fire, Earth, Wind, Ether)。如果咒语正确,程序会展示“奇妙的效果”。

ida运行会弹有反调试

看起来你作弊了!
Do not cheat!

先静态分析,定位到 main 函数,找到校验函数 check

if ( v6 == 130 )

返回值为 130,则 打印 Flag。

分析check

__int64 __fastcall check(__int64 a1, __int64 a2)
{
_QWORD v3[131]; // [rsp+20h] [rbp-60h] BYREF
unsigned int v4; // [rsp+43Ch] [rbp+3BCh]
char v5; // [rsp+443h] [rbp+3C3h]
int v6; // [rsp+444h] [rbp+3C4h]
int n; // [rsp+448h] [rbp+3C8h]
int m; // [rsp+44Ch] [rbp+3CCh]
unsigned int v9; // [rsp+450h] [rbp+3D0h]
int k; // [rsp+454h] [rbp+3D4h]
int j; // [rsp+458h] [rbp+3D8h]
int i; // [rsp+45Ch] [rbp+3DCh]
Obf_140002A14();
for ( i = 0; i <= 4; ++i )
{
if ( (unsigned __int8)Obf_140002A3D(65LL * i + a1) != 1 )
{
Obf_1400023EC(byte_14000C530);
11ll();
}
}
Obf_1400044C8(v3, 0, 1040);
for ( j = 0; j <= 4; ++j )
{
v6 = *(_DWORD *)(4LL * j + a2);
if ( v6 > 63 )
{
Obf_1400023EC(byte_14000C560);
11ll();
}
for ( k = 0; k < v6; ++k )
{
v5 = *(_BYTE *)(a1 + 65LL * j + k);
v4 = v5 - 97;
if ( v4 >= 0x1A )
{
Obf_1400023EC(byte_14000C590);
11ll();
}
1ii[64 * (__int64)j + k] = v4 + 1;
v3[26 * j + (int)v4] += 1LL << k;
}
}
v9 = 0;
for ( m = 0; m <= 4; ++m )
{
for ( n = 0; n <= 25; ++n )
{
if ( v3[26 * m + n] == I1iiIi[26 * m + n] )
++v9;
}
}
return v9;
}

最后v9返回值是v3和I1iiIi数组相同的个数,刚好130个

将字符串转换成了一个特征矩阵。

矩阵大小为 5 * 26(5 行输入,每行对应 a-z 26个字母)。

如果第 j 行的第 k 个字符是 c,则矩阵中对应的 (j, c) 位置的数值,其第 k 个二进制位会被置为 1。

1ii[64 * (__int64)j + k] = v4 + 1;
v3[26 * j + (int)v4] += 1LL << k;

编写解密脚本

def solve():
raw_bytes = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 131, 8, 128, 32,
20, 136, 160, 0, 0, 64, 21, 128, 64, 1,
4, 0, 0, 0, 0, 0, 0, 0, 8, 0,
64, 21, 64, 0, 2, 84, 0, 0, 16, 162,
42, 4, 129, 34, 0, 0, 8, 0, 0, 10,
8, 0, 80, 0, 0, 0, 0, 0, 0, 0,
0, 0, 4, 0, 0, 17, 0, 0, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0, 32, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 64, 32, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 2, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 9, 0,
16, 0, 0, 4, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 64, 0, 0, 8, 2, 16,
0, 0, 128, 20, 66, 145, 132, 160, 2, 0,
0, 0, 0, 0, 0, 0, 0, 0, 4, 0,
8, 0, 0, 2, 0, 0, 2, 9, 132, 66,
32, 65, 5, 0, 32, 0, 0, 4, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 98, 1, 32,
88, 8, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 16, 0,
32, 0, 1, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 128, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 2, 4, 16, 8,
8, 32, 1, 5, 0, 0, 0, 0, 0, 0,
0, 0, 96, 80, 0, 99, 209, 2, 24, 0,
0, 0, 0, 0, 0, 0, 32, 0, 1, 0,
0, 0, 0, 0, 0, 0, 16, 128, 128, 144,
36, 5, 0, 0, 0, 0, 1, 0, 0, 8,
64, 0, 0, 40, 0, 4, 0, 16, 132, 0,
0, 0, 0, 0, 0, 0, 0, 0, 132, 2,
42, 0, 0, 128, 2, 2, 0, 0, 0, 0,
0, 0, 0, 0, 0, 1, 4, 0, 2, 64,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
8, 0, 64, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 1, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 8,
16, 32, 160, 2, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 2, 129, 40, 132, 68, 5, 0, 0,
164, 64, 5, 3, 11, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 72, 38, 194, 88, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 16, 16, 0, 0, 16, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 24, 7, 0, 3, 56, 0, 12, 0,
0, 0, 0, 0, 0, 0, 0, 0, 3, 160,
66, 64, 130, 36, 1, 5, 0, 0, 0, 0,
0, 0, 32, 0, 0, 0, 0, 0, 0, 0,
64, 0, 128, 8, 4, 32, 65, 10, 0, 0,
0, 0, 0, 16, 0, 0, 0, 0, 36, 64,
129, 4, 4, 64, 130, 2, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 8, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 16, 48, 0, 0, 128, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 8, 128,
0, 17, 16, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 64, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]
qwords = []
for i in range(0, len(raw_bytes), 8):
chunk = raw_bytes[i:i + 8]
if len(chunk) < 8: break
val = 0
for b_idx, byte_val in enumerate(chunk):
val |= (byte_val << (b_idx * 8))
qwords.append(val)
print("--- 魔法咒语列表 ---")
spells = []
for row in range(5):
line_chars = [' '] * 65
for char_idx in range(26):
if (row * 26 + char_idx) >= len(qwords): break
mask = qwords[row * 26 + char_idx]
for pos in range(64):
if (mask >> pos) & 1:
line_chars[pos] = chr(97 + char_idx)
spell_text = "".join(line_chars).strip()
spells.append(spell_text)
print(f"Spell {row + 1}: {spell_text}")
if __name__ == "__main__":
solve()

--- 魔法咒语列表 --- Spell 1: ccjhglfcfgfcfgdgdgdgdgfcjhghjcndgfchcndgdgfcfgfcjpdehchc Spell 2: agfaphcdgldgdlltldgfapdgdghcdlgdpcdllgldgfalcdgdgdg Spell 3: eajnfccjljahchcfgjljajnfcchafccfclfacfccfcfghaljajhccdghaja Spell 4: aghlphlhglldplhghlhgdgllhhglldlghhghpdgdgdg Spell 5: cchaahrfaaaflchchcfnllchaahjgfcnfchaaafcnfcfnchlchaandehchc

最后输出的底部内容是hex,from hex转换后加上flag{}

MaybeAndroid#

题目说<check_flag.py的正确参数>但是给的是apk,猜测有自解密

搜索关键词py找到关键点,进行分析

然后看lib,把lib目录给ai大概知道怎么实现的了

发现真正发flag的点:VipDecryptor 把脚本写成 sitecustomize.py

VipDecryptor.kt :

private final native byte[] getDecryptedScript();
public final void saveDecryptedScript() {
byte[] decryptedScript = getDecryptedScript();
File file = new File(this.context.getFilesDir(), "python/lib/python3.14/site-packages");
FilesKt.writeBytes(new File(file, "sitecustomize.py"), decryptedScript);
}

关键含义:

  • libvipdecryptor.so 会解出一个 Python 脚本
  • 被保存为 sitecustomize.py
  • Python 启动时会自动 import sitecustomize ⇒ 相当于全局 hook/注入脚本

然后分析libvipdecryptor.so写ddump脚本

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
from elftools.elf.elffile import ELFFile
from Crypto.Cipher import AES
SO_NAME = "libvipdecryptor.so"
# 你现在 IDA 里看到的
BLOB_ADDR_OR_OFF = 0xAC10
BLOB_SIZE = 0x680 # 1664 bytes
# 从你给的 sub_F80 / aSitecustomizeP 推断:AES-128 key 就是这 16 字节
KEY = b"sitecustomize.py" # exactly 16 bytes
def vaddr_to_offset(elf: ELFFile, vaddr: int):
for seg in elf.iter_segments():
if seg["p_type"] != "PT_LOAD":
continue
p_vaddr = seg["p_vaddr"]
p_memsz = seg["p_memsz"]
p_off = seg["p_offset"]
if p_vaddr <= vaddr < p_vaddr + p_memsz:
return p_off + (vaddr - p_vaddr)
return None
def read_at_offset(path: str, off: int, size: int) -> bytes:
with open(path, "rb") as f:
f.seek(off)
b = f.read(size)
if len(b) != size:
raise RuntimeError(f"Short read at off=0x{off:X}: got {len(b)} want {size}")
return b
def looks_like_text(b: bytes) -> bool:
# 简单启发:ASCII/换行占比高,且包含 python 关键字
if not b:
return False
ascii_cnt = sum(1 for x in b if x in b"\t\r\n" or 32 <= x < 127)
if ascii_cnt / len(b) < 0.80:
return False
s = b.lower()
return (b"import " in s) or (b"def " in s) or (b"sys" in s) or (b"flag" in s)
def decrypt_ecb(ct: bytes) -> bytes:
return AES.new(KEY, AES.MODE_ECB).decrypt(ct)
def main():
so_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), SO_NAME)
if not os.path.exists(so_path):
raise SystemExit(f"[-] {SO_NAME} not found next to this script: {so_path}")
# 先按“vaddr”尝试映射
ct_candidates = []
with open(so_path, "rb") as f:
elf = ELFFile(f)
mapped = vaddr_to_offset(elf, BLOB_ADDR_OR_OFF)
if mapped is not None:
ct_candidates.append(("vaddr_mapped", mapped))
# 再按“直接文件偏移”也试一遍
ct_candidates.append(("file_offset_direct", BLOB_ADDR_OR_OFF))
out_dir = os.path.join(os.path.dirname(so_path), "out")
os.makedirs(out_dir, exist_ok=True)
ok = False
for tag, off in ct_candidates:
try:
ct = read_at_offset(so_path, off, BLOB_SIZE)
except Exception as e:
print(f"[-] {tag}: failed to read at 0x{off:X}: {e}")
continue
pt = decrypt_ecb(ct)
out_py = os.path.join(out_dir, f"sitecustomize_{tag}.py")
with open(out_py, "wb") as f:
f.write(pt)
head = pt[:200]
print(f"[*] {tag}: wrote {out_py}")
print("[*] head preview:")
print(head.decode("utf-8", errors="replace"))
if looks_like_text(pt):
print(f"[+] {tag}: looks like python/text ✅")
ok = True
if not ok:
print("\n[!] None of the outputs look like python yet.")
print(" If you can, run `readelf -lW libvipdecryptor.so` and give me the LOAD segments,")
print(" or tell me the .data section file offset from IDA, then we can set the exact offset.")
if __name__ == "__main__":
main()

dump得到

import builtins
class Origin:
def __init__(self):
self.open = builtins.open
origin = Origin()
class CustomSum:
def __init__(self):
self.sum = 0
def __lshift__(self, other):
if other == "😢":
self.sum += 1
return self
def __eq__(self, value):
if value == "😃":
return self.sum
return False
class Keyget:
def __init__(self):
self.key = "y0u_@re_vip_Us3r"
self.index = 0
def __lshift__(self, other):
if other == "😢":
val = ord(self.key[self.index % len(self.key)])
self.index += 1
return val
class GetEnc:
def __init__(self):
self.enc_data = bytes.fromhex("738d9ea5a7c5824836d63c872324e36936c1dd7026b2df418268066a936256a7")
self.index = 0
def __lshift__(self, other):
if other == "😢":
val = self.enc_data[self.index % len(self.enc_data)]
self.index += 1
return val ^ 0x55
class oprate:
def __init__(self,file,mode,*args,**kwargs):
if file == "😇" and mode == "r":
return
try :
origin.open(file = file, mode = mode, *args, **kwargs)
except Exception as e:
print(e)
def __xor__(self, other):
if other == "😋":
return CustomSum()
elif other == "🫨":
return list(range(256))
elif other == "😁":
return Keyget()
elif other == "😤":
return GetEnc()
return self
builtins.open = oprate

rc4解密

import binascii
def rc4_decrypt(key, ciphertext):
# 1. KSA (Key Scheduling Algorithm) - 初始化 S 盒
S = list(range(256))
j = 0
for i in range(256):
j = (j + S[i] + key[i % len(key)]) % 256
S[i], S[j] = S[j], S[i]
# 2. PRGA (Pseudo-Random Generation Algorithm) - 生成密钥流并解密
i = 0
j = 0
plaintext = []
for char in ciphertext:
i = (i + 1) % 256
j = (j + S[i]) % 256
S[i], S[j] = S[j], S[i]
K = S[(S[i] + S[j]) % 256]
plaintext.append(char ^ K)
return bytes(plaintext)
# ================= 提取数据 =================
# 1. 密钥 (来自 Keyget 类)
key_str = "y0u_@re_vip_Us3r"
key_bytes = key_str.encode('utf-8')
# 2. 原始 Enc 数据 (来自 GetEnc 类)
enc_hex = "738d9ea5a7c5824836d63c872324e36936c1dd7026b2df418268066a936256a7"
enc_raw = binascii.unhexlify(enc_hex)
# 3. 预处理密文 (GetEnc.__lshift__ 中有个 ^ 0x55 操作)
# 这才是真正的密文
real_ciphertext = bytes([b ^ 0x55 for b in enc_raw])
# ================= 执行解密 =================
flag_bytes = rc4_decrypt(key_bytes, real_ciphertext)
try:
print("解密结果:")
print(flag_bytes.decode('utf-8'))
except:
print("Hex 结果:", flag_bytes.hex())
解密结果:
5f19b83de29bd46e9e02f7f88bfb4ea2

Microsoft VS Code#

开始直接提取数据进行aes解密无法得到有效内容

有比较难绕过的反调试(反调试也在这个构造函数,用isbeingdebug实现)

汇编没有额外内容,看到main函数前的构造函数区域

02D358 First dq 0 ; DATA XREF: __scrt_common_main_seh(void)+75↑o
.rdata:000000014002D360 dq offset ?pre_cpp_initialization@@YAXXZ ; pre_cpp_initialization(void)
.rdata:000000014002D368 dq offset sub_140001000
.rdata:000000014002D370 dq offset sub_1400010E0
.rdata:000000014002D378 dq offset ??__Efin@std@@YAXXZ ; std::`dynamic initializer for 'fin''(void)
.rdata:000000014002D380 dq offset sub_140001038
.rdata:000000014002D388 dq offset sub_1400010B8
.rdata:000000014002D390 dq offset sub_1400011C8
.rdata:000000014002D398 dq offset ??__Efout@std@@YAXXZ ; std::`dynamic initializer for 'fout''(void)
.rdata:000000014002D3A0 dq offset sub_140001100
.rdata:000000014002D3A8 dq offset sub_140001180
.rdata:000000014002D3B0 dq offset sub_1400012A0
.rdata:000000014002D3B8 dq offset sub_140001218
.rdata:000000014002D3C0 dq offset sub_1400011E8
.rdata:000000014002D3C8 dq offset sub_140001268
.rdata:000000014002D3D0 dq offset sub_1400012C0
.rdata:000000014002D3D8 dq offset sub_1400012E0
.rdata:000000014002D3E0 dq offset sub_14000102C
.rdata:000000014002D3E8 dq offset ??__Eclassic_locale@std@@YAXXZ ; std::`dynamic initializer for 'classic_locale''(void)
.rdata:000000014002D3F0 dq offset sub_140008150
.rdata:000000014002D3F8 ; const _PVFV Last
.rdata:000000014002D3F8 Last dq 0

看到sub_140008150,

经过分析实际操作为:

table[0x10..0x1f] 这 16 个字节 异或 一个 16 字节 mask。

也就是:

57 00 69 00 6e 00 64 00 6f 00 77 00 73 00 20 00

于是运行时真正的 table[0x10..0x1f] 变成:

原始 10 11 12 … 1f XOR mask ⇒

47 11 7b 13 7a 15 72 17 77 19 6d 1b 6f 1d 3e 1f

初始化 XOR 后

ALG_ID = 0x6610CALG_AES_256(AES-256)

KeySize = 32

IV = ff ee dd cc bb aa 99 88 77 66 55 44 33 22 11 00(16字节)

最终 AES Key(32字节)

Key 是直接取 table[0x00..0x1f](前 16 字节没改,后 16 字节是 XOR 后的):

00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f
47 11 7b 13 7a 15 72 17 77 19 6d 1b 6f 1d 3e 1f

合并成 hex:

000102030405060708090a0b0c0d0e0f47117b137a15721777196d1b6f1d3e1f

用 AES-256-CBC(PKCS7 padding)解密 48 字节密文,得到明文:

M1cr0SOf7_V5_C0dE,d0_Y0U_Kn0W??\x00

Interstellar#

搜索字符串xref然后逐个函数进行分析

明文打包方式:48 字节 → 12 个 uint32

LODWORD(v28) = (((((v33 << 8) + v32) << 8) + v31) << 8) + v30;

等价b0+(b1<<8)+(b2<<16)+(b3<<24)

程序读入 48 字节,按 小端序每 4 字节打包成一个 32 位数:

打印时把 uint32 当作 int32 输出,所以会看到负数。


然后接着有两次 shuffle(只改变顺序)

1. 第一次 Shuffle (加密前)
对应代码中的 v42 循环:
v26 = 82;
v42 = 0;
for ( i = 1; ; v42 += i )
{
v26 = 82;
v7 = i < 0 ? -1 : i > 0;
if ( (v42 - 0x10000) * v7 > 0 ) // 循环 0x10001 次
break;
global_dsa = (dsa *)arg2;
sub_4042BF(); // 执行具体的 Swap 逻辑
}
位置: 在读取完明文并打包(v41 循环)之后,核心加密(v43 循环)之前。
功能: 调用 sub_4042BF()。这个函数内部会利用 LCG 生成随机索引,并对 12 个 uint32 进行位置交换(Swap)。
2. 第二次 Shuffle (加密后)
对应代码中的 v44 循环:
v26 = 95;
v44 = 0;
for ( i = 1; ; v44 += i )
{
v26 = 95;
v9 = i < 0 ? -1 : i > 0;
if ( (v44 - 0x10000) * v9 > 0 ) // 循环 0x10001 次
break;
global_dsa = (dsa *)arg2;
sub_404986(); // 再次执行具体的 Swap 逻辑
}
位置: 在核心混淆加密(v43 循环)执行完毕后,最终打印结果(v45 循环)之前。
功能: 调用 sub_404986()。这会对已经加密混淆过的密文块进行最后一次乱序。

程序对 12 个 word 做 两次 shuffle(每次 0x10001 = 65537 次 swap),随机数来自 time() 种子 + 一个 LCG。

总之最后是在存储 12 个 word 的数组中进行位置交换。


LCG加密

数组按顺序分成 4 组,每组三个 word:

  • (w0,w1,w2), (w3,w4,w5), (w6,w7,w8), (w9,w10,w11)

每组执行 65537 轮。每轮先更新常量:

a = a*b;
b = b*c;
c = c*a;

对应在这里

v0[2].file = (char *)stack_top;
*(_QWORD *)&v0[2].line = stack_top;
v0->line = 47;
v0->line = 48;
*(&v0[4].line + 1) *= LODWORD(v0[4].parent);
v0->line = 49;
LODWORD(v0[4].parent) *= HIDWORD(v0[4].parent);
v0->line = 50;
HIDWORD(v0[4].parent) *= *(&v0[4].line + 1);
v0->line = 51;
global_dsa = v0;
arg = make_arg(sub_4022CF, v0);
arg1 = arg.arg1;

初始:

a0=0x72bc3ef1, b0=0x179f2bcd, c0=0x5f78237f

三个值对应在这里

_QWORD *__fastcall sub_4038D9(
_QWORD *a1,
__int64 a2,
__int64 a3,
__int64 a4,
__int64 a5,
__int64 a6,
__int64 a7,
__int64 a8)
{
...
...
v32 = 0x72BC3EF1;
v33 = 0x179F2BCD;
v34 = 0x5F78237F;
v14 = 46;
v31 = 0;
for ( i = 1; ; v31 += i )
{
v14 = 46;
v10 = i < 0 ? -1 : i > 0;
if ( (v31 - 0x10000) * v10 > 0 )
break;
global_dsa = (dsa *)v13;
((void (__fastcall *)(_QWORD *, __int64))sub_403008)(a1, a3);
}
v14 = 71;
pop_stack(v21);
pop_stack(v19);
pop_stack(v17);
active_dsa = v15;
v11 = v23;
*a1 = 0;
a1[1] = v11;
a1[2] = v24;
return a1;
}

然后依次更新 x、y、z(全程 uint32 溢出截断),并且除法是 int32 的“向 0 截断”(C 语义)。

sdiv(v,d)((int32_t)v)/d(向 0 截断)。

每轮的三段增量函数(写成伪代码):

三段增量函数分步在

a = a*b;
b = b*c;
c = c*a;

计算完后面的函数里

x += ((y*0x275bcf2a + sdiv(z,29)) ^ (y*0x4328ffbc + sdiv(z,13)))
^ ((z-a) ^ (y+a));
y += ((z*0x5129bdff + sdiv(x,19)) ^ (z*0x27b3adb3 + sdiv(x,11)))
^ ((x-b) ^ (z+b));
z += ((x*0x623f299a + sdiv(y,23)) ^ (x*0x183fc211 + sdiv(y,17)))
^ ((y-c) ^ (x+c));

根据前面的分析写解密脚本

加密轮顺序是:先更新常量,然后 x → y → z 依次加上增量。

因此解密时倒序还原:

单轮逆变换:

z -= ...
y -= ...
x -= ...

逆混淆后会得到 12 个 4 字符块,但由于第一次 shuffle,块顺序被打乱。


分组枚举脚本(C,跑得快)

排列组合可以发现可能性并不多

C(12,3)*6 = 1320

枚举 1320 个有序三元组,跑逆变换,打印所有解出 12 字节全可打印的结果

#include <stdint.h>
#include <stdio.h>
static inline uint32_t u32(uint64_t x){ return (uint32_t)x; }
static inline int32_t i32(uint32_t x){ return (int32_t)x; }
static inline uint32_t F(uint32_t y, uint32_t z, uint32_t a){
uint32_t p = u32((uint64_t)y * 0x275bcf2aULL) + (uint32_t)(i32(z)/29);
p ^= u32((uint64_t)y * 0x4328ffbcULL) + (uint32_t)(i32(z)/13);
uint32_t q = (z - a) ^ (y + a);
return (p ^ q);
}
static inline uint32_t G(uint32_t z, uint32_t x, uint32_t b){
uint32_t p = u32((uint64_t)z * 0x5129bdffULL) + (uint32_t)(i32(x)/19);
p ^= u32((uint64_t)z * 0x27b3adb3ULL) + (uint32_t)(i32(x)/11);
uint32_t q = (x - b) ^ (z + b);
return (p ^ q);
}
static inline uint32_t H(uint32_t x, uint32_t y, uint32_t c){
uint32_t p = u32((uint64_t)x * 0x623f299aULL) + (uint32_t)(i32(y)/23);
p ^= u32((uint64_t)x * 0x183fc211ULL) + (uint32_t)(i32(y)/17);
uint32_t q = (y - c) ^ (x + c);
return (p ^ q);
}
#define R 65537
static uint32_t A[R], B[R], C[R];
static void precompute_constants(){
uint32_t a=0x72bc3ef1, b=0x179f2bcd, c=0x5f78237f;
for(int r=0;r<R;r++){
a = u32((uint64_t)a*b);
b = u32((uint64_t)b*c);
c = u32((uint64_t)c*a);
A[r]=a; B[r]=b; C[r]=c;
}
}
static void decrypt_triple(uint32_t *x, uint32_t *y, uint32_t *z){
uint32_t X=*x, Y=*y, Z=*z;
for(int r=R-1;r>=0;r--){
uint32_t a=A[r], b=B[r], c=C[r];
Z -= H(X,Y,c);
Y -= G(Z,X,b);
X -= F(Y,Z,a);
}
*x=X; *y=Y; *z=Z;
}
static void words_to_bytes(uint32_t x,uint32_t y,uint32_t z,uint8_t out[12]){
uint32_t w[3]={x,y,z};
for(int i=0;i<3;i++){
out[i*4+0] = w[i] & 0xff;
out[i*4+1] = (w[i]>>8) & 0xff;
out[i*4+2] = (w[i]>>16) & 0xff;
out[i*4+3] = (w[i]>>24) & 0xff;
}
}
static int printable12(const uint8_t *bs){
for(int i=0;i<12;i++){
if(bs[i] < 0x20 || bs[i] > 0x7e) return 0;
}
return 1;
}
int main(){
precompute_constants();
int32_t cipher_s[12]={
961328601, 940839751, 705572502, -1296360751, 1510741316, -746016568,
1758503669, -312360224, 1117958716, -1263168281, -1534151026, -1968871781
};
uint32_t cipher[12];
for(int i=0;i<12;i++) cipher[i]=(uint32_t)cipher_s[i];
for(int i=0;i<12;i++)
for(int j=i+1;j<12;j++)
for(int k=j+1;k<12;k++){
int ids[3]={i,j,k};
int perms[6][3]={{0,1,2},{0,2,1},{1,0,2},{1,2,0},{2,0,1},{2,1,0}};
for(int p=0;p<6;p++){
uint32_t x=cipher[ids[perms[p][0]]];
uint32_t y=cipher[ids[perms[p][1]]];
uint32_t z=cipher[ids[perms[p][2]]];
decrypt_triple(&x,&y,&z);
uint8_t bs[12];
words_to_bytes(x,y,z,bs);
if(printable12(bs)){
printf("CIPH (%d,%d,%d) -> PLAIN \"%.*s\"\n",
(int32_t)cipher[ids[perms[p][0]]],
(int32_t)cipher[ids[perms[p][1]]],
(int32_t)cipher[ids[perms[p][2]]],
12, bs);
}
}
}
}

得到 4 组:

  • CIPH (-746016568,961328601,-1534151026) -> PLAIN “h3_L1gH7K1nG” CIPH (1117958716,-1263168281,940839751) -> PLAIN “a1ig{4wa1LTh” CIPH (1510741316,705572502,-1968871781) -> PLAIN “4r5_NeD}_1nT” CIPH (1758503669,-312360224,-1296360751) -> PLAIN “E_s7flag_0fA”


    Process exited after 1.627 seconds with return value 0

flag {4wa1 NeD} 是可以确定的

题目给了md5的值,每4个分组拼接然后计算md5是否匹配即可

保存为 solve.py

#!/usr/bin/env python3
import itertools
import hashlib
MASK = 0xFFFFFFFF
R = 65537
def u32(x: int) -> int:
return x & MASK
def i32(x: int) -> int:
# x is uint32
return x if x < 0x80000000 else x - 0x100000000
def sdiv32(u: int, d: int) -> int:
"""C int32 division, trunc toward zero."""
v = i32(u)
if v >= 0:
return v // d
return -((-v) // d)
# precompute a,b,c for each round (values used inside the round)
def precompute_abc():
a, b, c = 0x72bc3ef1, 0x179f2bcd, 0x5f78237f
A = [0] * R
B = [0] * R
C = [0] * R
for r in range(R):
a = (a * b) & MASK
b = (b * c) & MASK
c = (c * a) & MASK
A[r], B[r], C[r] = a, b, c
return A, B, C
A, B, C = precompute_abc()
def F(y: int, z: int, a: int) -> int:
p = (((y * 0x275bcf2a) & MASK) + sdiv32(z, 29)) & MASK
p ^= ((((y * 0x4328ffbc) & MASK) + sdiv32(z, 13)) & MASK)
q = ((z - a) & MASK) ^ ((y + a) & MASK)
return (p ^ q) & MASK
def G(z: int, x: int, b: int) -> int:
p = (((z * 0x5129bdff) & MASK) + sdiv32(x, 19)) & MASK
p ^= ((((z * 0x27b3adb3) & MASK) + sdiv32(x, 11)) & MASK)
q = ((x - b) & MASK) ^ ((z + b) & MASK)
return (p ^ q) & MASK
def H(x: int, y: int, c: int) -> int:
p = (((x * 0x623f299a) & MASK) + sdiv32(y, 23)) & MASK
p ^= ((((x * 0x183fc211) & MASK) + sdiv32(y, 17)) & MASK)
q = ((y - c) & MASK) ^ ((x + c) & MASK)
return (p ^ q) & MASK
def decrypt_triple(tri):
x, y, z = (u32(tri[0]), u32(tri[1]), u32(tri[2]))
for r in range(R - 1, -1, -1):
a, b, c = A[r], B[r], C[r]
# reverse order: undo z, then y, then x
z = (z - H(x, y, c)) & MASK
y = (y - G(z, x, b)) & MASK
x = (x - F(y, z, a)) & MASK
return x, y, z
def words_to_ascii(words):
bs = bytearray()
for w in words:
bs += bytes((w & 0xff, (w >> 8) & 0xff, (w >> 16) & 0xff, (w >> 24) & 0xff))
return bs.decode("ascii")
def main():
# 这 4 个三元组来自 find_groups.c 的输出
cipher_groups = [
(-746016568, 961328601, -1534151026), # -> "h3_L1gH7K1nG"
(1117958716, -1263168281, 940839751), # -> "a1ig{4wa1LTh"
(1510741316, 705572502, -1968871781), # -> "4r5_NeD}_1nT"
(1758503669, -312360224, -1296360751), # -> "E_s7flag_0fA"
]
blocks = []
for g in cipher_groups:
x, y, z = decrypt_triple(g)
s = words_to_ascii([x, y, z]) # 12 chars
blocks += [s[i:i+4] for i in range(0, 12, 4)]
target = "2887941ed3ea0f6b7ca870cd11f0de13"
# 用 flag 格式 + md5 还原 12 块的顺序
b_flag = "flag"
b_open = next(b for b in blocks if b.startswith("{"))
b_close = next(b for b in blocks if b.endswith("}"))
remain = blocks[:]
for b in (b_flag, b_open, b_close):
remain.remove(b)
for perm in itertools.permutations(remain):
cand = b_flag + b_open + "".join(perm) + b_close
if hashlib.md5(cand.encode()).hexdigest() == target:
print(cand)
return
print("not found")
if __name__ == "__main__":
main()
flag{4waK1nG_1nTh3_L1gH7_0fA1LThE_s74r5_a1igNeD}

Find My Time#

程序运行会每秒显示乱码,且乱码内容与时间无关

分析发现题目专门对时间戳有约束

1.不能有0

2.查素数,从年到秒,组合起来,时间戳这些都是素数则满足条件

import sympy as sp
def days_from_civil(y, m, d):
y_adj = y - 1 if m <= 2 else y
era = (y_adj if y_adj >= 0 else y_adj - 399) // 400
yoe = y_adj - era * 400
mp = m + 9 if m <= 2 else m - 3
doy = (153 * mp + 2)//5 + d - 1
doe = yoe*365 + yoe//4 - yoe//100 + doy
return era*146097 + doe - 719468
def unix_seconds(y,m,d,h,mi,s):
return days_from_civil(y,m,d)*86400 + h*3600 + mi*60 + s
prime_month = 11
prime_days = [11,13,17,19,23,29]
prime_hours = [11,13,17,19,23]
prime_2to59 = [p for p in range(2,60) if sp.isprime(p)]
prime_ms = [p for p in prime_2to59 if p>=10 and '0' not in str(p)]
def search(ymin=1970, ymax=9999):
sols = []
years = [y for y in range(ymin, ymax+1) if '0' not in str(y) and sp.isprime(y)]
for y in years:
for d in prime_days:
date_int = y*10000 + prime_month*100 + d
if not sp.isprime(date_int):
continue
for h in prime_hours:
for mi in prime_ms:
for s in prime_ms:
time_int = h*10000 + mi*100 + s
if not sp.isprime(time_int):
continue
ts_str = f"{y}{prime_month:02d}{d:02d}{h:02d}{mi:02d}{s:02d}"
if '0' in ts_str:
continue
if not sp.isprime(int(ts_str)):
continue
ut = unix_seconds(y,prime_month,d,h,mi,s)
if ut <= 1 or not sp.isprime(ut):
continue
ustr = str(ut)
if not sp.isprime(int(ustr + ts_str)):
continue
if not sp.isprime(int(ts_str + ustr)):
continue
sols.append((y,prime_month,d,h,mi,s,ut,ts_str))
return sols
print(search(1970, 9999))
[(3583, 11, 11, 11, 23, 31, 50928521011, '35831111112331'), (9923, 11, 13, 23, 29, 19, 250999774159, '99231113232919')]

3583-11-11 11:23:31 UTC (epoch=50928521011)

9923-11-13 23:29:19 UTC (epoch=250999774159)

epoch=50928521011 → FILETIME 0x08AF0A121B35FB80

epoch=250999774159 → FILETIME 0x2472FF8493BFB180

动调在GetSystemTimeAsFileTime后断点两次分别修改为满足约束的时间值即可

image-20260126123111234

image-20260126123149974

N1junior(1/2) 2026
https://mxym.github.io/posts/n1j2026/
作者
mxym
发布时间
2026-02-13
许可协议
CC BY-NC-SA 4.0