案情简介

某日,警方接到举报,举报人称近期互联网上出现了一涉黄网站极为活跃并大肆推广。警方跟进线索分析后,找到相应的网站进行了摸排调查,最终锁定网站的运营者李安弘,警方在对其实施抓捕的现场对电子数据进行了提取固定。

通过对李安弘的审讯,警方了解到,其雇佣了技术人员帮忙架设淫秽视频平台,并找到境外团队对网站进行推广,经过审讯和调查,该嫌疑人还有多种其他违法行为,用以牟利。

请各位参赛选手对检材进行分析,尝试还原整个案件和关键信息。

(本故事纯属虚构,仅为考试设计,可能会出现各检材间时间细节无法对齐现象,请忽略)

容器密码

FIC-{e404d6e66586e9460c23755afab5a872bcf78ab4}

计算机部分

从来没见过的Deepin操作系统,应该是基于linux内核的

好多记录了答案的应用不在桌面上,要在下方任务栏找

有点恶心

1. 分析计算机检材,操作系统版本号为

23.1

2. 分析计算机检材,李安弘曾收到一份免费领取token的邮件的疑似钓鱼邮件,其发送用户邮箱为

hf13338261292@outlook.com

注意不要跟上面那个回复:Token 限时免费领的邮件搞混了

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

136 1281 7854

这种一般都是记录在记事本、便签之类的应用里面,但是找了半天没找到

最后在下方的启动器中找到了语音记事本

4. 分析计算机检材,推广设计图中的apk下载链接为

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

在邮件中发现与推广设计图相关的邮件

在仿真中通过启动器找到邮箱,找到这封邮件

还有3个附件,统统下载下来

看起来推广设计图被加密了,先看一看加密图片查看.html

图片应该是被rsa加密了,那么public.txt应该给出了rsa相关参数

public.txt给出了rsa模数nn:

57751892008149574447756694613209346511056045951970458143905594411398554113111623746466692172544473909892773600617029641656248235151775166339061269972238018743173330948084699695182438765935110193323089354031112350869626121317836465551360104372140181097747761558797918522051881262043738603183528521379831286761

和公钥指数ee:

65537

根据html,我们需要根据nn和ee恢复出私钥指数dd

回顾一下rsa:

  1. 随机选取两个不同的大质数pp和qq
  2. 计算模数nn

n=p×qn=p \times q

  1. 计算欧拉函数

φ(n)=(p−1)×(q−1)\varphi(n)=(p-1)\times(q-1)

  1. 选取公钥指数ee,满足1<e<φ(n)1<e<\varphi(n),且gcd(e,φ(n))=1gcd(e,\varphi(n))=1,即互质,一般取65537
  2. 计算私钥指数dd

e×d≡1(modφ(n))e\times d \equiv 1 \pmod{\varphi(n)}

即dd为ee在模φ(n)\varphi(n)的乘法逆元

d=e−1(modφ(n))d=e^{-1} \pmod{\varphi(n)}

这里已知了nn和ee,就可以尝试分解出pp和qq,计算出φ(n)\varphi(n),进而计算出dd

本题存在的漏洞是pp和qq比较接近,可以用Fermat法分解出来

上脚本

from math import isqrt, gcd
from random import randrange
import time
import sys

# =========================
# RSA 参数
# =========================

n = 57751892008149574447756694613209346511056045951970458143905594411398554113111623746466692172544473909892773600617029641656248235151775166339061269972238018743173330948084699695182438765935110193323089354031112350869626121317836465551360104372140181097747761558797918522051881262043738603183528521379831286761

e = 65537

# =========================================================
# 1. 小素数试除
# =========================================================

def trial_division(n, limit=100000):

    print("[*] Stage 1: small prime trial division")

    for i in range(2, limit):
        if n % i == 0:
            return i, n // i
    return None

# =========================================================
# 2. Fermat 分解(p≈q)
# =========================================================

def fermat_factor(n):

    print("[*] Stage 2: Fermat factorization")

    a = isqrt(n)

    if a * a < n:
        a += 1

    step = 0

    while True:

        b2 = a * a - n
        b = isqrt(b2)

        if b * b == b2:

            p = a - b
            q = a + b

            if p * q == n:
                return p, q

        a += 1
        step += 1

        if step % 200000 == 0:
            print(f"    iter={step}")

# =========================================================
# 3. Pollard Rho(通用)
# =========================================================

def pollard_rho(n):

    print("[*] Stage 3: Pollard Rho")

    if n % 2 == 0:
        return 2

    while True:

        x = randrange(2, n - 1)
        y = x
        c = randrange(1, n - 1)

        d = 1

        while d == 1:

            x = (pow(x, 2, n) + c) % n
            y = (pow(y, 2, n) + c) % n
            y = (pow(y, 2, n) + c) % n

            d = gcd(abs(x - y), n)

        if d != n:
            return d

# =========================================================
# 主流程
# =========================================================

start = time.time()

p = q = None

# -------------------------
# Stage 1
# -------------------------

res = trial_division(n)

if res:
    p, q = res
    print("[+] Found by trial division")

# -------------------------
# Stage 2
# -------------------------

if not p:
    res = fermat_factor(n)

    if res:
        p, q = res
        print("[+] Found by Fermat")

# -------------------------
# Stage 3
# -------------------------

if not p:
    factor = pollard_rho(n)

    p = factor
    q = n // factor

    print("[+] Found by Pollard Rho")

# =========================================================
# 输出 RSA 私钥
# =========================================================

print("\n==============================")

print("[+] p =", p)
print("[+] q =", q)

phi = (p - 1) * (q - 1)

d = pow(e, -1, phi)

print("[+] d =", d)

print("==============================")
print(f"[*] time = {time.time() - start:.2f}s")

'''
输出结果:
[*] Stage 1: small prime trial division
[*] Stage 2: Fermat factorization
[+] Found by Fermat

==============================
[+] p = 7599466560762641743052493422970151283281211357103780410638724588686758080375558505939025432223250000657857469723929623791715127854747607854590078838861499
[+] q = 7599466560762641743052493422970151283281211357103780410638724588686758080375558505939025432223250000657857469723929623791715127854747607854590078838861739
[+] d = 35523357349779905170178187485659935251135867134837314931979222758324282670976025397371947065926313872401811028702468817834912779886756496413632572670718195811151125658055182908097177746721685442556966643339035563906268671458582959248629716030078694677882031903468615733997083390985840508421177858054952995297
==============================
[*] time = 0.02s
'''

用html解密一下

还是张涩图,扫个码就能看到链接

5. 分析计算机检材,李安弘电脑vpn软件开放的代理端口为

9527

在启动器里面找到vpn软件Clash Verge,首页里面就有

设置里面也能看到

在控制中心-网络-系统代理里面也能看到

6. 分析计算机检材,李安弘电脑中AI软件当前使用的模型类型为

OpenRouter

在下方任务栏中找到UOS AI

在设置中能找到模型类型

7. 分析计算机检材,李安弘电脑中AI软件当前使用的模型apiKey为

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

从上一题的图片中我们能看到apiKey,但并不完整

只能尝试找UOS Ai的数据库,搜索一下UOS

最后找到数据库/var/home/lha/.local/share/deepin/uos-ai-assistant/db/basic

在llm表的account_proxy字段找到apiKey

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

beijixin996@tutanota.com

找了半天都没找可以软件,只能猜测藏在被加密的分区里面了

刚好在手机的便签发现了一张图片

其中记录的vc容器密码为9ed2@99y8.com.cn,直接拿去解密被加密的分区

解密完成后看到一个.hidden文件很可疑,看一看是什么

文件中的两个文件202603121310.mp4和get_token_linux,在这个加密分区中都有

其中202603121310.mp4只有声音没有画面,应该已经被勒索软件加密了

看一看get_token_linux

通过文件头可以看出这是一个.elf可执行文件,基本可以确定是勒索软件了

逆向!!!

放到ida里面,找了一圈没什么可用信息,直接来看main_main()函数

// main.main
void __golang main_main()
{
  char v0; // cl
  void *v1; // rax
  string_0 *array; // rax
  int len; // rbx
  uint8 v4[8]; // [rsp+1h] [rbp-65h] BYREF
  unsigned __int64 v5; // [rsp+9h] [rbp-5Dh]
  _QWORD v6[3]; // [rsp+11h] [rbp-55h] BYREF
  uint8 ptr[5]; // [rsp+29h] [rbp-3Dh] BYREF
  int v8; // [rsp+2Eh] [rbp-38h]
  string_0 *v9; // [rsp+36h] [rbp-30h]
  _OWORD a[2]; // [rsp+3Eh] [rbp-28h] BYREF
  retval_4AADE0 v11; // 0:kr20_40.40
  string_0 v12; // 0:rax.8,8:rbx.8
  string_0 v13; // 0:rax.8,8:rbx.8
  io_Writer_0 v14; // 0:rax.8,8:rbx.8
  io_Writer_0 v15; // 0:rax.8,8:rbx.8
  _slice_interface_ v16; // 0:rcx.8,8:rdi.16
  _slice_interface_ v17; // 0:rcx.8,8:rdi.16

  qmemcpy(ptr, "*.mp4", sizeof(ptr));
  v12 = runtime_slicebytetostring(0, ptr, 5);
  v11 = path_filepath_globWithLimit(v12, 0);
  array = v11.matches.array;
  len = v11.matches.len;
  if ( !v11.err.tab && v11.matches.len )
  {
    v0 = 0;
    while ( len > 0 )
    {
      v8 = len;
      v9 = array;
      v0 |= main__a(*array, 0x539u);
      len = v8 - 1;
      array = v9 + 1;
    }
    if ( v0 )
    {
      *(_QWORD *)v4 = 0xAFE886AFE5A3A7E8LL;
      v5 = 0xBBB3E7BBB3E720B7LL;
      qmemcpy(v6, "beijixin996@tutanota.com", sizeof(v6));
      v13 = runtime_slicebytetostring(0, v4, 40);
      a[1] = 0;
      v1 = runtime_convTstring(v13);
      *(_QWORD *)&a[1] = &RTYPE_string;
      *((_QWORD *)&a[1] + 1) = v1;
      v14.data = os_Stdout;
      v14.tab = (internal_abi_ITab *)&go_itab__ptr_os_File_comma_io_Writer;
      v16.array = (interface__0 *)&a[1];
      v16.len = 1;
      v16.cap = 1;
      fmt_Fprint(v14, v16);
      time_Sleep(1000000000);
      *(_QWORD *)&a[0] = &RTYPE_string;
      *((_QWORD *)&a[0] + 1) = &off_4FCB10;
      v15.data = os_Stdout;
      v15.tab = (internal_abi_ITab *)&go_itab__ptr_os_File_comma_io_Writer;
      v17.array = (interface__0 *)a;
      v17.len = 1;
      v17.cap = 1;
      fmt_Fprint(v15, v17);
    }
  }
}

可以看到有个邮箱字符串,应该就是解密服务的联系方式

9.分析计算机检材,李安弘电脑中记录的存放黄金的保险柜编号是

997546

显然这道题需要解密被加密的视频,因此需要进一步逆向分析勒索软件

简要分析一下上面的main_main()函数,程序会搜索电脑中的.mp4视频文件,调用main__a()函数对视频进行加密,跟进main__a()函数

// main._a
bool __golang main__a(string_0 _b, uint32 _c)
{
  int v2; // rax
  int v3; // rsi
  unsigned __int64 v4; // rcx
  int v5; // rdi
  __int128 v6; // kr00_16
  unsigned __int32 v7; // r9d
  unsigned __int32 i; // edx
  unsigned __int32 v10; // r11d
  __int64 v11; // rdx
  unsigned __int64 v12; // r12
  unsigned __int64 v13; // r13
  _DWORD sep[9]; // [rsp+0h] [rbp-24h] BYREF
  _slice_uint8_0 v17; // 0:rdi.24
  _slice_uint8_0 v18; // 0:kr48_24.24
  retval_4A0100 File; // 0:kr20_40.40

  File = os_ReadFile(_b);
  if ( File._r1.tab )
    return 0;
  *(_QWORD *)&sep[5] = File._r0.array;
  *(_QWORD *)&sep[3] = File._r0.cap;
  *(_QWORD *)&sep[1] = File._r0.len;
  sep[0] = 1868788851;
  v17.array = (uint8 *)sep;
  v17.len = 4;
  v17.cap = 4;
  v2 = bytes_Index(File._r0, v17);
  if ( v2 == -1 )
    return 0;
  v4 = v2 + 12;
  v5 = *(_QWORD *)&sep[1];
  if ( *(__int64 *)&sep[1] < v2 + 12 )
    return 0;
  if ( *(_QWORD *)&sep[3] < v4 )
    runtime_panicSliceAcap();
  if ( v4 < v2 + 8 )
    runtime_panicSliceB();
  v6 = *(_OWORD *)&sep[3];
  v7 = _byteswap_ulong(*(_DWORD *)(*(_QWORD *)&sep[5] + ((v2 + 8) & ((8 - (*(_QWORD *)&sep[3] - v2)) >> 63))));
  if ( *(__int64 *)&sep[1] < v2 + 4LL * v7 + 12 )
    return 0;
  for ( i = 0; i < v7; i = v10 + 1 )
  {
    v10 = i;
    v11 = 4 * i;
    v12 = v2 + v11 + 16;
    v13 = v2 + v11 + 12;
    if ( (unsigned __int64)v6 < v12 )
      runtime_panicSliceAcap();
    if ( v12 < v13 )
      runtime_panicSliceB();
    *(_DWORD *)(*((_QWORD *)&v6 + 1) + (v13 & ((12 - ((__int64)v6 - (v2 + v11))) >> 63))) = _byteswap_ulong(
                                                                                              _c
                                                                                            + _byteswap_ulong(*(_DWORD *)(*((_QWORD *)&v6 + 1) + (v13 & ((12 - ((__int64)v6 - (v2 + v11))) >> 63)))));
  }
  v18.array = (uint8 *)*((_QWORD *)&v6 + 1);
  v18.len = v5;
  v18.cap = v3;
  return (unsigned __int64)os_WriteFile(_b, v18, 0x1A4u).tab == 0;
}

加密逻辑很简单,就是对mp4文件的一个“索引表”加上了一个固定偏移0x539(也就是1337),导致播放器去错误的位置读取视频数据

解密只需要把偏移量减去0x539即可

请ai写脚本解密.hidden文件中提到的202603121310.mp4

import struct

  

# ===== 在这里填写被加密的 MP4 文件路径 =====

INPUT_PATH  = "202603121310.mp4"

OUTPUT_PATH = "202603121310_dec.mp4"

# ==========================================

  

OFFSET = 0x539  # 病毒加的偏移量 1337

  

def fix_mp4(input_path, output_path):

    with open(input_path, 'rb') as f:

        data = bytearray(f.read())

  

    pos = data.find(b'stco')

    if pos == -1:

        print("未找到 stco,该文件不是被此病毒加密的 MP4")

        return

  

    entry_count, = struct.unpack('>I', data[pos + 8: pos + 12])

    entries_start = pos + 12

  

    for i in range(entry_count):

        ep = entries_start + i * 4

        old, = struct.unpack('>I', data[ep: ep + 4])

        struct.pack_into('>I', data, ep, (old - OFFSET) & 0xFFFFFFFF)

  

    with open(output_path, 'wb') as f:

        f.write(data)

  

    print(f"解密成功!输出文件:{output_path}")

  

fix_mp4(INPUT_PATH, OUTPUT_PATH)

input("按回车退出...")

解密出来一段ai视频

10.分析计算机检材,李安弘电脑中记录的保险柜密码是

583985

搜索保险,定位到/var/root/文档/zhongyao/保险箱的秘密.et

导出到本地看一看,发现是一个excel表格

只有无意义的点阵,可能又被加密了

在仿真的tool目录下找到了一个疑似用于加密excel的脚本

function GenerateStego() {
    var _0x5e2b = "baidu.com";
    var _0x4c1a = 85; 
    var _0x2f8d = Application.ActiveSheet;
    
    if (_0x2f8d.Shapes.Count > 0) {
        _0x2f8d.Shapes.SelectAll();
        Application.Selection.ShapeRange.Delete();
    }

    var _0x1b4f = _0x3a9c(_0x5e2b);

    for (var _0x1122 = 0; _0x1122 < _0x1b4f.length; _0x1122++) {
        var _0x4455 = _0x1b4f[_0x1122];
        var _0x1a3c = _0x4455.x + 100;
        var _0x1a4d = _0x4455.y + 100;
        var _0x5566 = ((_0x1a3c ^ _0x4c1a) * 1000) + (_0x1a4d ^ _0x4c1a);
        var _0x7788 = Math.random() * 600;
        var _0x8899 = Math.random() * 400;
        var _0x99aa = _0x2f8d.Shapes.AddShape(1, _0x7788, _0x8899, 3, 3);
        _0x99aa.AlternativeText = _0x5566.toString();
        _0x99aa.Fill.ForeColor.RGB = Math.floor(Math.random() * 16777215);
        _0x99aa.Line.Visible = false;
    }
    alert("\u5df2\u5c06 " + _0x5e2b + " \u5236\u4f5c\u4e3a " + _0x1b4f.length + " \u4e2a\u70b9\u9635\u3002");
}

function _0x3a9c(_0xde11) {
    var _0xfe22 = [];
    var _0xcc33 = 25; 
	var _0xfont = {
		'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]]
	};
    for (var i = 0; i < _0xde11.length; i++) {
        var _char = _0xde11[i];
        var _matrix = _0xfont[_char] || [[0,0]];
        for (var j = 0; j < _matrix.length; j++) {
            _0xfe22.push({x: i * _0xcc33 + _matrix[j][0]*4, y: _matrix[j][1]*4});
        }
    }
    return _0xfe22;
}

事实证明这并不是加密而是隐写

脚本里面的baidu.com只是一个演示用例,和解码结果没关系

脚本将原始字符串逐字符渲染为点阵图形,进一步根据坐标编码为一个数字,再在excel表格中创建大量彩色点阵,将数字写进每个点的文字属性中

让ai写个解码脚本

"""

隐写提取脚本 - 对应 GenerateStego() JS 脚本的逆向解码

支持 .et / .xls (OLE2) 和 .xlsx (OOXML) 格式

  

原理:

  - 图形 AlternativeText 存储编码值 = ((x+100) ^ 85) * 1000 + ((y+100) ^ 85)

  - 点坐标按列排布,字符间距 25px,像素缩放 4x

  - 通过点阵字体字典匹配还原原始字符串

"""

  

import sys

import os

  

# ── 目标文件(直接修改这里) ───────────────────────────────────────────────

FILEPATH = r"保险箱的秘密.et"

  

# ── 字体字典(与 JS 完全一致) ──────────────────────────────────────────────

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]],

}

  

XOR_KEY = 85

SPACING = 25  # 字符间距(像素)

SCALE   = 4   # 点阵缩放倍数

  
  

# ── 解码单个 AlternativeText 编码值 → (x, y) ──────────────────────────────

def decode_value(encoded: int):

    x_part = encoded // 1000

    y_part = encoded % 1000

    x = (x_part ^ XOR_KEY) - 100

    y = (y_part ^ XOR_KEY) - 100

    return x, y

  
  

def is_valid_point(x, y):

    """过滤合法的点阵坐标"""

    return x >= 0 and 0 <= y <= 28 and y % SCALE == 0

  
  

# ── 点集 → 字符匹配(F1 分数) ─────────────────────────────────────────────

def match_char(point_set: set):

    best_char, best_f1 = '?', -1.0

    for char, matrix in FONT.items():

        matrix_set = set(map(tuple, matrix))

        overlap    = len(point_set & matrix_set)

        precision  = overlap / len(point_set)   if point_set   else 0

        recall     = overlap / len(matrix_set)  if matrix_set  else 0

        f1 = 2 * precision * recall / (precision + recall) if (precision + recall) else 0

        if f1 > best_f1:

            best_f1, best_char = f1, char

    return best_char, best_f1

  
  

# ── 可视化点阵 ─────────────────────────────────────────────────────────────

def visualize(point_set: set):

    if not point_set:

        return

    max_x = max(p[0] for p in point_set)

    max_y = max(p[1] for p in point_set)

    for row in range(max_y + 1):

        print("    " + "".join("#" if (col, row) in point_set else "." for col in range(max_x + 1)))

  
  

# ══════════════════════════════════════════════════════════════════════════════

# OLE2 格式提取(.et / .xls)

# ══════════════════════════════════════════════════════════════════════════════

def extract_from_ole(filepath: str):

    import struct

    try:

        import olefile

    except ImportError:

        print("[!] 缺少依赖:pip install olefile")

        sys.exit(1)

  

    print(f"[*] 读取 OLE2 文件: {filepath}")

    ole  = olefile.OleFileIO(filepath)

    data = ole.openstream('Workbook').read()

    ole.close()

  

    # 解析 BIFF 记录

    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((rec_type, rec_data))

        i += 4 + rec_len

        if rec_len == 0 and rec_type == 0:

            i += 1

  

    mso_records = [d for t, d in records if t == 0x00EC]

    print(f"[*] 找到 {len(mso_records)} 条 MSODRAWING 记录")

  

    alt_texts = []

    for rec_data in mso_records:

        # 尝试 UTF-16LE

        j = 0

        while j < len(rec_data) - 1:

            code = struct.unpack('<H', rec_data[j:j+2])[0]

            if 0x30 <= code <= 0x39:

                num_str, k = "", j

                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:

                    alt_texts.append(num_str)

                j = k

            else:

                j += 2

  

        # 回退 ANSI

        if not alt_texts:

            j = 0

            while j < len(rec_data):

                if 0x30 <= rec_data[j] <= 0x39:

                    num_str, k = "", j

                    while k < len(rec_data) and 0x30 <= rec_data[k] <= 0x39:

                        num_str += chr(rec_data[k]); k += 1

                    if len(num_str) >= 4:

                        alt_texts.append(num_str)

                    j = k

                else:

                    j += 1

  

    return alt_texts

  
  

# ══════════════════════════════════════════════════════════════════════════════

# OOXML 格式提取(.xlsx)

# ══════════════════════════════════════════════════════════════════════════════

def extract_from_xlsx(filepath: str):

    import zipfile, re

    print(f"[*] 读取 XLSX 文件: {filepath}")

  

    alt_texts = []

    with zipfile.ZipFile(filepath, 'r') as z:

        # 遍历所有工作表的 drawing XML

        drawing_files = [n for n in z.namelist() if 'drawing' in n and n.endswith('.xml')]

        print(f"[*] 找到 {len(drawing_files)} 个 drawing 文件")

        for df in drawing_files:

            xml = z.read(df).decode('utf-8', errors='ignore')

            # 提取 descr 属性(AlternativeText 在 OOXML 中对应 descr)

            matches = re.findall(r'descr=["\']([^"\']+)["\']', xml)

            for m in matches:

                if m.isdigit() and len(m) >= 4:

                    alt_texts.append(m)

  

    return alt_texts

  
  

# ══════════════════════════════════════════════════════════════════════════════

# 主流程

# ══════════════════════════════════════════════════════════════════════════════

def decode(filepath: str):

    ext = os.path.splitext(filepath)[1].lower()

  

    if ext in ('.et', '.xls'):

        raw_values = extract_from_ole(filepath)

    elif ext == '.xlsx':

        raw_values = extract_from_xlsx(filepath)

    else:

        print(f"[!] 不支持的文件格式: {ext}(支持 .et / .xls / .xlsx)")

        return ""

  

    print(f"[*] 提取到 {len(raw_values)} 个原始编码值")

  

    # 解码坐标 & 过滤

    valid_points = []

    for num_str in raw_values:

        try:

            x, y = decode_value(int(num_str))

            if is_valid_point(x, y):

                valid_points.append((x, y))

        except ValueError:

            pass

  

    print(f"[*] 有效点数: {len(valid_points)}")

  

    if not valid_points:

        print("[!] 未找到有效点,请检查文件或参数")

        return ""

  

    # 按字符列分组

    char_points: dict[int, set] = {}

    for x, y in valid_points:

        char_idx = x // SPACING

        local_x  = (x % SPACING) // SCALE

        local_y  = y // SCALE

        char_points.setdefault(char_idx, set()).add((local_x, local_y))

  

    print(f"[*] 识别到 {len(char_points)} 个字符位置\n")

  

    # 逐字符匹配,始终显示点阵

    result = ""

    for idx in sorted(char_points.keys()):

        pts = char_points[idx]

        char, f1 = match_char(pts)

        result += char

        print(f"  [字符 {idx:02d}] 匹配='{char}'  F1={f1:.2f}  点数={len(pts)}")

        visualize(pts)

        print()

  

    print("=" * 50)

    print(f"  解码结果: {result}")

    print("=" * 50)

    return result

  
  

if __name__ == "__main__":

    if not os.path.exists(FILEPATH):

        print(f"[!] 文件不存在: {FILEPATH}")

        print( "    请把文件放在脚本同目录,或修改顶部 FILEPATH 为完整路径")

    else:

        decode(FILEPATH)
'''
输出内容:
[*] 读取 OLE2 文件: 保险箱的秘密.et
[*] 找到 171 条 MSODRAWING 记录
[*] 提取到 171 个原始编码值
[*] 有效点数: 171
[*] 识别到 21 个字符位置

  [字符 00] 匹配='b'  F1=1.00  点数=10
    #...
    #...
    ###.
    #..#
    ###.

  [字符 01] 匹配='a'  F1=1.00  点数=8
    ....
    ....
    .###
    #..#
    .###

  [字符 02] 匹配='o'  F1=1.00  点数=6
    ....
    .##.
    #..#
    .##.

  [字符 03] 匹配='x'  F1=1.00  点数=9
    #...#
    .#.#.
    ..#..
    .#.#.
    #...#

  [字符 04] 匹配='i'  F1=1.00  点数=4
    .#
    ..
    .#
    .#
    .#

  [字符 05] 匹配='a'  F1=1.00  点数=8
    ....
    ....
    .###
    #..#
    .###

  [字符 06] 匹配='n'  F1=1.00  点数=9
    .##.
    #...
    #..#
    #..#
    #..#

  [字符 07] 匹配='g'  F1=1.00  点数=11
    ....
    ....
    .###
    #..#
    .###
    ...#
    .##.

  [字符 08] 匹配='u'  F1=1.00  点数=7
    ....
    ....
    #..#
    #..#
    .###

  [字符 09] 匹配='i'  F1=1.00  点数=4
    .#
    ..
    .#
    .#
    .#

  [字符 10] 匹配='m'  F1=1.00  点数=10
    .....
    #####
    #.#.#
    #...#

  [字符 11] 匹配='i'  F1=1.00  点数=4
    .#
    ..
    .#
    .#
    .#

  [字符 12] 匹配='m'  F1=1.00  点数=10
    .....
    #####
    #.#.#
    #...#

  [字符 13] 匹配='a'  F1=1.00  点数=8
    ....
    ....
    .###
    #..#
    .###

  [字符 14] 匹配=':'  F1=1.00  点数=2
    ..
    .#
    ..
    .#

  [字符 15] 匹配='5'  F1=1.00  点数=11
    ###.
    #...
    ###.
    ...#
    ###.

  [字符 16] 匹配='8'  F1=1.00  点数=10
    .##.
    #..#
    .##.
    #..#
    .##.

  [字符 17] 匹配='3'  F1=1.00  点数=10
    ###.
    ...#
    .##.
    ...#
    ###.

  [字符 18] 匹配='9'  F1=1.00  点数=9
    .##.
    #..#
    .###
    ...#
    ..#.

  [字符 19] 匹配='8'  F1=1.00  点数=10
    .##.
    #..#
    .##.
    #..#
    .##.

  [字符 20] 匹配='5'  F1=1.00  点数=11
    ###.
    #...
    ###.
    ...#
    ###.

==================================================
  解码结果: baoxianguimima:583985
==================================================
'''

保险柜密码是583985

服务器部分

服务器检材有两个镜像文件

开始以为是服务器集群,结果放到取证工具里啥也识别不出

还以为需要raid重组,后来发现好像可以直接仿真

将两个镜像依次导入即可

登录进来发现有图形界面

如果上来直接做题是不对的,因为现在在docker容器里面

如何判断当前是否在docker容器中

cat /proc/1/cgroup
  • 在容器中,会看到docker或kubepods等字样
  • 不在容器中,会看到唯一的0::/或1:name=systemd:/init.scope、system.slice、user.slice等字样

显然这个图形化界面在docker容器中

回到宿主机的方式为Ctrl+Alt+F2,如果要切回容器内的图形化界面的话就是Ctrl+Alt+F1

尝试ssh连接的时候发现没有安装ssh服务,手动安装一下,再修改一下配置文件/etc/ssh/sshd_config就可以连接了

1. 该服务器主机操作系统版本为

Debian GNU/Linux 13 (trixie)

cat /etc/os-release

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

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

lsblk -f

由于问的是根分区,因此我们关注挂载位置,也就是MOUNTPOINTS字段为/的硬盘

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

2026-04-16T07:15:50.535713491Z

查看镜像信息

docker images

发现最新的镜像为u22,但没有显示时间信息

docker inspect u22

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

root/history

在第2题中我们可以确定根分区类型为btrfs,使用命令

btrfs subvolume list /

@rootfs是当前系统真正的根分区子卷,root/history就是根分区快照路径

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

user.php

网站根目录为/var/www/html/maccms10

查看目录下的文件user.php

猜测本文件原本就是后台入口文件admin.php,后来被改名为user.php

这里发现网站已经可以打开了,那么拼接/user.php试一试

发现确实是后台管理入口

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

icp1919810

icp备案号一般都在前端页面中,查看index.html

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

www.2026fic.forensix

还是在上一题的前端页面index.html中搜索

也可以查看网站的配置文件./application/extra/maccms.php(不知道怎么找到的)

答案就在site_url字段

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

sipaanshe

这一题需要连接数据库,在第11题中,我们已经得到了连接数据库需要的必要信息

  • 数据库类型:mysql
  • ip:10.0.3.100
  • 端口:3306
  • 用户名:aa
  • 密码:123456

要走ssh隧道

打开网站,看一看分类3有什么

根据视频的名字,再查看mac2数据库中的mac_vod表就能找到答案

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

在配置文件中过滤模板的英文’template’

找到模板的目录为001tep

进入./template/001tep,发现文件info.ini,查看一下

可以看出这就是前端设计的模板

10. 该网站的伪静态规则配置文件sm3值为

e73407468e6f52af54c7b14632eeeb9be25b05106d06c4c3085fc843c223793f

进入目录./说明文档/伪静态规则,其中有一个rewrite说明.txt文件

rewrite = url重写作用:把动态网址伪装成静态网址,浏览器看着是静态 html,服务器实际跑的是 PHP/Java 等动态程序

明显是和伪静态相关的,打开看看

从中可以看出网站使用的是nginx,伪静态规则存路径为/etc/nginx/sites-enabled/default

直接计算sm3即可

openssl sm3 /etc/nginx/sites-enabled/default

11. 该网站关联的数据库的ip地址为

10.0.3.100

数据库关系到后端,查看application文件夹,其中有个database.php,显然和数据库有关,看一看

数据库地址只给了域名,并没有给ip

我们可以查看hosts文件(hosts文件实现域名到ip的本地映射)

从文件中我们还能看出数据库的端口为3306,看一看服务器监听的端口

ss -tulnp

显然没有3306端口

如果数据库安装在宿主机上,地址一定是127.0.0.1或localhost,而不是mytidb,因此数据库可能在容器里面

看一看服务器安装了哪些容器服务

ps aux | grep -E "dockerd|lxd|lxcfs|containerd|podman|crio|kata|firecracker|runc" | grep -v grep
#能够涵盖大部分的容器服务

回显

root        1004  0.0  0.0 453988  2692 ?        Ssl  06:37   0:02 /usr/bin/lxcfs /var/lib/lxcfs                                                                                                                                                            
root        1135  0.0  0.6 1798704 24404 ?       Ssl  06:37   0:02 /usr/bin/containerd                                                                                                                                                                      
root        1587  0.0  0.9 1989052 39660 ?       Ssl  06:37   0:02 /usr/sbin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock                                                                                                                  
root        2157  0.0  0.2 2133472 9972 ?        Sl   06:37   0:02 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 1c79e988f43b73c0fecead1b143d67864fd4883e82fa65aeebde04b814a9dd7c -address /run/containerd/containerd.sock
进程作用
/var/lib/lxcfsLXC容器服务
/usr/sbin/dockerddocker容器服务
/usr/bin/containerd底层运行时,各种容器服务运行的依赖
/usr/bin/containerd-shim-runc-v2容器执行器,用于启动容器进程

这个服务器有docker和LXC两个容器服务,分别看一下

可以确定数据库在LXC容器中,查看详细信息

lxc-info -n mytidb

还有一种方案,就是看arp

arp -n

同样能找到ip

12. 该网站数据库使用了哪一类容器技术

LXC

见上一题的分析

13. 运行在4000端口的备份数据库版本号为

8.0.11-TiDB-v7.5.0

在navicat中切换4000端口连接(其他保持不变)

14. 新注册用户数量最多的日期为

2026-04-15

在mac_user表中

user_reg_time字段就是用户注册时间戳,将该字段转换为标准日期并进行聚合,并按人数降序排列

SELECT 
    DATE(FROM_UNIXTIME(user_reg_time)) AS reg_date, 
    COUNT(*) AS user_count 
FROM mac_user 
WHERE user_reg_time > 0 
GROUP BY reg_date 
ORDER BY user_count DESC 
LIMIT 1;

15. 马慧美最后一次登录该网站的ip为

51.43.21.163

看一下mac_user表的user_name字段,大致看一下名字的存储方式

user_last_login_ip字段存储用户登录该网站的ip

筛选一下”Ma Hui Mei”,用inet_ntoa()函数将整数形式的ip转换为点分十进制

SELECT user_name, inet_ntoa(user_last_login_ip) FROM `mac_user` where user_name = 'Ma Hui Mei';

16. 以下哪个文件系统未被使用
A.ntfs B.btrfs C.xfs D.Lvm

AC

使用命令

lsblk -f

查看系统所有磁盘、分区,并显示文件系统类型、uuid和挂载点

关注FSTYPE字段,btrfs和Lvm均出现了,而ntfs和xfs没有出现

17. 该服务器安装了以下那些数据库服务
A.mysql B.GuessDB C.tidb D.postgresql E.Mariadb

ACD

直接看系统中正在运行的进程,针对字符串过滤

root@ubuntu:/var/www/html/maccms10# ps -ef | grep -E 'mysql|mariadb|postgres|tidb|guessdb'                                                                                                             
root        1321       1  0 May07 ?        00:00:00 [lxc monitor] /var/lib/lxc mytidb                                                  

postgres    1561       1  0 May07 ?        00:00:00 /usr/lib/postgresql/17/bin/postgres -D /var/lib/postgresql/17/main -c config_file=/etc/postgresql/17/main/postgresql.conf 

postgres    1562    1561  0 May07 ?        00:00:00 postgres: 17/main: checkpointer                                      

postgres    1563    1561  0 May07 ?        00:00:00 postgres: 17/main: background writer                                   

postgres    1580    1561  0 May07 ?        00:00:00 postgres: 17/main: walwriter                                      

postgres    1581    1561  0 May07 ?        00:00:00 postgres: 17/main: autovacuum launcher                           

postgres    1582    1561  0 May07 ?        00:00:00 postgres: 17/main: logical replication launcher                         

postgres    1750    1322  0 May07 ?        00:00:00 /lib/systemd/systemd-resolved                             

106         1904    1322  0 May07 ?        00:03:55 /usr/sbin/mysqld                                             
root        2444    1906  2 May07 ?        00:37:25 /root/.tiup/components/tidb/v7.5.0/tidb-server -P 4000 --store=tikv --host=0.0.0.0 --status=10080 --path=10.0.3.100:2379 --log-file=/root/.tiup/data/aamac/tidb-0/tidb.log --config=/root/.tiup/data/aamac/tidb-0/tidb.toml             

root       25308    4065  0 03:25 pts/0    00:00:00 grep -E mysql|mariadb|postgres|tidb|guessdb  

root@ubuntu:/var/www/html/maccms10#

最后一个进程是grep本身进程,可忽略

其他进程中出现了mysql、tidb和postgresql

互联网部分

1. 售卖卡密的公开群组ID为

FIC_2026

依然是看配置文件./application/extra/maccms.php

2. 备份数据库中视频图片的文件名为

7b3fdd9d464ce48e7f20cd45f918c9a6.jpg

在服务器部分的第13题我们已知端口为4000的数据库就是备份数据库

再次连接这个数据库,查看mac数据库的mac_vod表

只有1条记录,vod_pic字段就是答案

3. ngrok提供的域名为

blemish-junior-unengaged.ngrok-free.dev

ngrok是一种内网穿透工具,可以把本机上的服务,通过一个公网域名临时暴露到互联网

我们这里直接在网站根目录下启动ngrok

ngrok http 80

上面命令的功能是将本地的80端口(可以任意选取)映射到一个公网地址

域名就是blemish-junior-unengaged.ngrok-free.dev

二进制程序部分

二进制程序藏在u盘里

1. 分析u盘检材,找到其中保存的加密程序SampleVC.exe,请给出这个exe程序的md5值?

764789dd9c095d74b6b258cf0f7568b2

导出计算下哈希

2. 分析SampleVC.exe,该程序编译的日期可能是什么?

2026-04-17

编译日期可以用die查看

编译日期就在时间戳字段

3. 分析SampleVC.exe,正确的密码是什么?

PleaseRunAsAdmin

先直接运行看看

结合程序名称里面的VC不难猜测这应该是一个解密加密容器的工具,只有当用户输入了正确的密码才予以解密

接下来就是喜闻乐见的逆向环节,请出ida

无论是字符串定位还是从main()函数入手都没办法丝滑地定位到程序的主要逻辑,这里本人是接mcp分析得到程序主要逻辑在sub_140002200()函数

__int64 sub_140002200()
{
  unsigned __int64 v0; // rax
  __int64 v1; // rcx
  const WCHAR *v2; // rdx
  int v3; // r8d
  LPARAM *v4; // rdx
  CHAR *lpMultiByteStr; // rbx
  _OWORD *v6; // rax
  _DWORD v8[4]; // [rsp+40h] [rbp-C0h] BYREF
  char v9; // [rsp+50h] [rbp-B0h]
  char Str1[4]; // [rsp+58h] [rbp-A8h] BYREF
  int v11; // [rsp+5Ch] [rbp-A4h]
  int v12; // [rsp+60h] [rbp-A0h]
  int v13; // [rsp+64h] [rbp-9Ch]
  char v14; // [rsp+68h] [rbp-98h]
  LPARAM v15[2]; // [rsp+70h] [rbp-90h] BYREF
  __int128 v16; // [rsp+80h] [rbp-80h]
  __int128 v17; // [rsp+90h] [rbp-70h]
  __int128 v18; // [rsp+A0h] [rbp-60h]
  char Str2[16]; // [rsp+B0h] [rbp-50h] BYREF
  __int128 v20; // [rsp+C0h] [rbp-40h]
  __int128 v21; // [rsp+D0h] [rbp-30h]
  __int128 v22; // [rsp+E0h] [rbp-20h]
  __int128 v23; // [rsp+F0h] [rbp-10h]
  __int128 v24; // [rsp+100h] [rbp+0h]
  int v25; // [rsp+110h] [rbp+10h]
  wchar_t lParam[256]; // [rsp+120h] [rbp+20h] BYREF

  memset(lParam, 0, sizeof(lParam));
  *(_OWORD *)v15 = 0;
  v16 = 0;
  v17 = 0;
  v18 = 0;
  SendMessageW(hWnd, 0xDu, 0x200u, (LPARAM)lParam);
  SendMessageW(qword_140008798, 0xDu, 0x40u, (LPARAM)v15);
  v0 = -1;
  do
    ++v0;
  while ( lParam[v0] );
  if ( !v0 || v0 > 0xFF )
  {
    v2 = &word_1400056A8;
    goto LABEL_19;
  }
  v1 = -1;
  do
    ++v1;
  while ( *((_WORD *)v15 + v1) );
  if ( v1 != 16 )
  {
    v2 = (const WCHAR *)&unk_1400056B8;
LABEL_19:
    MessageBoxW(0, v2, &word_14000569C, 0);
    return 0;
  }
  v3 = 0;
  v4 = v15;
  do
  {
    if ( (unsigned __int16)(*(_WORD *)v4 - 32) > 0x5Eu )
    {
      v2 = (const WCHAR *)&unk_1400056C8;
      goto LABEL_19;
    }
    ++v3;
    v4 = (LPARAM *)((char *)v4 + 2);
  }
  while ( (unsigned __int64)v3 < 0x10 );
  lpMultiByteStr = (CHAR *)operator new(0x10u);
  WideCharToMultiByte(0xFDE9u, 0, (LPCWCH)v15, -1, lpMultiByteStr, 16, 0, 0);
  v8[0] = 1732584193;
  v8[1] = -271733879;
  v25 = 0;
  *(_OWORD *)Str2 = 0;
  v8[2] = 1732584193;
  v20 = 0;
  v8[3] = -271733879;
  v21 = 0;
  v9 = 0;
  v22 = 0;
  v23 = 0;
  v24 = 0;
  v6 = operator new(0x11u);
  *v6 = *(_OWORD *)lpMultiByteStr;
  sub_140001230(Str2, v6, v8);
  *(_DWORD *)Str1 = -1401439825;
  v11 = 215362084;
  v12 = -1386142476;
  v13 = 1230097010;
  v14 = 0;
  if ( !strncmp(Str1, Str2, 0x10u) )
  {
    if ( (unsigned int)sub_140001CF0(lParam, (__int64)lpMultiByteStr) == 1 )
      MessageBoxW(0, &Text, &Caption, 0);
    else
      MessageBoxW(0, &word_1400056F0, &Caption, 0);
  }
  else
  {
    MessageBoxW(0, &word_140005700, &word_14000569C, 0);
  }
  return 0;
}

先看到最后的if校验处

一看就是一道经典的ctf逆向题,输入值经过加密后得到ciper与一个内置的target值作比较

根据最后的0x10u可以猜测输入的密码长度为16,但是目前Str1和Str2都还没有被赋值,无法确定哪个是ciper哪个是target,动调一下

两次动调输入不同的值,发现Str2会随输入变化而变化,而Str1的值始终为AF B9 77 AC 24 2A D6 0C F4 24 61 AD 72 CA 51 49

因此Str2就是ciper,Str1是target,那么上面那个调用了Str2的sub_140001230()函数就是加密函数了

sub_140001230()函数还调用了v6和v8,v8在上面有赋值,猜测就是key

现在现在密文和key都已经拿到,分析加密算法即可,跟进分析sub_140001230()函数

大致分析一下可以确定加密算法

  1. 对输入进行逐字节异或
for i in range(16):
	p[i] ^= (3*i + 127)
  1. 标准AES-128 ECB加密(其中调用sub_140001010()函数负责轮密钥异或)

接下来就可以写脚本解密了

from Crypto.Cipher import AES

# key
key = bytes.fromhex("0123456789ABCDEF0123456789ABCDEF")

# 密文
ciphertext = bytes.fromhex("AFB977AC242AD60CF42461AD72CA5149")

# AES-128 ECB 解密
cipher = AES.new(key, AES.MODE_ECB)
data = cipher.decrypt(ciphertext)

# 还原 XOR 扰动
plain = bytearray(data)
for i in range(16):
    plain[i] ^= (3*i + 127) & 0xFF

print(plain)
print(plain.decode(errors="ignore"))

'''
输出内容:
bytearray(b'PleaseRunAsAdmin')
PleaseRunAsAdmin
'''

拿到密码PleaseRunAsAdmin

密码似乎在提示我们需要以管理员身份运行解密程序

4. 分析u盘检材,利用SampleVC1234.exe解密U盘中被加密的文件,解密后的文件的后缀是什么?

.vhd

u盘中存在一个名为vc的文件,猜测就是加密容器

解密并不容易,经过多次实验发现成功解密需要两个条件

  • SampleVC.exe需要以管理员身份运行
  • 文件存放路径要有中文(对你没有听错)

满足以上两个条件可以成功解密

虽然提示挂载失败,但是生成了一个vc.vhd磁盘映像文件

如果存储路径全英文则会提示挂载成功,但是vc.vhd生成一下就消失了,如下图

应该是出题人的失误,不管了反正本题答案是.vhd

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

186948.09

直接双击vc.vhd可以看到里面有一个.xlsx表格

记录了几个虚拟币地址的交易流水,直接截图丢给ai写一个excel函数或py脚本计算收款金额即可,这里本人选择的是excel函数