本文最后更新于 2025年11月1日 凌晨
低空经济网络安全 The Hidden Link 1 2 我们截获了一个名为“MAV1”的恐怖组织发出的无人机遥测下行数据。`capture.pcap`中的数据流量看起来很正常,是吗? We've intercepted a drone' s telemetry downlink from a terrorist group called "MAV1". The traffic in `capture.pcap` appears to be normal, or is it?
访问几个数据包就会发现有疑似 flag 的字符
用 strings 提取字符
1 2 3 4 5 6 7 8 9 Bk3d} Bll3r BflagJ B{dr0 Bt_c0: Bntr0 B_h4c! Bn3_f Bl1gh
去掉多余字母和标点符号,拼好 flag
1 flag{dr0 n3 _fl1 ght_c0 ntr0 ll3 r_h4 ck3 d}
Mobile EZMiniAPP 未加密,可以直接用 wxappUnpacker 反编译
在 chunk_0.appservice.js 文件中找到主函数,customEncrypt 是加密函数,有校验
密文 : [1, 33, 194, 133, 195, 102, 232, 104, 200, 14, 8, 163, 131, 71, 68, 97, 2, 76, 72, 171, 74, 106, 225, 1, 65]
密钥 Key: “newKey2025!”
要找出输入字符串 a,使得 customEncrypt(a, "newKey2025!") 结果等于上面的密文数组,customEncrypt 直接调用了真正的加密函数 enigmaticTransformation
这里对密钥进行处理
进行异或,用当前字符索引 o 对 3 取模的结果,采用不同的 XOR 规则
按照值 c 不同对异或的结果 u 进行不同的移位
脚本如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 import json TARGET_CIPHER = [ 1 , 33 , 194 , 133 , 195 , 102 , 232 , 104 , 200 , 14 , 8 , 163 , 131 , 71 , 68 , 97 , 2 , 76 , 72 , 171 , 74 , 106 , 225 , 1 , 65 ] KEY_STR = "newKey2025!" def get_key_array (t_str ): return [ord (c) for c in t_str]def calculate_rotation_c (i_arr ): t = 0 for e, val in enumerate (i_arr): if e % 4 == 0 : t += 1 * val elif e % 4 == 1 : t += val + 0 elif e % 4 == 2 : t += 0 | val elif e % 4 == 3 : t += 0 ^ val return t % 8 def circular_right_shift (value, shift ): value &= 0xFF return ((value >> shift) | (value << (8 - shift))) & 0xFF def enigmatic_decryption (cipher_arr, key_str ): key_arr = get_key_array(key_str) s = len (key_arr) c = calculate_rotation_c(key_arr) decrypted_bytes = [] for o, h in enumerate (cipher_arr): u = circular_right_shift(h, c) key_stream_val = key_arr[o % s] original_char_code = u ^ key_stream_val decrypted_bytes.append(original_char_code) try : flag = "" .join([chr (b) for b in decrypted_bytes]) return flag except ValueError: return "无法解码为有效字符串,可能解密错误" FLAG = enigmatic_decryption(TARGET_CIPHER, KEY_STR)print (f"目标密文 (Target Cipher): {TARGET_CIPHER} " )print (f"使用的密钥 (Key): '{KEY_STR} '" )print ("-" * 30 )print (f"计算出的旋转量 (c): {calculate_rotation_c(get_key_array(KEY_STR))} " )print (f"解密结果 (Flag): {FLAG} " )
得到 flag
1 flag {JustEasyMiniProgram}
Just il2cpp 在安卓上的加密应用
查看源码,发现在程序启动时会加载 libjust.so 文件
解包查看 libjust.so 文件,根据字符串定位到 sub_8288 函数,发现 hook 了dlopen
sub_8A0C 函数是校验函数,跟进分析,一个 RC4 的加密,异或 0x33
密钥为 “nihaounity”
创建一个名为 “dec_il2cpp”的新文件,将解密后的内容写入此文件
加载 il2cpp.so 文件,发现加载不出来,所以可能需要解密这个 il2cpp.so
根据加密逻辑和密钥用脚本解密 so 文件,脚本如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 import osimport sysdef decrypt_rc4_variant (data: bytes , key: str = "nihaounity" , xor_constant: int = 0x33 ) -> bytes : key_bytes = key.encode('utf-8' ) key_len = len (key_bytes) data_len = len (data) s_box = list (range (256 )) j = 0 for i in range (256 ): j = (j + s_box[i] + key_bytes[i % key_len]) % 256 s_box[i], s_box[j] = s_box[j], s_box[i] i = 0 j = 0 decrypted_data = bytearray (data) for k in range (data_len): i = (i + 1 ) % 256 j = (j + s_box[i]) % 256 s_box[i], s_box[j] = s_box[j], s_box[i] t = (s_box[i] + s_box[j]) % 256 keystream_byte = s_box[t] decrypted_data[k] = data[k] ^ keystream_byte ^ xor_constant return bytes (decrypted_data)def main (): input_path = sys.argv[1 ] output_path = sys.argv[2 ] KEY = "nihaounity" XOR_CONST = 0x33 try : with open (input_path, 'rb' ) as f: encrypted_data = f.read() print (f"[*] 正在读取文件: {input_path} ({len (encrypted_data)} 字节)" ) print (f"[*] 解密密钥: '{KEY} ', 异或常数: 0x{XOR_CONST:02x} " ) decrypted_data = decrypt_rc4_variant(encrypted_data, KEY, XOR_CONST) with open (output_path, 'wb' ) as f: f.write(decrypted_data) print (f"[+] 解密完成,文件已保存至: {output_path} " ) except FileNotFoundError: print (f"[!] 错误: 找不到文件 {input_path} " ) except Exception as e: print (f"[!] 发生错误: {e} " )if __name__ == "__main__" : main()
加载解密出来的 decil2cpp.so,文件很大,加载很慢,根据字符串的 global-metadata.dat 定位过去
定位到 sub_211B8C 函数,发现这里在定位并加载元数据文件,调用了 sub_211D94 函数加载
跟进
加载数据的地方,继续跟进
来到 sub_21A2C8,这是一个解密函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 char *__fastcall sub_21A2C8 (unsigned __int16 *a1, __int64 a2) { __int64 v2; __int64 v4; __int64 v5; char *v6; __int64 i; __int64 v8; __int64 v9; v2 = a1[512 ]; v4 = a2 - 4 * v2; v5 = v4 - 1028 ; v6 = (char *)malloc (v4 - 4 ); memcpy (v6, a1, 0x400 u); if ( v5 >= 1 ) { for ( i = 0 ; i < v5; i += 4 ) { v8 = i + 3 ; v9 = i + i / v2; if ( i >= 0 ) v8 = i; *(_DWORD *)&v6[(v8 & 0xFFFFFFFFFFFFFFFC LL) + 1024 ] = *(_DWORD *)((char *)&a1[2 * v2 + 514 ] + (v8 & 0xFFFFFFFFFFFFFFFC LL)) ^ *(_DWORD *)&a1[2 * (v9 % v2) + 514 ]; } } return v6; }
根据逻辑写解密脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 import structimport sysimport osdef decrypt_metadata_custom_xor (encrypted_data: bytes ) -> bytes : data_len = len (encrypted_data) v2 = struct.unpack('<H' , encrypted_data[1024 :1026 ])[0 ] v5 = data_len - (4 * v2) - 1028 decrypted_data = bytearray (encrypted_data) encrypted_data_base_offset = 4 * v2 + 1028 keystream_base_offset = 1028 i = 0 while i < v5: v9 = i + i // v2 keystream_index = v9 % v2 keystream_offset = keystream_base_offset + keystream_index * 4 encrypted_offset = encrypted_data_base_offset + i decrypted_offset = 1024 + i keystream_block = struct.unpack('<I' , encrypted_data[keystream_offset:keystream_offset+4 ])[0 ] encrypted_block = struct.unpack('<I' , encrypted_data[encrypted_offset:encrypted_offset+4 ])[0 ] decrypted_block = encrypted_block ^ keystream_block struct.pack_into('<I' , decrypted_data, decrypted_offset, decrypted_block) i += 4 print (f"[+] 解密完成。共处理 {i} 字节数据。" ) return bytes (decrypted_data) expected_output_len = data_len - (4 * v2) - 4 if expected_output_len < len (decrypted_data): decrypted_data = decrypted_data[:expected_output_len] print (f"[+] 解密完成。共处理 {i} 字节数据。最终输出长度: {len (decrypted_data)} 字节" ) return bytes (decrypted_data)def main (): input_path = sys.argv[1 ] output_path = sys.argv[2 ] with open (input_path, 'rb' ) as f: encrypted_data = f.read() print (f"[*] 正在读取文件: {input_path} ({len (encrypted_data)} 字节)" ) decrypted_data = decrypt_metadata_custom_xor(encrypted_data) with open (output_path, 'wb' ) as f: f.write(decrypted_data) print (f"[+] 解密完成,文件已保存至: {output_path} " )if __name__ == "__main__" : main()
解密后的 dat 文件用 Il2CppDumper 直接解包
进入 DummyDll 目录,查看 Assembly-CSharp.dll,可以看到 Check 函数和加密函数
so 附加 Il2CppDumper 自动生成的 ida_py3.py 脚本和 script.json 文件恢复符号表,找 Flagchecker 看逻辑
CheckFlag 函数中有加密,两段不一样的,前面对前 8 个字节进行 tea 加密,v78 是 40 字节的密文
后一段是对剩下的 32 字节处理,每 8 字节还与前一个 8 字节进行异或
观察 tea 函数,密文存在 a2 指向的内存位置
密文和密钥我没有环境,不好调试,在 cctor 中有初始化类的静态字段,初始化了密钥和密文,可以从这 Filed 去找
查看 dump.cs,搜索 PrivateImplementationDetails
1 2 3 4 5 6 internal sealed class < PrivateImplementationDetails> { internal static readonly <PrivateImplementationDetails>.__StaticArrayInitTypeSize=40 29F C2CC7503351583297C73AC477A5D7AA78899F3C0D66E1F909139D4AA1FFB2 ; internal static readonly <PrivateImplementationDetails>.__StaticArrayInitTypeSize=16 C8E4E9E3F34C25560172B0D40B6DF4823260AA87EC6866054AA4691711E5D7BF ; }
这里找到了偏移 0xF901D 和 0xF904,直接按结构偏移读 global-data.dat 的数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import sys, binasciifrom pathlib import Path META_PATH = "g-d.dat" DEFAULTS = { "Ciphertext_40" : {"offset" : 0xF901D , "size" : 40 }, "XTEAKey_16" : {"offset" : 0xF9045 , "size" : 16 }, }def main (): meta = Path(META_PATH) if not meta.is_file(): print (f"metadata file not found: {META_PATH} " ) sys.exit(1 ) data = meta.read_bytes() cipher = key = None for name, info in DEFAULTS.items(): off = info["offset" ] size = info["size" ] if off >= len (data): continue blob = data[off:off + size] if "Ciphertext" in name: cipher = blob elif "Key" in name: key = blob if cipher: print ("Ciphertext HEX:" , binascii.hexlify(cipher).decode().upper()) if key: print ("Key HEX :" , binascii.hexlify(key).decode().upper())if __name__ == "__main__" : main()
拿到密文和密钥
1 2 Ciphertext HEX: AF5864409DB92167AEB529049E86C543230FBFA6B2AE4AB5C569B7A803D1AECFC62C5B7FA2861E1A Key HEX : 78563412121110091615141318171615
解密逻辑:最开始对最前 8 个字节进行 tea 解密,随后的 32 字节每 8 字节要与前一个 8 字节进行异或,从后往前循环
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 import structimport binascii ROUNDS = 16 DELTA = 0x61C88647 MASK = 0xFFFFFFFF KEY_BYTES = binascii.unhexlify("78563412121110091615141318171615" ) KEY_WORDS = struct.unpack('<IIII' , KEY_BYTES) CIPHERTEXT_HEX = "AF5864409DB92167AEB529049E86C543230FBFA6B2AE4AB5C569B7A803D1AECFC62C5B7FA2861E1A" CIPHERTEXT_BYTES = bytearray (binascii.unhexlify(CIPHERTEXT_HEX))def decrypt_block (v0, v1, key_words ): k0, k1, k2, k3 = key_words s = (0 - DELTA * ROUNDS) & MASK for _ in range (ROUNDS): s = (s + DELTA) & MASK v1 = (v1 - (((v0 << 4 ) + k2) ^ (v0 + s) ^ ((v0 >> 5 ) + k3))) & MASK v0 = (v0 - (((v1 << 4 ) + k0) ^ (v1 + s) ^ ((v1 >> 5 ) + k1))) & MASK return v0, v1def decrypt_data (cipher_bytes, key_words ): for i in range (8 , 0 , -2 ): c0, c1 = struct.unpack('<II' , cipher_bytes[0 :8 ]) offset = i * 4 ci, cj = struct.unpack('<II' , cipher_bytes[offset:offset+8 ]) pi = ci ^ c0 pj = cj ^ c1 cipher_bytes[offset:offset+8 ] = struct.pack('<II' , pi, pj) dv0, dv1 = decrypt_block(c0, c1, key_words) cipher_bytes[0 :8 ] = struct.pack('<II' , dv0, dv1) f0, f1 = struct.unpack('<II' , cipher_bytes[0 :8 ]) rf0, rf1 = decrypt_block(f0, f1, key_words) cipher_bytes[0 :8 ] = struct.pack('<II' , rf0, rf1) return cipher_bytesdef run_decryption (): result = decrypt_data(CIPHERTEXT_BYTES, KEY_WORDS) flag = result.rstrip(b'\x00' ).decode('ascii' , errors='ignore' ) print ("Decrypted HEX:" , binascii.hexlify(result).decode().upper()) print ("Decrypted ASCII:" , flag)if __name__ == "__main__" : run_decryption()
得到 flag
1 flag{unitygame_I5S0ooFunny_Isnotit??? ?? }
Reverse hyperjump vm 类型的题目
根进函数可以看到调度器,典型的 VM switch-case 结构
跟进 case 里跳转的函数可以看到这是 VM 指令处理器,有一些 VM 指令
分析可知为单字节加密,输入长度为 24,这里的 v24 是字符串输入的索引
可以进行侧信道爆破,用 Pintools,并且题目还给了 md5 值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 import reimport stringimport sysimport osimport subprocessfrom pwn import * TARGET_BINARY = os.path.abspath('./hyperjump' ) FLAG_LEN = 24 START_INDEX = 0 PIN_ROOT = '/root/pin' PIN_TOOL_PATH = os.path.abspath('./pin/source/tools/ManualExamples/obj-intel64/inscount0.so' ) PIN_CMD_LIST = [os.path.join(PIN_ROOT, 'pin' ), '-t' , PIN_TOOL_PATH, '--' , TARGET_BINARY] CHARSET = string.ascii_lowercase + string.digits + "_@{}" context.log_level = 'info' def read_inscount_outfile (workdir=None ): candidates = ['inscount.out' , 'inscount.txt' , 'pin.out' ] for name in candidates: path = os.path.join(workdir if workdir else '.' , name) if os.path.exists(path): try : with open (path, 'r' , encoding='utf-8' , errors='ignore' ) as f: data = f.read() m = re.search(r'(\d+)\s+instructions' , data) if m: return int (m.group(1 )), data m2 = re.search(r'(\d+)' , data) if m2: return int (m2.group(1 )), data except Exception: pass return None , None def get_inscount (payload: bytes ) -> int : IO_TIMEOUT = 5 log.info(f"Testing payload: {payload.decode('latin-1' )[:START_INDEX+1 ]} ..." ) try : proc = subprocess.run( PIN_CMD_LIST, input =payload.decode('latin-1' ) + '\n' , capture_output=True , timeout=IO_TIMEOUT, text=True , cwd='.' ) stdout = proc.stdout or "" stderr = proc.stderr or "" exitcode = proc.returncode sys.stderr.write(f"\n--- Pin Tool 完整输出 (Exit Code {exitcode} ): ---\n" ) if stdout.strip(): sys.stderr.write("[stdout]\n" + stdout.strip() + "\n" ) if stderr.strip(): sys.stderr.write("[stderr]\n" + stderr.strip() + "\n" ) sys.stderr.write("--------------------------------------------------\n" ) combined = stdout + "\n" + stderr match_re = re.search(r'(\d+)\s+instructions' , combined, re.IGNORECASE) if match_re: return int (match_re.group(1 )) lines = combined.strip().splitlines() for line in reversed (lines): s = line.strip() if s.isdigit(): return int (s) file_count, file_content = read_inscount_outfile(workdir='.' ) if file_count is not None : try : os.remove('inscount.out' ) except Exception: pass return file_count low = combined.lower() if exitcode is not None and exitcode != 0 : if any (k in low for k in ['nope' , 'try again' , 'wrong' , 'incorrect' ]): return 0 if stderr.strip(): return 0 if exitcode == 0 and any (k in low for k in ['flag' , 'correct' , 'congrats' , 'success' ]): return 10 **9 log.warning("无法从 Pin 输出或文件中解析到指令数(stdout/stderr/file 均无数字)。" ) return 0 except subprocess.TimeoutExpired as te: sys.stderr.write(f"[TIMEOUT] Pin 运行超时: {te} \n" ) return 0 except FileNotFoundError as fe: sys.stderr.write(f"[FATAL] 找不到 pin 可执行文件或路径错误: {fe} \n" ) return 0 except Exception as e: sys.stderr.write(f"[FATAL ERROR] get_inscount 发生错误: {e} \n" ) return 0 def main (): known_flag = list ("A" * FLAG_LEN) log.info(f"开始爆破,长度 {FLAG_LEN} ,字符集大小 {len (CHARSET)} ,从索引 {START_INDEX} 开始。" ) if not os.path.exists(TARGET_BINARY): log.critical(f"目标文件不存在: {TARGET_BINARY} " ) return if not os.path.exists(PIN_TOOL_PATH): log.critical(f"Pin Tool 不存在: {PIN_TOOL_PATH} " ) return if not os.path.exists(PIN_CMD_LIST[0 ]): log.critical(f"Pin 可执行文件不存在: {PIN_CMD_LIST[0 ]} " ) return for i in range (START_INDEX, FLAG_LEN): log.info(f"\n>>>> 正在爆破第 {i} 位..." ) max_count = 0 best_char = None for char in CHARSET: current_test = known_flag[:] current_test[i] = char payload = "" .join(current_test).encode('latin-1' ) count = get_inscount(payload) if count == 0 : continue if count > max_count: max_count = count best_char = char log.info(f"发现新的最大值: 字符 '{best_char} ' -> {max_count} 条指令" ) if best_char: known_flag[i] = best_char log.success(f"第 {i} 位确定为: '{best_char} '" ) log.success(f"当前 Flag: {'' .join(known_flag)} " ) else : log.warning(f"第 {i} 位爆破失败。所有字符返回 0。" ) break log.success(f"\n--- 爆破完成 ---" ) log.success(f"最终 Flag: {'' .join(known_flag)} " )if __name__ == '__main__' : main()
等一会儿
1 flag {m4z3d_vm_jump5__42}
还可以改文件字节码,让程序返回值为长度,修改 mov eax,1 为 mov eax,ebx,patch 为新文件
再利用 BFS 搜索进行单字节爆破
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 import stringimport subprocessimport sys FLAG_LENGTH = 24 CHARSET = string.ascii_letters + string.digits + string.punctuation jobs = [(list ("flag{" ), 5 )] final_flags = set ()print ("Starting BFS brute-force to find ALL passing flags..." )print ("-" * 50 )while jobs: current_guess, i = jobs.pop(0 ) if i == FLAG_LENGTH: final_flags.add("" .join(current_guess)) continue res = [] for char in CHARSET: test_guess = current_guess + [char] + ["A" ] * (FLAG_LENGTH - i - 1 ) input_data = "" .join(test_guess).encode() try : p = subprocess.run( ["./hyperjump_patched" ], input =input_data, timeout=10 , capture_output=True ) count = p.returncode if count == 0 : count = FLAG_LENGTH except subprocess.TimeoutExpired: count = -1 except Exception: count = -1 res.append((char, count)) if not res: continue max_count = max (r[1 ] for r in res) if max_count <= 0 : continue for char, count in res: if count == max_count: new_guess = current_guess + [char] jobs.append((new_guess, i + 1 )) current_flag = "" .join(new_guess) output_line = f"[Index {i+1 :02d} ] Testing: {current_flag:<{FLAG_LENGTH} } (Max Match: {max_count} ) | Jobs left: {len (jobs)} )" sys.stdout.write(f"\r{output_line} " ) sys.stdout.flush()print ("\n" + "-" * 50 )if final_flags: print ("🎉 Found ALL passing Flags:" ) for flag in sorted (list (final_flags)): print (f" -> {flag} " )else : print ("❌ No complete flag found." )