FIC 2026

2026FIC初赛wp

围绕 FIC 的公开复盘与解题记录。

上传者:Serendipity 发布日期:2026-05-15 316 次阅读

检材密码:FIC-{e404d6e66586e9460c23755afab5a872bcf78ab4}

排名22,与大佬们的差距还是太大了orz......好消息是晋级了,大佬们决赛见!

本来想着全写完发的,实在是太忙了,就先发计算机和手机吧

计算机部分

计算机VC密码在手机便签中

1. 分析计算机检材,操作系统版本号为【参考格式:1.1】

23.1

在基本信息里可以看到

2. 分析计算机检材,李安弘曾收到一份免费领取token的邮件的疑似钓鱼邮件,其发送用户邮箱为【参考格式:123@qq.com

hf13338261292@outlook.com

在邮件中一眼可以看到

3. 分析计算机检材,李安弘电脑中记录的黄金换现金的商家联系方式为【参考格式:110】

13612817854

仿真之后再开始页面找到了一个记事本

打开看到黄金换现金的商家联系方式

4. 分析计算机检材,推广设计图中的apk下载链接为【参考格式:http:///?*】

https://drive.google.com/file/d/1z3aRS-lkaJYKm7Cp1XjtUmVPsOEVW2fV/view?usp=sharing

在下载文件夹中看到了推广设计图

这个图片是被加密的,这个html可以查看加密图片,私钥是rsa的n和d

在public.txt中看到了n和e,算出d,写出解密脚本

import sympy
import libnum
​
n = 57751892008149574447756694613209346511056045951970458143905594411398554113111623746466692172544473909892773600617029641656248235151775166339061269972238018743173330948084699695182438765935110193323089354031112350869626121317836465551360104372140181097747761558797918522051881262043738603183528521379831286761
e = 65537
​
# 分解 n
p, q = sympy.factorint(n).keys()  # 只适用于小 n
p, q = list(sympy.factorint(n).keys())
print(f"p={p}, q={q}")
​
phi = (p-1)*(q-1)
d = libnum.invmod(e, phi)
print(f"d = {d}")

** **接下来解密一下图片

扫码识别一下

5. 分析计算机检材,李安弘电脑vpn软件开放的代理端口为【参考格式:80】

9527

在开始菜单这里看到clash

查看设置

6. 分析计算机检材,李安弘电脑中AI软件当前使用的模型类型为【参考格式:deepseek】

OpenRouter

在电脑右下角看到了ai软件

查看设置

7. 分析计算机检材,李安弘电脑中AI软件当前使用的模型apiKey为【参考格式:sk-abcd...】

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

搜索一下有关这个ai的文件夹,找到一堆,从文件数量最多的开始看

/home/lha/.local/share/deepin/uos-ai-assistant文件夹下找到db文件

8. 分析计算机检材,李安弘电脑中勒索软件提供的解密服务联系方式为【参考格式:abcd123232】

beijixin996@tutanota.com

用手机中的VC密码解密一下磁盘9ed2@99y8.com.cn

看到文件中有个get_token_linux文件,在浏览器的历史记录中看到访问了github中类似的文件

将这个文件导出拿ida查看一下,反编译main_main,看到有个邮箱

**转换一下邮箱上面两行可以看到 **解密请系系,所以就是这个邮箱

9. 分析计算机检材,李安弘电脑中记录的存放黄金的保险柜编号是【参考格式:1】

997546

分析main_main和main_a两个函数可知,这个程序会将mp4文件进行加密,然后同目录下.hidden文件也有提示

写一个解密脚本,解密一下与这个程序同目录下的mp4文件

#!/usr/bin/env python3
import struct, sys, shutil
for f in sys.argv[1:]:
    shutil.copy2(f, f+'.bak')
    d = bytearray(open(f,'rb').read())
    p = 0
    while (p := d.find(b'stco', p)) != -1:
        n = struct.unpack('>I', d[p+8:p+12])[0]
        for i in range(n):
            e = p+12+i*4
            d[e:e+4] = struct.pack('>I', (struct.unpack('>I', d[e:e+4])[0]-1337)&0xFFFFFFFF)
        p = p+12+n*4
    open(f,'wb').write(d)
    print(f'已修复: {f}')

修复之后打开mp4文件

10. 分析计算机检材,李安弘电脑中记录的保险柜密码是【参考格式:123456】

583985

/root/文档/zhongyao看到有关保险箱密码的文件

导出发现没有内容,在电脑中看到excel的加密代码

让ai写一个脚本解密一下

import olefile
import struct
​
ole = olefile.OleFileIO(r'd:\OneDrive\Desktop\新建文件夹\保险箱的秘密.et')
workbook_data = ole.openstream('Workbook').read()
ole.close()
​
xor_key = 85
spacing = 25
​
# Parse BIFF records
def parse_biff_records(data):
    records = []
    i = 0
    while i < len(data) - 3:
        rec_type = struct.unpack('<H', data[i:i+2])[0]
        rec_len = struct.unpack('<H', data[i+2:i+4])[0]
        rec_data = data[i+4:i+4+rec_len]
        records.append((i, rec_type, rec_len, rec_data))
        i += 4 + rec_len
        if rec_len == 0 and rec_type == 0:
            i += 1
    return records
​
records = parse_biff_records(workbook_data)
​
# Collect MSODRAWING records with their positions
mso_records = [(p, t, l, d) for p, t, l, d in records if t == 0x00EC]
obj_records = [(p, t, l, d) for p, t, l, d in records if t == 0x005D]
​
print(f"MSODRAWING records: {len(mso_records)}")
print(f"OBJ records: {len(obj_records)}")
​
# Extract UTF-16LE numeric strings from each MSODRAWING record individually
shape_values = []
for i, (pos, rec_type, rec_len, rec_data) in enumerate(mso_records):
    # Search for UTF-16LE digit sequences in this record
    nums_in_record = []
    j = 0
    while j < len(rec_data) - 1:
        code = struct.unpack('<H', rec_data[j:j+2])[0]
        if 0x30 <= code <= 0x39:
            k = j
            num_str = ""
            while k < len(rec_data) - 1:
                c = struct.unpack('<H', rec_data[k:k+2])[0]
                if 0x30 <= c <= 0x39:
                    num_str += chr(c)
                    k += 2
                else:
                    break
            if len(num_str) >= 4:
                nums_in_record.append((j, num_str))
            j = k
        else:
            j += 2
    
    if nums_in_record:
        for offset, num_str in nums_in_record:
            encoded = int(num_str)
            x_part = encoded // 1000
            y_part = encoded % 1000
            x = (x_part ^ xor_key) - 100
            y = (y_part ^ xor_key) - 100
            shape_values.append((x, y, i, num_str))
​
print(f"\nExtracted {len(shape_values)} values from MSODRAWING records")
​
# Also try: search for ANSI numbers in each MSODRAWING record
ansi_shape_values = []
for i, (pos, rec_type, rec_len, rec_data) in enumerate(mso_records):
    j = 0
    while j < len(rec_data):
        if 0x30 <= rec_data[j] <= 0x39:
            k = j
            num_str = ""
            while k < len(rec_data) and 0x30 <= rec_data[k] <= 0x39:
                num_str += chr(rec_data[k])
                k += 1
            if len(num_str) >= 4:
                encoded = int(num_str)
                x_part = encoded // 1000
                y_part = encoded % 1000
                x = (x_part ^ xor_key) - 100
                y = (y_part ^ xor_key) - 100
                ansi_shape_values.append((x, y, i, num_str))
            j = k
        else:
            j += 1
​
print(f"Extracted {len(ansi_shape_values)} ANSI values from MSODRAWING records")
​
# Check which extraction gives better results
# Valid points should have: 0 <= y <= 28 (7 rows * 4 pixels), y % 4 == 0, x >= 0
print("\n--- UTF-16LE extraction validation ---")
valid_utf16 = [(x, y, i, n) for x, y, i, n in shape_values if 0 <= y <= 28 and y % 4 == 0 and x >= 0]
print(f"Valid UTF-16LE points: {len(valid_utf16)}/{len(shape_values)}")
​
print("\n--- ANSI extraction validation ---")
valid_ansi = [(x, y, i, n) for x, y, i, n in ansi_shape_values if 0 <= y <= 28 and y % 4 == 0 and x >= 0]
print(f"Valid ANSI points: {len(valid_ansi)}/{len(ansi_shape_values)}")
​
# Use whichever source gives more valid points
all_values = valid_utf16 if len(valid_utf16) >= len(valid_ansi) else valid_ansi
source = "UTF-16LE" if len(valid_utf16) >= len(valid_ansi) else "ANSI"
print(f"\nUsing {source} with {len(all_values)} valid points")
​
# Print all decoded points
print("\nAll valid decoded points:")
for x, y, rec_idx, num_str in sorted(all_values, key=lambda v: (v[0], v[1])):
    char_idx = x // spacing
    local_x = (x % spacing) // 4
    local_y = y // 4
    print(f"  x={x:3d}, y={y:2d}, char_idx={char_idx:2d}, local=({local_x},{local_y}), encoded={num_str}")
​
# Group points by character index
char_points = {}
for x, y, rec_idx, num_str in all_values:
    char_idx = x // spacing
    local_x = (x % spacing) // 4
    local_y = y // 4
    if char_idx not in char_points:
        char_points[char_idx] = set()
    char_points[char_idx].add((local_x, local_y))
​
print(f"\nGrouped into {len(char_points)} character positions")
print(f"Character positions: {sorted(char_points.keys())}")
​
# Font dictionary
font = {
    '0':[[1,0],[2,0],[0,1],[3,1],[0,2],[3,2],[0,3],[3,3],[1,4],[2,4]],
    '1':[[2,0],[1,1],[2,1],[2,2],[2,3],[1,4],[2,4],[3,4]],
    '2':[[1,0],[2,0],[0,1],[3,1],[2,2],[1,3],[0,4],[1,4],[2,4],[3,4]],
    '3':[[0,0],[1,0],[2,0],[3,1],[1,2],[2,2],[3,3],[0,4],[1,4],[2,4]],
    '4':[[3,0],[2,1],[3,1],[1,2],[3,2],[0,3],[1,3],[2,3],[3,3],[4,3],[3,4]],
    '5':[[0,0],[1,0],[2,0],[0,1],[0,2],[1,2],[2,2],[3,3],[0,4],[1,4],[2,4]],
    '6':[[1,0],[2,0],[0,1],[0,2],[1,2],[2,2],[0,3],[3,3],[1,4],[2,4]],
    '7':[[0,0],[1,0],[2,0],[3,0],[3,1],[2,2],[1,3],[1,4]],
    '8':[[1,0],[2,0],[0,1],[3,1],[1,2],[2,2],[0,3],[3,3],[1,4],[2,4]],
    '9':[[1,0],[2,0],[0,1],[3,1],[1,2],[2,2],[3,2],[3,3],[2,4]],
    'a':[[1,2],[2,2],[3,2],[0,3],[3,3],[1,4],[2,4],[3,4]],
    'b':[[0,0],[0,1],[0,2],[1,2],[2,2],[0,3],[3,3],[0,4],[1,4],[2,4]],
    'c':[[1,0],[2,0],[3,0],[0,1],[0,2],[0,3],[1,4],[2,4],[3,4]],
    'd':[[3,0],[3,1],[1,2],[2,2],[3,2],[0,3],[3,3],[1,4],[2,4],[3,4]],
    'e':[[1,0],[2,0],[0,1],[0,2],[1,2],[2,2],[0,3],[1,4],[2,4]],
    'f':[[1,0],[2,0],[1,1],[0,2],[1,2],[2,2],[1,3],[1,4]],
    'g':[[1,2],[2,2],[3,2],[0,3],[3,3],[1,4],[2,4],[3,4],[3,5],[1,6],[2,6]],
    'h':[[0,0],[0,1],[0,2],[1,2],[2,2],[0,3],[3,3],[0,4],[3,4]],
    'i':[[1,0],[1,2],[1,3],[1,4]],
    'j':[[2,0],[2,2],[2,3],[2,4],[2,5],[1,6],[0,5]],
    'k':[[0,0],[0,1],[0,2],[0,3],[0,4],[2,2],[1,3],[3,3],[2,4]],
    'l':[[1,0],[1,1],[1,2],[1,3],[1,4]],
    'm':[[0,1],[1,1],[2,1],[3,1],[4,1],[0,2],[2,2],[4,2],[0,3],[4,3]],
    'n':[[0,1],[1,0],[2,0],[0,2],[3,2],[0,3],[3,3],[0,4],[3,4]],
    'o':[[1,1],[2,1],[0,2],[3,2],[1,3],[2,3]],
    'p':[[0,2],[1,2],[2,2],[0,3],[3,3],[0,4],[1,4],[2,4],[0,5],[0,6]],
    'q':[[1,2],[2,2],[0,3],[3,2],[3,3],[3,4],[3,5],[4,5]],
    'r':[[0,2],[0,3],[0,4],[1,2],[2,2]],
    's':[[1,0],[2,0],[3,0],[0,1],[1,2],[2,2],[3,3],[0,4],[1,4],[2,4]],
    't':[[1,0],[1,1],[1,2],[1,3],[1,4],[0,2],[2,2]],
    'u':[[0,2],[0,3],[3,2],[3,3],[1,4],[2,4],[3,4]],
    'v':[[0,0],[4,0],[1,2],[3,2],[2,4]],
    'w':[[0,2],[0,3],[1,4],[2,3],[3,4],[4,2],[4,3]],
    'x':[[0,0],[4,0],[1,1],[3,1],[2,2],[1,3],[3,3],[0,4],[4,4]],
    'y':[[0,0],[4,0],[1,1],[3,1],[2,2],[2,3],[1,4]],
    'z':[[0,0],[1,0],[2,0],[3,0],[2,1],[1,2],[0,3],[0,4],[1,4],[2,4],[3,4]],
    ':':[[1,1],[1,3]],
    '@':[[1,0],[2,0],[0,1],[3,1],[0,2],[2,2],[3,2],[0,3],[1,4],[2,4]],
    '.':[[1,4]]
}
​
# Match each character
result = ""
for idx in sorted(char_points.keys()):
    point_set = char_points[idx]
    matches = []
    for char, matrix in font.items():
        matrix_set = set(tuple(p) for p in matrix)
        overlap = len(point_set & matrix_set)
        total_font = len(matrix_set)
        total_data = len(point_set)
        precision = overlap / total_data if total_data > 0 else 0
        recall = overlap / total_font if total_font > 0 else 0
        f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0
        matches.append((char, precision, recall, f1, overlap, total_font, total_data))
    matches.sort(key=lambda x: x[3], reverse=True)
    best = matches[0]
    char, prec, rec, f1, overlap, total_font, total_data = best
    result += char
    print(f"\n  Char {idx}: {total_data} data pts")
    print(f"    Best: '{char}' (overlap={overlap}/{total_font}, prec={prec:.2f}, rec={rec:.2f}, f1={f1:.2f})")
​
    # Visualize
    max_y = max(p[1] for p in point_set) if point_set else 0
    max_x = max(p[0] for p in point_set) if point_set else 0
    for row in range(max_y + 1):
        line = ""
        for col in range(max_x + 1):
            line += "#" if (col, row) in point_set else "."
        print(f"    {line}")
​
print(f"\n{'='*50}")
print(f"DECODED MESSAGE: {result}")
print(f"{'='*50}")
​

手机部分

1. 分析手机检材,该手机型号为【参考格式:HUAWEIP90】

Redmi Note 7 Pro

直接查看设备信息

2. 分析手机检材,李安弘手机计划前往迪拜的日期是【参考格式:20260101】

20260606

便签中没有看到计划,那看看有没有其他软件可以写计划的,在应用列表中看到小米便签

直接查看一下数据库/data/user/0/com.miui.notes/databases/todo.db,找到计划

3. 分析手机检材,李安弘手机中与网站搭建人员沟通所使用的app安装日期为【参考格式:20260101】

20260414

在应用程序中看到了类似于聊天的app

去应用列表查看一下安装时间

4. 分析手机检材,李安弘手机中与网站搭建人员沟通所使用的app,存放聊天数据的数据库为【参考格式:message.db】

wk_9628874a3c6b403593766496fa985893.db

查看一下文件的数据库文件com.talk.uuuim/databases

发现第二个查看不了,结合第五题,说明这个数据库加密了,用DB Browser for SQLCipher打开

需要密码解密,那么问题来了,密码怎么找?不妨看看文件名,为什么长这样

说实话要不是ai真不知道密码是文件名🫣

使用文件名解开数据库可以看到聊天记录,同时也印证了上一题是正确的

5. 分析手机检材,存放聊天数据的数据库的解密密码为【参考格式:123456】

9628874a3c6b403593766496fa985893

密码即文件名,具体解析见上题

6. 分析手机检材,李安弘购买云服务器商家的收款备用钱包地址为【参考格式:1】

TN8vQzB3n7W5wVca9W4kL2wP7xY9zM5nU1

在数据库中可以看到

7. 分析手机检材,李安弘手机中给网站搭建人员第一次转账的交易hash前6位为【参考格式:1】

26226f

在这里看到一张图片,说是转账,然后对方说收到了

查看一下这张图片名称

直接搜索,看到交易哈希

8. 分析手机检材,手机中使用的AI软件李安弘主动向AI提问了几次【参考格式:1】

5

得先找一下ai软件,搜索ai看到一个软件

/data/user/0/com.pocketpalai/pocketpalai.db找到对话记录

9. 分析手机检材,李安弘手机使用的AI软件调用本地AI模型及版本为【参考格式:Fic2.6】

Qwen3.5

继续在该目录下找到本地ai模型/data/user/0/com.pocketpalai/files/models/local

10. 分析手机检材,李安弘曾使用无人机航拍,分析其飞行轨迹,其在哪个县进行飞行【参考格式:平安县】

米脂县

无人机的话,我只知道大疆,在应用程序中看到了个fly

查看一下应用文件,在/storage/emulated/0/Android/data/dji.go.v5/files/FlightRecord文件夹中看到了记录的飞行日志

写一个脚本分析一下

import struct
import os
import json
import subprocess
import sys
import math
from datetime import datetime, timezone, timedelta
​
BASE = os.path.dirname(os.path.abspath(__file__))
NODE_PARSER = os.path.join(BASE, "parse_dji.mjs")
​
​
def call_node_parser(filepath):
    js_code = f'''
import {{ DJILog }} from "dji-log-parser-js";
import fs from "fs";
​
const buffer = fs.readFileSync("{filepath.replace(chr(92), "/")}");
const bytes = new Uint8Array(buffer);
try {{
    const parser = new DJILog(bytes);
    const result = {{
        version: parser.version,
        details: parser.details,
        keychainsRequest: parser.keychainsRequest ? parser.keychainsRequest() : null
    }};
    console.log(JSON.stringify(result));
}} catch (e) {{
    console.error(JSON.stringify({{error: e.message}}));
    process.exit(1);
}}
'''
    tmp_js = os.path.join(BASE, "_tmp_parse.mjs")
    with open(tmp_js, "w", encoding="utf-8") as f:
        f.write(js_code)
    try:
        result = subprocess.run(
            ["node", tmp_js],
            capture_output=True, text=True, timeout=30,
            cwd=BASE
        )
        if result.returncode != 0:
            return {"error": result.stderr.strip()}
        return json.loads(result.stdout.strip())
    finally:
        if os.path.exists(tmp_js):
            os.remove(tmp_js)
​
​
def analyze_entropy(data):
    if len(data) == 0:
        return 0.0
    freq = [0] * 256
    for b in data:
        freq[b] += 1
    total = len(data)
    entropy = -sum((c / total) * math.log2(c / total) for c in freq if c > 0)
    return entropy
​
​
def format_duration(seconds):
    m, s = divmod(int(seconds), 60)
    h, m = divmod(m, 60)
    if h > 0:
        return f"{h}小时{m}分{s}秒"
    return f"{m}分{s}秒"
​
​
def format_speed(ms):
    return f"{ms:.1f} m/s ({ms * 3.6:.1f} km/h)"
​
​
def main():
    files = sorted([
        f for f in os.listdir(BASE)
        if f.startswith("FlightRecord") and f.endswith(".txt")
    ])
​
    print("=" * 80)
    print("          DJI 无人机飞行记录日志分析报告")
    print("=" * 80)
    print(f"分析时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
    print(f"文件目录: {BASE}")
    print(f"日志文件数: {len(files)}")
    print()
​
    flights = []
    for fname in files:
        fpath = os.path.join(BASE, fname)
        fsize = os.path.getsize(fpath)
​
        with open(fpath, "rb") as f:
            body = f.read()
​
        header_val, detail_bytes, version = struct.unpack_from("<Qhb", body, 0)
​
        entropy = analyze_entropy(body[100:])
​
        parsed = call_node_parser(fpath)
        details = parsed.get("details", {})
​
        flights.append({
            "file": fname,
            "size": fsize,
            "version": version,
            "header_val": header_val,
            "detail_bytes": detail_bytes,
            "entropy": entropy,
            "details": details,
            "error": parsed.get("error"),
        })
​
    cns = timezone(timedelta(hours=8))
​
    for i, fl in enumerate(flights, 1):
        d = fl["details"]
        print("=" * 80)
        print(f"  飞行记录 #{i}")
        print("=" * 80)
​
        if fl["error"]:
            print(f"  [错误] 解析失败: {fl['error']}")
            print()
            continue
​
        print(f"\n  [文件信息]")
        print(f"    文件名:       {fl['file']}")
        print(f"    文件大小:     {fl['size']:,} bytes ({fl['size']/1024:.1f} KB)")
        print(f"    日志版本:     {fl['version']}")
        print(f"    数据熵值:     {fl['entropy']:.4f} bits/byte")
        print(f"    记录行数:     {d.get('recordLineCount', 'N/A')}")
​
        print(f"\n  [无人机信息]")
        print(f"    机型:         {d.get('aircraftName', 'N/A')}")
        print(f"    产品类型:     {d.get('productType', 'N/A')}")
        print(f"    飞行器SN:     {d.get('aircraftSn', 'N/A')}")
        print(f"    相机SN:       {d.get('cameraSn', 'N/A')}")
        print(f"    遥控器SN:     {d.get('rcSn', 'N/A')}")
        print(f"    电池SN:       {d.get('batterySn', 'N/A')}")
        print(f"    应用平台:     {d.get('appPlatform', 'N/A')}")
        print(f"    应用版本:     {d.get('appVersion', 'N/A')}")
​
        print(f"\n  [飞行参数]")
        start_utc = d.get("startTime", "")
        if start_utc:
            try:
                dt_utc = datetime.fromisoformat(start_utc.replace("Z", "+00:00"))
                dt_local = dt_utc.astimezone(cns)
                start_str = dt_local.strftime("%Y-%m-%d %H:%M:%S")
            except:
                start_str = start_utc
        else:
            start_str = "N/A"
        print(f"    起飞时间:     {start_str}")
​
        total_time = d.get("totalTime", 0)
        print(f"    飞行时长:     {format_duration(total_time)} ({total_time:.1f}s)")
​
        total_dist = d.get("totalDistance", 0)
        print(f"    飞行距离:     {total_dist:.3f} km ({total_dist * 1000:.1f} m)")
​
        max_h = d.get("maxHeight", 0)
        print(f"    最大高度:     {max_h} m")
​
        max_hs = d.get("maxHorizontalSpeed", 0)
        print(f"    最大水平速度: {format_speed(max_hs)}")
​
        max_vs = d.get("maxVerticalSpeed", 0)
        print(f"    最大垂直速度: {format_speed(max_vs)}")
​
        lon = d.get("longitude", 0)
        lat = d.get("latitude", 0)
        print(f"    起飞经度:     {lon:.10f}")
        print(f"    起飞纬度:     {lat:.10f}")
        if lon and lat:
            print(f"    Google地图:   https://maps.google.com/?q={lat},{lon}")
​
        takeoff_alt = d.get("takeOffAltitude", 0)
        print(f"    起飞点海拔:   {takeoff_alt:.1f} m")
​
        print(f"\n  [拍摄记录]")
        print(f"    拍照数量:     {d.get('captureNum', 0)}")
        video_time_ms = d.get("videoTime", 0)
        print(f"    录像时长:     {video_time_ms / 1000:.1f}s ({format_duration(video_time_ms / 1000)})")
​
        print()
​
    if len(flights) >= 2:
        print("=" * 80)
        print("          两次飞行对比分析")
        print("=" * 80)
​
        d1, d2 = flights[0]["details"], flights[1]["details"]
​
        print(f"\n  [基本信息对比]")
        print(f"    {'项目':<16} {'飞行#1':<30} {'飞行#2':<30}")
        print(f"    {'-'*16} {'-'*30} {'-'*30}")
​
        items = [
            ("起飞时间",
             lambda d: _fmt_time(d.get("startTime", ""), cns),
             lambda d: _fmt_time(d.get("startTime", ""), cns)),
            ("飞行时长",
             lambda d: format_duration(d.get("totalTime", 0)),
             lambda d: format_duration(d.get("totalTime", 0))),
            ("飞行距离",
             lambda d: f"{d.get('totalDistance', 0):.3f} km",
             lambda d: f"{d.get('totalDistance', 0):.3f} km"),
            ("最大高度",
             lambda d: f"{d.get('maxHeight', 0)} m",
             lambda d: f"{d.get('maxHeight', 0)} m"),
            ("最大水平速度",
             lambda d: format_speed(d.get("maxHorizontalSpeed", 0)),
             lambda d: format_speed(d.get("maxHorizontalSpeed", 0))),
            ("最大垂直速度",
             lambda d: format_speed(d.get("maxVerticalSpeed", 0)),
             lambda d: format_speed(d.get("maxVerticalSpeed", 0))),
            ("起飞海拔",
             lambda d: f"{d.get('takeOffAltitude', 0):.1f} m",
             lambda d: f"{d.get('takeOffAltitude', 0):.1f} m"),
            ("录像时长",
             lambda d: f"{d.get('videoTime', 0)/1000:.1f}s",
             lambda d: f"{d.get('videoTime', 0)/1000:.1f}s"),
            ("记录行数",
             lambda d: str(d.get("recordLineCount", "N/A")),
             lambda d: str(d.get("recordLineCount", "N/A"))),
            ("电池SN",
             lambda d: d.get("batterySn", "N/A"),
             lambda d: d.get("batterySn", "N/A")),
        ]
​
        for label, fn1, fn2 in items:
            print(f"    {label:<16} {fn1(d1):<30} {fn2(d2):<30}")
​
        print(f"\n  [关键发现]")
        same_aircraft = d1.get("aircraftSn") == d2.get("aircraftSn")
        same_battery = d1.get("batterySn") == d2.get("batterySn")
        same_rc = d1.get("rcSn") == d2.get("rcSn")
​
        print(f"    * 两次飞行使用{'同一架' if same_aircraft else '不同'}飞行器 (SN: {d1.get('aircraftSn', 'N/A')})")
        print(f"    * 两次飞行使用{'同一块' if same_battery else '不同'}电池")
        if not same_battery:
            print(f"      - 飞行#1电池: {d1.get('batterySn', 'N/A')}")
            print(f"      - 飞行#2电池: {d2.get('batterySn', 'N/A')}")
        print(f"    * 两次飞行使用{'同一个' if same_rc else '不同'}遥控器 (SN: {d1.get('rcSn', 'N/A')})")
​
        t1 = d1.get("totalTime", 0)
        t2 = d2.get("totalTime", 0)
        dist1 = d1.get("totalDistance", 0)
        dist2 = d2.get("totalDistance", 0)
        print(f"    * 飞行#1时长是飞行#2的 {t1/max(t2,0.01):.1f} 倍")
        print(f"    * 飞行#1距离是飞行#2的 {dist1/max(dist2,0.0001):.1f} 倍")
        print(f"    * 飞行#1最高飞行 {d1.get('maxHeight',0)}m,飞行#2仅 {d2.get('maxHeight',0)}m")
​
        lon1, lat1 = d1.get("longitude", 0), d1.get("latitude", 0)
        lon2, lat2 = d2.get("longitude", 0), d2.get("latitude", 0)
        if lon1 and lat1 and lon2 and lat2:
            from math import radians, cos, sin, asin, sqrt
            dlat = radians(lat2 - lat1)
            dlon = radians(lon2 - lon1)
            a = sin(dlat/2)**2 + cos(radians(lat1)) * cos(radians(lat2)) * sin(dlon/2)**2
            c = 2 * asin(sqrt(a))
            r = 6371000
            dist_m = c * r
            print(f"    * 两次起飞点相距约 {dist_m:.1f} 米")
​
        gap_min = 0
        try:
            dt1 = datetime.fromisoformat(d1.get("startTime", "").replace("Z", "+00:00"))
            dt2 = datetime.fromisoformat(d2.get("startTime", "").replace("Z", "+00:00"))
            gap = (dt2 - dt1).total_seconds() - t1
            if gap > 0:
                gap_min = gap / 60
                print(f"    * 两次飞行间隔约 {gap_min:.1f} 分钟 (飞行#1着陆到飞行#2起飞)")
        except:
            pass
​
        h1 = d1.get("maxHeight", 0)
        h2 = d2.get("maxHeight", 0)
        if h2 < 50 and h1 > h2:
            print(f"    * [!] 飞行#2最大高度仅{h2}m,远低于飞行#1的{h1}m,可能为低空短距测试飞行")
​
        v1 = d1.get("videoTime", 0)
        v2 = d2.get("videoTime", 0)
        if v1 > 10000 and v2 < 5000:
            print(f"    * 飞行#1录像时间较长({v1/1000:.1f}s),飞行#2录像时间很短({v2/1000:.1f}s)")
​
        print(f"\n  [地理位置]")
        print(f"    根据坐标({lat1:.4f}, {lon1:.4f})判断,起飞位置位于中国山西省吕梁市附近")
​
        print()
​
    print("=" * 80)
    print("  注意: 飞行日志版本14,遥测数据已加密(AES),需要DJI API密钥才能解密详细帧数据。")
    print("  以上信息来源于日志未加密的元数据(Detail)区域。")
    print("  如需完整遥测数据(轨迹、速度曲线等),可使用AirData或DJI Logbook工具在线解析。")
    print("=" * 80)
​
​
def _fmt_time(ts_str, tz):
    if not ts_str:
        return "N/A"
    try:
        dt_utc = datetime.fromisoformat(ts_str.replace("Z", "+00:00"))
        dt_local = dt_utc.astimezone(tz)
        return dt_local.strftime("%Y-%m-%d %H:%M:%S")
    except:
        return ts_str
​
​
if __name__ == "__main__":
    main()
​

打开地图看一眼

11. 分析手机检材,李安弘最近安装了一个视频类APP,该APP声明了多个敏感权限用于收集用户隐私。请选择其中涉及用户隐私的敏感权限。

A. READ_CONTACTS

B. READ_SMS

C. RECEIVE_BOOT_COMPLETED

D. READ_CALL_LOGE

ABD

找一下这个app

用雷电app分析一下

12. 上述APP启动后会加载一个色情网站。请找出该APP当网络不可用时APP加载的本地离线页面路径。

file:///android_asset/www/index.html

分析MainActivity源码,在onCreate方法中发现WebView加载逻辑

可以看到网络可用时加载在线URL:https://www.sp-live88.com,网络不可用时加载本地离线页面:file:///android_asset/www/index.html

13. 上述APP将非法收集的用户隐私数据上传至远程服务器。上传地址在代码中经过编码处理。请找出编码方式,还原出完整的上传服务器URL。

https://api.sp-live88.com/collect/userdata

既然是上传,搜素upload

看到个类名,查看一下

base64加密,进行解密

14. 该APP在本地创建了SQLite数据库存储收集到的用户信息。请分析代码,写出用于存储用户信息的表名

user_collection

直接搜索有关sql语句的代码,看到了题目所需信息

查看代码逻辑可以知道他是收集用户信息的表

15. 该APP的assets目录中存在一个加密配置文件config.dat。请解密该文件,写出其中的USDT钱包地址

TXqH7sVn8bR4kL2mN9pW6xJ3cY5dF1gA

搜索一下config.dat

分析一下代码

可以看到先从assets/config.dat读取加密的二进制数据,再从strings.xml获取config_seed字符串值,取md5的前16位作为key进行解密,接下来得找一下config_seed,搜索一下

找到为hotclub_2026_sec,进行解密

import hashlib
import zipfile
import json
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
​
# Step 1: 从 APK 的 resources.arsc 中提取 config_seed
with zipfile.ZipFile("HotClub_v2.1.6.apk", 'r') as z:
    arsc_data = z.read('resources.arsc')
​
idx = arsc_data.find(b'hotclub')
seed = arsc_data[idx:arsc_data.find(b'\x00', idx)].decode('utf-8')
# seed = "hotclub_2026_sec"
​
# Step 2: MD5 派生 AES 密钥
md5_hash = hashlib.md5(seed.encode('utf-8')).hexdigest()
# md5_hash = "3ffc0b996b851d8020ec9232219284ef"
aes_key = md5_hash[:16].encode('utf-8')
# aes_key = b"3ffc0b996b851d80"
​
# Step 3: AES/ECB/PKCS5Padding 解密
with open("config.dat", "rb") as f:
    encrypted = f.read()
​
cipher = AES.new(aes_key, AES.MODE_ECB)
plaintext = unpad(cipher.decrypt(encrypted), AES.block_size, style="pkcs7")
config = json.loads(plaintext.decode('utf-8'))
​
# Step 4: 提取 USDT 钱包地址
for s in config.get('sponsors', []):
    if 'wallet' in s:
        print(f"USDT钱包地址: {s['wallet']}")

得到钱包地址

16. 该APP前端JS代码可以直接调用Android原生方法获取用户隐私数据。请分析暴露了哪些方法用于获取通讯录?

getContactsList()

  1. 首先需要理解一个关键概念:Android中WebView可以通过 addJavascriptInterface 方法将Java对象注入到JavaScript环境中,使得前端网页能够直接调用Android原生方法。被注入的方法必须带有 @JavascriptInterface 注解。
  2. 所以解题的第一步是搜索 addJavascriptInterface 这个关键字,找到JS桥接的注册位置。在MainActivity的onCreate方法中,发现注册了一个名为 AppNative 的桥接对象,对应的Java类是 NativeBridge 。
  3. 第二步是分析 NativeBridge 这个类,查看其中所有带有 @JavascriptInterface 注解的方法。这个类一共暴露了7个方法,分别用于获取设备ID、设备型号、IMEI号、手机号、通讯录、已安装应用列表和位置信息。
  4. 其中与通讯录相关的方法是 getContactsList ,它通过ContentResolver查询系统的联系人数据库,提取所有联系人的姓名和电话号码,以JSON格式返回给前端。

17. 当主上传服务器不可达时,APP会获取备用服务器地址。请分析备用服务器的完整域名和端口 a.b.c:80

backup.sp-live88.xyz:8443

  1. 定位native方法: 在DataUploader类中发现主上传服务器失败时调用native方法getBackupEndpoint()获取备用地址。该方法和getCommKey()均声明为native,加载自libsecurity.so
  2. 提取SO文件并反汇编: 从APK中提取lib/arm64-v8a/libsecurity.so(8376字节),使用capstone反汇编器(CS_ARCH_AARCH64)进行ARM64反汇编
  3. 分析ELF结构: 解析ELF段表,定位关键段:
    • .rodata (0x980-0xAE8): 存储密钥块和字符串常量
    • .text (0xCEC-0x1578): 代码段
  4. 识别关键函数:
    • 0xDFC: getBackupEndpoint — 加载偏移0xAB0处32字节加密blob,调用0x1324解密,按%s://%s%s格式拼接URL
    • 0xF04: getCommKey — 加载偏移0xAD0处16字节加密blob,调用0x1324解密
    • 0x1324: decrypt — 核心解密函数,包含两阶段解密算法

写代码进行解密

import struct
​
# 从 libsecurity.so 中提取的关键数据
so_data = open("libsecurity.so", "rb").read()
​
blob1 = so_data[0xAB0:0xAD0]      # 32字节加密blob (备用服务器)
key_block2 = so_data[0x990:0x9A0]  # 16字节XTEA密钥
k = list(struct.unpack('<4I', key_block2))
# k = [0xDEADBEEF, 0xCAFEBABE, 0x12345678, 0x9ABCDEF0]
​
# ---- Phase 1: XOR解密 ----
# ARM64 NEON反汇编:
#   movi v0.16b, #0xf   ; AND掩码
#   movi v1.16b, #0x55  ; XOR常量
#   key_block1 = {0,1,...,15}, &0x0F后不变
#   等价于: decrypted[i] = encrypted[i] ^ (i%16) ^ 0x55
def phase1_xor(data):
    result = bytearray(data)
    for i in range(len(result)):
        result[i] = result[i] ^ (i & 0x0F) ^ 0x55
    return bytes(result)
​
# ---- Phase 2: 自定义双sum XTEA解密 ----
# ARM64反汇编关键常量:
#   delta     = 0x61C8864F
#   sum1_init = 0x8DDE6C40
#   sum2_init = 0xEFA6F28F
#   rounds    = 64
DELTA = 0x61C8864F
SUM1_INIT = 0x8DDE6C40
SUM2_INIT = 0xEFA6F28F
ROUNDS = 64
​
def phase2_xtea_decrypt(data):
    result = bytearray(data)
    for block_idx in range(len(data) // 8):
        offset = block_idx * 8
        v0 = struct.unpack_from('<I', result, offset)[0]
        v1 = struct.unpack_from('<I', result, offset + 4)[0]
        sum1 = SUM1_INIT
        sum2 = SUM2_INIT
        for _ in range(ROUNDS):
            # v1 update
            key_idx1 = (sum1 >> 11) & 3
            mix1 = (((v0 << 4) & 0xFFFFFFFF) ^ (v0 >> 5)) & 0xFFFFFFFF
            mix1 = (mix1 + v0) & 0xFFFFFFFF
            combined1 = (sum1 + k[key_idx1]) & 0xFFFFFFFF
            v1 = (v1 - (mix1 ^ combined1)) & 0xFFFFFFFF
            sum1 = (sum1 + DELTA) & 0xFFFFFFFF
​
            # v0 update
            key_idx2 = sum2 & 3
            mix2 = (((v1 << 4) & 0xFFFFFFFF) ^ (v1 >> 5)) & 0xFFFFFFFF
            mix2 = (mix2 + v1) & 0xFFFFFFFF
            combined2 = (sum2 + k[key_idx2]) & 0xFFFFFFFF
            v0 = (v0 - (mix2 ^ combined2)) & 0xFFFFFFFF
            sum2 = (sum2 + DELTA) & 0xFFFFFFFF
        struct.pack_into('<II', result, offset, v0, v1)
    return bytes(result)
​
# 执行两阶段解密
step1 = phase1_xor(blob1)
step2 = phase2_xtea_decrypt(step1)
​
# PKCS7去填充
pad_len = step2[-1]
host = step2[:-pad_len].decode('ascii')
print(f"备用服务器: {host}")

得到答案

服务器部分

这个服务器主要就是难在了仿真这块,当然也可以不仿真直接ai做,这里我还是仿真做法

仿真的时候把两个镜像都放进去

会显示操作系统检测失败,有人说最新版可以检测到,但是我就是最新版,不知道为什么

自定义选择操作系统,选择其他仿真即可。

启动虚拟机可能有一段时间是黑屏,这是正常的,因为太卡了,我启动了一下3D图形好像会好一点

成功仿真

用finalshell连接的时候发现没有ssh,所以装一下ssh(可以先往后看,少走弯路,因为我要干坏事)

sudo apt update
sudo apt install openssh-server
sudo systemctl status ssh / service ssh start
如果系统防火墙已启用,需要开放SSH端口
sudo ufw allow ssh

这里输命令的时候别管他卡不卡,直接输了按回车就行

用finalshell连接一下,不然太折磨了,这里注意账号为mac密码为123456

很好,你以为这就结束了吗,其实刚刚那一切都在容器里,因为做题的时候发现什么都没有,甚至不能用systemctl还有用ls的时候蹦出来的东西

出题人真恶心啊,所以我们需要按ctrl+alt+f2退出容器(在按之前,记得先把容器里的ssh卸载掉,不然会冲突的,如果已经按了,可以重启一下然后卸载apt-get remove --purge openssh-server,如果不行,重新仿真一下虚拟机)** ** 退出容器之后,进入虚拟机,这就对味了

重新安装ssh吧😼 安装好之后,编辑一下配置文件vim /etc/ssh/sshd_config

** **然后重启sshservice ssh restart这样就可以用root身份连接了

现在就可以让ai用ssh连上服务器大放光彩了😁

1. 该服务器主机操作系统版本为 【参考格式:0.9】

Debian GNU/Linux 13 (trixie)

查看系统版本cat /etc/os-release

2. 该服务器根分区硬盘的uuid号为 【参考格式:a1b2-c3】

3231e52f-5e15-44c4-b224-e29cb4201c0e

cat /etc/fstab

3. 该服务器中最新的docker镜像创建时间为 【参考格式:2020-01-01T00:00:00.012345678Z】

2026-04-16T07:15:50.535713491Z

查看Docker镜像列表及创建时间

docker images --format "{{.Repository}}:{{.Tag}} {{.CreatedAt}}"

4. 该服务器根分区快照路径为 【参考格式:/abc/def】

/root/history

查看根分区文件系统类型df -Th /

列出所有btrfs子卷btrfs subvolume list /

**查看子卷详细信息,确认 **root/history 为快照btrfs subvolume show /root/history

5. 该网站后台管理入口对应的文件名为 【参考格式:123.txt】

user.php

/etc/nginx/sites-enabled/default中找到网站根目录root /var/www/html/maccms10

去网站根目录找一下后台管理文件/var/www/html/maccms10/

看到一个user.php文件

说明是改名改成user.php的

6. 该网站设置的icp备案号为 【参考格式:icp123】

icp1919810

查看MacCMS站点配置文件/var/www/html/maccms10/application/extra/maccms.php

7. 该网站设置的主域名为 【参考格式:abc.com】

www.2026fic.forensix

由上题可知

8. 该网站分类3中,视频的拼音为 【参考格式:abc】

sipaanshe

/var/www/html/maccms10/application/database.php找到数据库信息

连接数据库

成功连接数据库

SELECT vod_id, type_id, type_id_1, vod_name, vod_en
FROM mac_vod
WHERE type_id=3 OR type_id_1=3;

9. 该站点设置页面中,被使用的前端模板来自于哪个源文件? 【参考格式:abc.def】

info.ini

查看当前使用的前端模板配置/var/www/html/maccms10/application/extra/maccms.php

/var/www/html/maccms10/template/001tep/找文件

打开info.ini看到前端模版

10. 该网站的伪静态规则配置文件sm3值为 【参考格式:ABC123】

e73407468e6f52af54c7b14632eeeb9be25b05106d06c4c3085fc843c223793f

该网站使用Nginx,伪静态规则配置在Nginx站点配置文件中/etc/nginx/sites-available/default

查看到关键规则内容,计算sm3openssl dgst -sm3 /etc/nginx/sites-available/default

11. 该网站关联的数据库的ip地址为 【参考格式:1.1.1.1】

10.0.3.100

刚刚在数据库配置中看到'hostname' => 'mytidb',去/etc/hosts查看一下对应ip

12. 该网站数据库使用了哪一类容器技术 【参考格式:abc】

LXC

**从Q11已知数据库主机名为 **mytidb,IP为 10.0.3.100。该IP是内网地址,但不在宿主机网卡上,说明数据库运行在某种容器/虚拟化环境中

先看dockerdocker ps

没有与数据库相关的容器,排除Docker,查看宿主机网络接口

ip addr

**发现 **lxcbr0 网桥接口,IP为 10.0.3.1,与数据库IP 10.0.3.100 在同一网段,这是LXC的默认网桥

查看LXC容器配置确认cat /var/lib/lxc/mytidb/config确认答案

13. 运行在4000端口的备份数据库版本号为 【参考格式:v1.1.1】

v7.5.0

确认4000端口运行的服务lxc-attach -n mytidb -- ss -tlnp | grep 4000

确认4000端口运行的是TiDB服务,查看版本cat /db/tidb/etc/systemd/system/tidb.service

14. 新注册用户数量最多的日期为 【参考格式:2000/1/1】

2026/4/15

直接在数据库里查询

SELECT DATE_FORMAT(FROM_UNIXTIME(user_reg_time), '%Y/%c/%e') 
AS reg_date, COUNT(*) AS cnt 
FROM mac_user 
GROUP BY reg_date 
ORDER BY cnt DESC LIMIT 5

15. 马慧美最后一次登录该网站的ip为 【参考格式:1.1.1.1】

51.43.21.163

查询"马慧美"用户最后一次登录该网站的ip

select user_id,user_name,inet_ntoa(user_login_ip),from_unixtime(user_login_time),inet_ntoa(user_last_login_ip),from_unixtime(user_last_login_time)
from mac_user
where lower(user_name)='ma hui mei';

16. 以下哪个文件系统未被使用

A. ntfs

B. btrfs

C. xfs

D. lvm

A

检查块设备及文件系统lsblk -f

查看已挂载文件系统的类型、挂载点和源设备findmnt -no FSTYPE,TARGET,SOURCE | sort -u

查看块设备UUID和类型blkid

总结发现ntfs和xfs均未被挂载使用,但ntfs是Windows专用文件系统,Linux服务器不可能使用ntfs,是最明确"未被使用"的选项

17. 该服务器安装了以下那些数据库服务

A. mysql

B. GuessDB

C. tidb

D. postgresql

E. mariadb

ACD

  1. 检查宿主机上的数据库服务dpkg -l | grep -iE 'mysql|mariadb|postgres|tidb|guessdb'
  • ostgresql-17 (完整安装,v17.9)
  • mariadb-client (仅客户端,v11.8.6)
  • php8.4-mysql, php8.4-pgsql (PHP驱动)
  1. 检查LXC容器内的数据库服务lxc-attach -n mytidb -- dpkg -l | grep -iE 'mysql|mariadb'
  • mysql-server-8.0 (v8.0.45,完整服务端)
  • mysql-client-8.0 (客户端)
  1. 检查TiDB运行状态lxc-attach -n mytidb -- ps aux | grep tidb
  2. 检查GuessDB

find / -name "*guessdb*" -o -name "*GuessDB*" 2>/dev/null

dpkg -l | grep -i guess

未找到任何GuessDB相关安装

  1. 检查MariaDB服务端

dpkg -l | grep mariadb-server

systemctl status mariadb

宿主机仅安装了mariadb-client(客户端),未安装mariadb-serverLXC容器内运行的是MySQL 8.0.45,不是MariaDB

互联网部分

1. 售卖卡密的公开群组ID为 【参考格式:@abc123】

@FIC_2026

/var/www/html/maccms10/application/extra/maccms.php中找到tg链接

2. 备份数据库中视频图片的文件名为 【参考格式:abc.png】

7b3fdd9d464ce48e7f20cd45f918c9a6.jpg

由服务器13题可知,4000端口存在备份数据库,连接数据库,只需要换端口即可

查看数据库文件,在mac_vod表中看到文件名

3. ngrok提供的域名为 【参考格式:a.b.c】

blemish-junior-unengaged.ngrok-free.dev

/root/.config/ngrok/ngrok.yml找到配置文件

发现没有任何信息,查看crontab发现开机自启配置cat /etc/crontab | grep ngrok

启动ngrokngrok http 80查看到域名

二进制程序部分

1. 分析u盘检材,找到其中保存的加密程序SampleVC.exe,请给出这个exe程序的md5值? (答案格式:c4ca4238a0b923820dcc509a6f75849b)

764789DD9C095D74B6B258CF0F7568B2

在U盘中找到该文件

直接计算md5即可

2. 分析SampleVC.exe,该程序编译的日期可能是什么? (答案格式:2025-06-06)

2026-04-17 13:53:20

将程序导出,用die查看一下

3. 分析SampleVC.exe,正确的密码是什么? (答案格式:abcdefghABCDEFGH)

PleaseRunAsAdmin

放进ida里分析一下,分析一下winmain函数

**这是一个标准的 Windows GUI 程序模板,创建了一个基本的窗口应用程序框架。实际功能在窗口过程函数 **sub_1400024A0中实现,定位该函数

再去sub_140002200函数中获取key与密文

  1. 获取输入 :从两个EDIT控件获取文件路径(hWnd)和密码(hWnd_0)
  2. 长度校验 :密码必须恰好 16个字符
  3. 字符范围校验 :每个字符必须在 0x20~0x7E (可打印ASCII)
  4. 编码转换 :使用代码页 0xFDE9 (UTF-8无BOM)将宽字符转为多字节
  5. AES加密验证 :
  • 初始状态(密钥): [0x67452301, 0xEFCDAB89, 0x67452301, 0xEFCDAB89]
  • 对密码进行 XOR预处理 : password[i] ^= (3*i + 127)
  • 然后进行 AES-128加密
  1. 比较结果 :与硬编码密文比较
import struct
from Crypto.Cipher import AES

ciphertext = struct.pack('<4i', -1401439825, 215362084, -1386142476, 1230097010)
key = struct.pack('<4I', 0x67452301, 0xEFCDAB89, 0x67452301, 0xEFCDAB89)
mask = bytes([(3 * i + 127) & 0xFF for i in range(16)])

decrypted = AES.new(key, AES.MODE_ECB).decrypt(ciphertext)
password = bytes([decrypted[i] ^ mask[i] for i in range(16)])
print(password.decode('ascii'))

4. 分析u盘检材,利用SampleVC.exe解密U盘中被加密的文件,解密后的文件的后缀是什么? (答案格式:exe)

vhd

分析解密函数 sub_140001CF0当密码验证通过后,程序调用 sub_140001CF0(文件路径, 密码) :

  1. 打开加密文件 : wfopen_s(&Stream, lParam, L"rb")
  2. 创建输出文件 :
wcscpy_s(Destination, 0x104u, lParam);
wcscat_s(Destination, 0x104u, L".vhd");  // ← 输出文件后缀为 .vhd
wfopen_s(&Stream_, Destination, L"wb");
  1. RC4解密 :使用密码作为RC4密钥,对文件内容进行流式解密
  2. 调用VHD挂载 : sub_140001760 → sub_1400027B0

分析VHD挂载函数 sub_1400027B0

该函数使用Windows VHD API:

  • OpenVirtualDisk() — 打开VHD文件
  • AttachVirtualDisk() — 挂载为虚拟磁盘
  • GetVirtualDiskPhysicalPath() — 获取物理磁盘路径
  • 挂载成功后删除临时.vhd文件

SampleVC.exe先用RC4解密加密文件,输出为 .vhd 格式的虚拟硬盘文件,然后自动挂载该VHD。

分析完之后,在U盘中导出加密文件进行解密

用管理员模式启动程序

解密成功,可以看到这个文件一闪即逝,这是正常的,因为他挂载成功后会删除临时.vhd文件

5. 分析u盘检材,找到被加密的交易记录,统计李安弘虚拟币收款地址钱包总收款金额为 【参考格式:1.00】

186948.09

因为解密后文件会消失,所以我选择写脚本解密,当然也可以修改程序,但是我不会

import sys

def rc4_init(key_bytes):
    S = list(range(256))
    j = 0
    for i in range(256):
        j = (j + S[i] + key_bytes[i % len(key_bytes)]) % 256
        S[i], S[j] = S[j], S[i]
    return S

def rc4_crypt(data, S):
    output = []
    i = j = 0
    for byte in data:
        i = (i + 1) % 256
        j = (j + S[i]) % 256
        S[i], S[j] = S[j], S[i]
        k = S[(S[i] + S[j]) % 256]
        output.append(byte ^ k)
    return bytes(output)

def decrypt_file(input_path, output_path, password):
    with open(input_path, 'rb') as f:
        encrypted_data = f.read()

    key = password.encode('utf-8')
    S = rc4_init(key)
    decrypted_data = rc4_crypt(encrypted_data, S)

    output_final_path = output_path + '.vhd'
    with open(output_final_path, 'wb') as f:
        f.write(decrypted_data)

    print(f"Decrypted successfully!")
    print(f"Output: {output_final_path}")
    return output_final_path

if __name__ == "__main__":
    if len(sys.argv) != 4:
        print("Usage: python decrypt.py <input_file> <output_file> <password>")
        print("Example: python decrypt.py encrypted_file output PleaseRunAsAdmin")
        sys.exit(1)

    input_file = sys.argv[1]
    output_file = sys.argv[2]
    password = sys.argv[3]

    decrypt_file(input_file, output_file, password)

挂载恢复的磁盘

筛选李安弘虚拟币收款地址对应的收款记录,计算一下即可