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 osfrom elftools.elf.elffile import ELFFilefrom Crypto.Cipher import AES
SO_NAME = "libvipdecryptor.so"
# 你现在 IDA 里看到的BLOB_ADDR_OR_OFF = 0xAC10BLOB_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 = opraterc4解密
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())解密结果:5f19b83de29bd46e9e02f7f88bfb4ea2Microsoft 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 = 0x6610→CALG_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 0f47 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 65537static 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 python3import itertoolsimport hashlib
MASK = 0xFFFFFFFFR = 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 = 11prime_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后断点两次分别修改为满足约束的时间值即可

