FIC 2026 团队赛

2026FIC取证决赛wp(手机取证)

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

上传者:玫幽倩 发布日期:2026-05-26 93 次阅读

手机取证

这套题的手机取证真是见过最难的了,感谢羊羊羊和mumuzi的博客

https://mumuzi.blog/docs/Forensic/%E5%8F%96%E8%AF%81%E6%AF%94%E8%B5%9BWP/07-2026%E7%AC%AC%E5%85%AD%E5%B1%8AFIC%E5%86%B3%E8%B5%9B/%E6%89%8B%E6%9C%BA%E5%8F%96%E8%AF%81
​
https://mp.weixin.qq.com/s/S_n3EwT7oeIgSMCLlJm70w

其实这边直接解压之后再用文件集合导入火眼比较好,不会出现那么多data文件夹,不过我这边还是以普通的直接导入镜像来写好了

1. 分析手机检材,该手机设备名称为

第一题就来个下马威了

image.png

看起来火眼这边分析出来的是Xbox Wireless Controller

这明显不是手机设备的名称,因为这是Xbox无线控制器的意思,是手柄的名字 image.png

可以看到火眼这边解析的是蓝牙属性,很奇怪,因此这边都不太对

我们需要去找手机设置的路径,可以通过爆搜如device_name等内容找一下

在/data/system/users/0/settings_global.xml我们可以看到真正的设备名称 image.png

得到设备名称为REDMAGIC 9 Pro+

比赛的时候没找到这块,做不出去手机相册闲逛了 image.png

能看见屏幕截图,正好截图截到了本机的设备名称

2. 分析手机检材,该手机系统magisk【环境版本】为

image.png

参考格式是26000,所以明显不该是这边的30.7

搜索magisk image.png

发现了adb中存在记录,过去看看 image.png

得到最终答案,环境版本为30600

3. 分析手机检材,嫌疑人通过盖世游戏app安装的《最终幻想》游戏版本是

image.png

直接搜索盖世游戏,发现包名为com.xiaoji.egggame image.png

搜索确定路径

进行爆搜,这个路径没有很多文件,所以不管是手动搜索还是爆搜都可以搜到 image.png

通过对fantasy进行的搜索,我们最终在data_15/media/0/Android/data/com.xiaoji.egggame/files/Documents/XiaoKunLogcat/XiaoKunLogInfo-5.txt

找到了下的是版本号XIII的最终幻想

4. 分析手机检材,5月6日,嫌疑人最后一次使用谷歌套件中的某个app,其包名是

image.png

根据题目说法,将最后使用时间过滤为2026-05-06

再过滤包名含有google

只剩两个了

根据下一题,这个app是会推送新闻的

因此确定是com.google.android.googlequicksearchbox

因为另一个是Googleplay服务,不会推新闻,一般都是推送基础服务,而这一个搜索的更会推新闻一点

如果不放心我们可以由下一题进行验证(虽然比赛的时候根本就没做出第五题

5. 分析上述app5月6日推送新闻的相关痕迹和缓存,新闻《男子拾获钱包以为天降横财》中事件发生的地点是

image.png

先根据包名搜索确定路径 image.png

题目说了根据痕迹和缓存,明显暗示我们去cache文件夹看看 image.png

发现有一个叫做image的文件夹,很显然在这里边

遍历一下就能看到本题答案了 image.png

地址是柔佛麻坡

6. 分析该手机关机信息情况,最近一次因电池电池异常过热导致关机的北京时间为

关机信息情况,直接搜索shutdown看看 image.png

搜索shutdown发现了一个记录关机的文件夹 image.png

里边将关机时间都作为时间戳写入了文件名,只需看内容找电池过热即可 image.png

寻找一番,发现是这个文件,里边明确写了

BatteryService.lambda$shutdownIfOverTempLocked
Shutdown request from SYSTEM for reason thermal,battery at 2026-01-21 17:28:02.432 GMT+08:00

即电池异常过热

**所以答案的时间就是时间戳转化的2026-01-21 17:28:02 ** image.png

或者文件里边其实也写了这个时间,不用转化直接复制粘贴也一样

7. 分析手机检材,北京时间2026-05-06 10:43:38左右那些应用的通知被查看了?

这题真的很难,比赛的时候几乎毫无思路,跟着mumuzi老师走一下

这边需要我们查看使用统计数据库

在/data/system_ce/0/usagestats image.png

这四个是 IntervalStatsObfuscatedProto 文件

即UsageStats应用使用统计文件,文件名的时间戳其实是统计区间的一个开始时间(是一个区间 image.png

题目说的时间差不多是1778035418000

所以说左右,最接近的就是第一个文件1778031702128,应该会包含到题目的时间那个区间

接下来就是对里边内容进行了解

这些文件,明文看什么都看看不出,因为他们把很多内容都是保存成了数字token,而映射表是mappings文件 image.png

就在同路径下

因此我们可以写脚本读取,整合为一个csv文件,把内容全部提取

#!/usr/bin/env python3
from pathlib import Path
from datetime import datetime, timezone, timedelta
import argparse, csv, sys
​
EVENT_TYPES = {
    0:'NONE',1:'ACTIVITY_RESUMED',2:'ACTIVITY_PAUSED',3:'END_OF_DAY',4:'CONTINUE_PREVIOUS_DAY',
    5:'CONFIGURATION_CHANGE',6:'SYSTEM_INTERACTION',7:'USER_INTERACTION',8:'SHORTCUT_INVOCATION',
    9:'CHOOSER_ACTION',10:'NOTIFICATION_SEEN',11:'STANDBY_BUCKET_CHANGED',12:'NOTIFICATION_INTERRUPTION',
    13:'SLICE_PINNED_PRIV',14:'SLICE_PINNED',15:'SCREEN_INTERACTIVE',16:'SCREEN_NON_INTERACTIVE',
    17:'KEYGUARD_SHOWN',18:'KEYGUARD_HIDDEN',19:'FOREGROUND_SERVICE_START',20:'FOREGROUND_SERVICE_STOP',
    21:'CONTINUING_FOREGROUND_SERVICE',22:'ROLLOVER_FOREGROUND_SERVICE',23:'ACTIVITY_STOPPED',24:'ACTIVITY_DESTROYED',
    25:'FLUSH_TO_DISK',26:'DEVICE_SHUTDOWN',27:'DEVICE_STARTUP',28:'USER_UNLOCKED',29:'USER_STOPPED',
    30:'LOCUS_ID_SET',31:'APP_COMPONENT_USED',
}
​
def read_varint(data, i):
    shift=0; result=0
    while True:
        if i>=len(data):
            raise EOFError('truncated varint')
        b=data[i]; i+=1
        result |= (b & 0x7f) << shift
        if not (b & 0x80):
            return result, i
        shift += 7
        if shift > 70:
            raise ValueError('varint too long')
​
def skip_value(data, i, wire):
    if wire == 0:
        _, i = read_varint(data, i)
        return i
    if wire == 1:
        return i+8
    if wire == 2:
        n, i = read_varint(data, i)
        return i+n
    if wire == 5:
        return i+4
    raise ValueError(f'unsupported wire type {wire}')
​
def iter_fields(data):
    i=0
    while i < len(data):
        key, i = read_varint(data, i)
        field = key >> 3
        wire = key & 7
        val_start = i
        if wire == 0:
            value, i = read_varint(data, i)
            yield field, wire, value
        elif wire == 1:
            value = data[i:i+8]; i += 8; yield field, wire, value
        elif wire == 2:
            n, i = read_varint(data, i)
            value = data[i:i+n]; i += n; yield field, wire, value
        elif wire == 5:
            value = data[i:i+4]; i += 4; yield field, wire, value
        else:
            raise ValueError(f'bad wire {wire} at {val_start}')
​
def parse_event(msg):
    e={}
    for f,w,v in iter_fields(msg):
        if w != 0:
            continue
        if f == 1: e['package_token'] = v
        elif f == 2: e['class_token'] = v
        elif f == 3: e['time_ms'] = v
        elif f == 4: e['flags'] = v
        elif f == 5: e['type'] = v
        elif f == 7: e['shortcut_id_token'] = v
        elif f == 8: e['standby_bucket'] = v
        elif f == 9: e['notification_channel_id_token'] = v
        elif f == 10: e['instance_id'] = v
        elif f == 11: e['task_root_package_token'] = v
        elif f == 12: e['task_root_class_token'] = v
        elif f == 13: e['locus_id_token'] = v
    return e
​
def parse_interval(path):
    data=Path(path).read_bytes()
    out={'events':[], 'end_time_ms':None, 'major_version':None, 'minor_version':None, 'packages':0, 'configurations':0}
    for f,w,v in iter_fields(data):
        if f == 1 and w == 0: out['end_time_ms'] = v
        elif f == 2 and w == 0: out['major_version'] = v
        elif f == 3 and w == 0: out['minor_version'] = v
        elif f == 20 and w == 2: out['packages'] += 1
        elif f == 21 and w == 2: out['configurations'] += 1
        elif f == 22 and w == 2: out['events'].append(parse_event(v))
    return out
​
def parse_mappings(path):
    """Return {package_token: [package_name, string1, string2, ...]}"""
    if not path:
        return {}
    p=Path(path)
    if not p.exists():
        return {}
    data=p.read_bytes()
    maps={}
    for f,w,v in iter_fields(data):
        if f == 2 and w == 2:
            pkg_token=None; strings=[]
            for sf,sw,sv in iter_fields(v):
                if sf == 1 and sw == 0:
                    pkg_token=sv
                elif sf == 2 and sw == 2:
                    try:
                        strings.append(sv.decode('utf-8','replace'))
                    except Exception:
                        strings.append(repr(sv))
            if pkg_token is not None:
                maps[pkg_token]=strings
    return maps
​
def ts(ms, tz_hours=8):
    return datetime.fromtimestamp(ms/1000, tz=timezone(timedelta(hours=tz_hours))).strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
​
def token_name(maps, package_token, string_token=None):
    arr = maps.get(package_token)
    if not arr:
        return f'token:{package_token}' if package_token is not None else ''
    if string_token is None:
        return arr[0] if arr else f'token:{package_token}'
    if 0 <= string_token < len(arr):
        return arr[string_token]
    return f'class_token:{string_token}'
​
def main():
    ap=argparse.ArgumentParser(description='Parse Android /data/system/usagestats/0/daily/* protobuf files')
    ap.add_argument('files', nargs='+', help='daily/weekly/monthly/yearly usage stats files')
    ap.add_argument('-m','--mappings', help='optional /data/system/usagestats/0/mappings file')
    ap.add_argument('--tz', type=int, default=8, help='timezone offset hours, default +8 Beijing')
    ap.add_argument('--csv', dest='csv_path', help='write full event list to CSV')
    ap.add_argument('--only-shutdown', action='store_true', help='print only DEVICE_SHUTDOWN/DEVICE_STARTUP')
    args=ap.parse_args()
    maps=parse_mappings(args.mappings)
    rows=[]
    for fp in args.files:
        p=Path(fp)
        begin_ms=int(p.name.split('.')[0])
        st=parse_interval(p)
        end_abs=begin_ms + (st['end_time_ms'] or 0)
        print(f'\n== {p.name} ==')
        print(f'begin: {ts(begin_ms,args.tz)}  end: {ts(end_abs,args.tz)}  events: {len(st["events"])}  packages: {st["packages"]}  configs: {st["configurations"]}  version: {st["major_version"]}.{st["minor_version"]}')
        for idx,e in enumerate(st['events']):
            typ=e.get('type')
            if args.only_shutdown and typ not in (26,27):
                continue
            abs_ms = begin_ms + e.get('time_ms',0)
            pkg_token=e.get('package_token')
            cls_token=e.get('class_token')
            pkg = token_name(maps, pkg_token) if pkg_token is not None else ''
            cls = token_name(maps, pkg_token, cls_token) if (pkg_token is not None and cls_token is not None) else ''
            row = {
                'file': p.name, 'index': idx, 'time': ts(abs_ms,args.tz), 'abs_ms': abs_ms,
                'type': typ, 'event': EVENT_TYPES.get(typ, f'UNKNOWN_{typ}'),
                'package_token': pkg_token, 'package': pkg,
                'class_token': cls_token, 'class': cls,
                'flags': e.get('flags'), 'instance_id': e.get('instance_id'),
                'task_root_package_token': e.get('task_root_package_token'),
                'task_root_class_token': e.get('task_root_class_token'),
            }
            rows.append(row)
            print(f"{row['time']}  {row['event']:<28} pkg={pkg} cls={cls} raw_pkg={pkg_token} raw_cls={cls_token}")
    if args.csv_path:
        keys=['file','index','time','abs_ms','type','event','package_token','package','class_token','class','flags','instance_id','task_root_package_token','task_root_class_token']
        with open(args.csv_path,'w',newline='',encoding='utf-8-sig') as f:
            w=csv.DictWriter(f, fieldnames=keys); w.writeheader(); w.writerows(rows)
        print(f'\nCSV written: {args.csv_path}')
if __name__ == '__main__':
    main()

image.png

整理好之后对event进行筛查,我们只需要看通知被查看的情况,这边选择NOTIFICATION_SEEN

过滤后再次过滤时间 image.png

最后得到选AC

8. 该手机曾进行过一次备份,使用的工具是

image.png

在data路径发现了一个backup文件夹

里边存在有一个包名的文件夹com.stevesoltys.seedvault image.png

得到包名和路径

所以工具是seedvault

9. 使用上述工具进行备份的具体日期是

image.png

上一题已经找到了备份的工具包名

步进查看文件

可以看到上一次lastBackup是1778307867843

转时间即可 image.png

所以是2026/5/9

A. 二维码

B. 密码相关提示

C. FIC

D. 助记词

Android的开机动画文件一般就叫bootanimation.zip image.png

直接搜索即可 image.png

打开就可以看到开机动画了

重点就是上边有一个密码1-81-8 image.png

还有一个二维码

别的都没有所以答案是AB

除logo以外就一个密码,一个二维码

扫描二维码得到

+XTcxmYcgCkSTMTeURBAIOqg7Bz+xq8qlFIzY6SdJ0wl+RSh3g7VvrJRiG9/LwEaKc7I4bTjWzU51wQAwUSNOA==

明显先是一个base64,后边就需要密码了

所以明显现在有一个密文,一个密码,看看能不能解密

说真的,这真的能想到吗()

你需要想到这个1-81-1是两个1到8的意思,也就是1234567812345678 image.png

没有找到提示,只能猜测几个需要密码的简单的对称加密算法

最后确定是SM4算法

得到dept aci clus reco jou hors rooki sign san canc will bag

12. 助记词有部分残缺,需要补全的助记词数量为

明显我们刚刚拿到的这个助记词是残缺的,很多单词都不完整

这边最好是有一个BIP39的2048词表,因为Seedvault的12词是基于BIP39的2048词表的,用mnemonic也行

https://github.com/bitcoin/bips/blob/master/bip-0039/english.txt

写一个脚本对照,或者一个个对照都可以

from mnemonic import Mnemonic
​
raw = "dept aci clus reco jou hors rooki sign san canc will bag".split()
​
mnemo = Mnemonic("english")
wordlist = mnemo.wordlist
​
fixed = []
​
for x in raw:
    matches = [w for w in wordlist if w.startswith(x)]
​
    if len(matches) == 1:
        fixed.append(matches[0])
        print(f"{x:8s} -> {matches[0]}")
    elif x in wordlist:
        fixed.append(x)
        print(f"{x:8s} -> {x}")
    else:
        print(f"{x:8s} -> 候选异常: {matches}")
        raise SystemExit
​
phrase = " ".join(fixed)
​
print("\n修复后助记词:")
print(phrase)
​
print("\nBIP39 校验:", mnemo.check(phrase))

image.png

所以最后是9个残缺,答案是9

13. 嫌疑人近期使用了一款笔记软件,该应用数据加密使用的主要加密算法为

image.png

先锁定软件,基本上可以确定是这个Standard Notes

要看加密算法,那就不能急着仿真,先根据包名去找路径 image.png

image.png

找到存储的数据库,即可根据这个来研究加密算法了 image.png

**这边其实很明显开头写了004,Standard Notes的004加密协议就是 XChaCha20+Poly1305 **

可是比赛的时候哪里知道这是什么协议(

或者就只能看nonce是48位十六进制,即24字节,itemskey是64位十六进制,即32字节,所以得到XChaCha20+Poly1305算法

14. 分析该笔记软件,其数据库一次解密密钥为

image.png

写在数据库了已经,这个SN|ItemsKey就是

"content_type": "SN|ItemsKey",
"itemsKey": "69bf3693d45cd5485cc53cd7ad9c5c5bf769aa48847253c3991ef18bd3f2ae87",
"version": "004",
"uuid": "6ff1c1bc-3c6d-4003-84e5-e94f4a1f0214"

** 这个 **itemsKey 就是数据库里用于解开各条笔记 enc_item_key 的一次解密密钥

所以一次解密密钥是69bf3693d45cd5485cc53cd7ad9c5c5bf769aa48847253c3991ef18bd3f2ae87

15. 用来解密5月8日的收入的二次解密密钥为

说是5月8日,因此先确定时间

**发现对应的应该是 Item-2963e8b4-00b3-4d42-847e-71b2ca79e182 ** image.png

我们知道了是XChaCha20+Poly1305算法,解密即可

import base64
import json
from nacl.bindings import crypto_aead_xchacha20poly1305_ietf_decrypt
​
​
ITEMS_KEY_HEX = "69bf3693d45cd5485cc53cd7ad9c5c5bf769aa48847253c3991ef18bd3f2ae87"
​
ENC_ITEM_KEY = "004:7c1ef6f717d20faf8cdca32cef2dd39736704940918302f6:0Pul8qMWK7Gu+12Y/+0qI6QhiLIm4lFrpd7ebnEslEVukAfQ3k/QZqeyHCx3wR2iG+r6urZ4KsS/7W5utnRWfW+HNThEQjtgmLlqIF56mXs=:eyJ1IjoiMjk2M2U4YjQtMDBiMy00ZDQyLTg0N2UtNzFiMmNhNzllMTgyIiwidiI6IjAwNCJ9:e30="
​
CONTENT = "004:b863f59a4aa43af996eac1293d983a39e104ca83781300b3:NleNZvh1dmlhnnkGu3nnTM4G4LN5nVTSqa8s7RJ/LImFvNUjeOL7nGr3m41ECs5pic+ZhKuhTCggvuQltq9+b9+Y0+aXO79o4to4xda38aYjd5OHu/4PEdpmEqLNS5atTufwORccVjxabns8WRkCHqWeFMBskxniK3xh/rFdyW4e2Md8B7bd/oMuluS3uBR4I+JlkVE2Q0zNqto2sOMHEnnxB1xhtYrvgRi39UMt/oRwIBaTBv/l/qU99P92ESfVdcxfi5AhROTv3aITNh6NOiO+fZ2Z9yhfe5MApBcVATI9sApoBVFd3gXAATqKc043/+WQMVl+fD6D20CXnqERuDX4XP4VEAat2WPLiuiIv73mai9/wxGht2n0kkcK6kljh+6Hx4AH1rEBw6a1WfWr/g==:eyJ1IjoiMjk2M2U4YjQtMDBiMy00ZDQyLTg0N2UtNzFiMmNhNzllMTgyIiwidiI6IjAwNCJ9:e30="
​
​
def decrypt_004(payload, key):
    parts = payload.strip().split(":")
​
    version = parts[0]
    nonce_hex = parts[1]
    ciphertext_b64 = parts[2]
    associated_data_b64 = parts[3]
​
    if version != "004":
        raise ValueError("不是 Standard Notes 004 格式")
​
    nonce = bytes.fromhex(nonce_hex)
    ciphertext = base64.b64decode(ciphertext_b64)
​
    # 这里不能写成 base64.b64decode(parts[3])
    # Standard Notes 004 的 AAD 用的是 base64 字符串本身
    associated_data = associated_data_b64.encode()
​
    return crypto_aead_xchacha20poly1305_ietf_decrypt(
        ciphertext,
        associated_data,
        nonce,
        key,
    )
​
​
def main():
    items_key = bytes.fromhex(ITEMS_KEY_HEX)
​
    item_key_hex = decrypt_004(ENC_ITEM_KEY, items_key).decode()
    print("[+] 二次解密密钥:")
    print(item_key_hex)
​
    item_key = bytes.fromhex(item_key_hex)
​
    content_plain = decrypt_004(CONTENT, item_key).decode("utf-8", "replace")
    print("\n[+] 明文 JSON:")
    print(content_plain)
​
    content = json.loads(content_plain)
    print("\n[+] 标题:", content.get("title", ""))
    print("[+] 正文:", content.get("text", ""))
    print("[+] 预览:", content.get("preview_plain", ""))
​
​
if __name__ == "__main__":
    main()

得到结果

[+] 二次解密密钥:
c1790efd9f93361ed78291524d46983d32f866308c4eda7d2228bcd35405e999
​
[+] 明文 JSON:
{"text":"今日收入59275.25","title":"2026年5月8日星期五 at 13:19","noteType":"plain-text","editorIdentifier":"com.standardnotes.plain-text","references":[],"appData":{"org.standardnotes.sn":{"client_updated_at":"2026-05-08T05:20:05.084Z"}},"preview_plain":"今日收入59275.25"}
​
[+] 标题: 2026年5月8日星期五 at 13:19
[+] 正文: 今日收入59275.25
[+] 预览: 今日收入59275.25

所以二次解密密钥为c1790efd9f93361ed78291524d46983d32f866308c4eda7d2228bcd35405e999

16. 笔记软件中记录5月7日的收入为

我们可以直接仿真来看 image.png

雷电安装之后直接导入应用数据就好了,就不细说了

image.png

很方便就能得到5.7收入为84826.90

17. 笔记软件中记录5月6日的收入为

在笔记软件转了好几圈,垃圾桶里也翻过了就是没有5.6的收入,数据库里都没写

实在是难,因为这一题需要我们联系前边的备份来解密,我们需要恢复原来的笔记软件,找到旧版的数据库再进行解密

我们回到之前创建了备份的seedvault image.png

这边打开来可以看到,很像是一个IOS的备份 image.png

这边比赛实在是想不明白要怎么做,恢复这备份最方便的就是使用下边这个工具,Seednaut

https://github.com/Baltram/seednaut image.png

我们直接下载使用恢复备份即可 image.png

这边需要写一个mnemoic phrase,即我们刚刚恢复的助记词

depth acid cluster record journey horse rookie sign sand cancel will bag

image.png

发现有两个,我们这边提取6号那个 image.png

直接提取即可 image.png

然后到老目录下看,找到数据包 image.png

里边有个压缩包,提取解压 image.png

找到了新的数据库 image.png

根据之前的思路解密即可

import base64
import json
from nacl.bindings import crypto_aead_xchacha20poly1305_ietf_decrypt
​
​
ITEMS_KEY_HEX = "69bf3693d45cd5485cc53cd7ad9c5c5bf769aa48847253c3991ef18bd3f2ae87"
​
ENC_ITEM_KEY = "004:68d91e5d73ca45af30662758d6d51b22757469e31a5541c8:ORPnUGEq5yG+kzhUVwubXe8PLNrV2a/NJl+sZVyzPrtbtAgnYFC3II/pWro0RwoGKrXWDVT/duXmiSsZREOlL0uP5KxNLGZoZvYvO3obsS0=:eyJ1IjoiNTdiODNjZDMtZDIzNS00OWQ4LWI1ZDAtNWZhOWQ3MmNiYzgzIiwidiI6IjAwNCJ9:e30="
​
CONTENT = "004:0caebe72961434120849269874d5d0bbdc1160ca4dd35c8f:mH20bxINSWKGE33TwiZPq0dqNtemjukYPZfrUJrrcVDQHa6iOVbnRq3ZAJxgYJpEOdBh9uNr/6PL38KViJh0wHsCJ4/ieHED+iYv734uK0rpryhdaP7TZB5ZYZDKJ75SOp+UCZiY++R9I9RP79V9xtksGnNyOdGy/q3NPQXbmZynvdxO1TJFGhl8QDWbzwYsFmcWGX5/reeOdcbuK9rlddg8kdsfkoTIxX9Z7QJ8xkXwIQa242pGsQYZ4s12WknxpW36UKWQAa/DvstCTRC6ddRMq6HID/96JLtCx5fMv0cTxtCaZMhEL5pSxXK7Zw1SNcQ+hKfDOPC4clOYGnhfTjEoR0JyOpkzGuuIr+Z3wvfHv6HO9BOc64NEBP7Qvr7B8BhONhdPgzxz2vqwnk/W4OtTAqW4NA==:eyJ1IjoiNTdiODNjZDMtZDIzNS00OWQ4LWI1ZDAtNWZhOWQ3MmNiYzgzIiwidiI6IjAwNCJ9:e30="
​
​
def decrypt_004(payload, key):
    parts = payload.strip().split(":")
​
    version = parts[0]
    nonce_hex = parts[1]
    ciphertext_b64 = parts[2]
    associated_data_b64 = parts[3]
​
    if version != "004":
        raise ValueError("不是 Standard Notes 004 格式")
​
    nonce = bytes.fromhex(nonce_hex)
    ciphertext = base64.b64decode(ciphertext_b64)
​
    # 这里不能写成 base64.b64decode(parts[3])
    # Standard Notes 004 的 AAD 用的是 base64 字符串本身
    associated_data = associated_data_b64.encode()
​
    return crypto_aead_xchacha20poly1305_ietf_decrypt(
        ciphertext,
        associated_data,
        nonce,
        key,
    )
​
​
def main():
    items_key = bytes.fromhex(ITEMS_KEY_HEX)
​
    item_key_hex = decrypt_004(ENC_ITEM_KEY, items_key).decode()
    print("[+] 二次解密密钥:")
    print(item_key_hex)
​
    item_key = bytes.fromhex(item_key_hex)
​
    content_plain = decrypt_004(CONTENT, item_key).decode("utf-8", "replace")
    print("\n[+] 明文 JSON:")
    print(content_plain)
​
    content = json.loads(content_plain)
    print("\n[+] 标题:", content.get("title", ""))
    print("[+] 正文:", content.get("text", ""))
    print("[+] 预览:", content.get("preview_plain", ""))
​
​
if __name__ == "__main__":
    main()

image.png

所以5.6收入为76583.87

18. 手机检材中有一个AI助手程序,分析该程序配置与任务,哪个应用程序运行后会清空本地存储内容?

image.png

ai软件还是比较好锁定的,就是这一个Operit AI

与此同时我们可以在下载处发现一个Operit文件夹 image.png

workflow下看见了这样子一个json文件 image.png

可以看到访问了com.tencent.mm之后,AI助手会执行/sdcard的动作

即删除本地共享存储目录

所以本题答案是com.tencent.mm

19. 分析AI助手程序调用的模型,结合笔记软件中的记录,用来隐藏银行卡密码的模型文件名为

image.png

依旧直接仿真APK,来导入应用数据 image.png

直接就能看见,隐藏银行卡密码的模型文件名为ultraman-663M-BF16.gguf

(其实截图里边也有

20. 嫌疑人曾自行修改上题中的模型,他是通过什么原始模型修改而来的?

先定位上题的模型ultraman-663M-BF16.gguf image.png

image.png

直接放到Winhex里可以看到是hunyuan模型 image.png

所以直接搜索Hunyuan即可搜到模型全称

答案是Hunyuan-7B-Instruct-MNN

21. 被修改后的模型量化精度为

首先是模型里直接写了是BF16

其次这个会写在十六进制,我们只需要搜索general.file_type即可 image.png

** 在 GGUF / llama.cpp 的 general.file_type 枚举里这一个32就是MOSTLY_BF16的意思**

**所以答案是BF16 ** image.png

装LM Studio里还会自动解析

22. 能够提升修改后模型对话能力的虚拟token为

作为对话,我们可以搜索 chat_template 来定位

chat_template是ai把一段聊天记录转化为模型能读懂的输入格式的地方

所以如果有能提升修改后模型对话能力的虚拟token的话,大概率在这边 image.png

发现多了很多,其中每段助手恢复后的结束符都是<|hy_place▁holder▁no▁8|>,这会使其及时停下,提升对话能力

所以答案是<|hy_place▁holder▁no▁8|>

当然也可以去搜一下operit.log

在日志中查找一下对话记录 image.png

同样能找到,并发现是每句话末尾必带的token

即为答案

23. 分析AI助手与笔记软件中的记录,找出银行卡6位数字密码为

这边让我们分析AI助手和笔记软件的记录

我们其实都仿真了 image.png

笔记里说密码就是秘密 image.png

然后AI一直在说秘密的英文是secret image.png

所以直接看这些几乎没有头绪

但是题目给了提示,19题说模型是用来隐藏银行卡密码的

所以也就是说密码一定和模型有关,要么直接写在模型里边,要么对话可以得到密码

首先是对模型进行分析

因为GGUF文件前边的固定结构

GGUF 魔数
version
tensor_count
metadata_count
metadata 数据

每一条metadata大概都是

key 长度 + key 字符串 + value 类型 + value 内容

所以我们可以以此写脚本,在metadata中列出所有的6位数字

import re
import struct
import sys
from pathlib import Path
​
​
def read(fmt, f):
    size = struct.calcsize(fmt)
    data = f.read(size)
    if len(data) != size:
        raise EOFError("文件不完整")
    return struct.unpack(fmt, data)[0]
​
​
def read_string(f):
    length = read("<Q", f)
    data = f.read(length)
    return data.decode("utf-8", errors="replace")
​
​
def read_value(f, value_type):
    # GGUF 常见 value_type
    if value_type == 0:
        return read("<B", f)
    if value_type == 1:
        return read("<b", f)
    if value_type == 2:
        return read("<H", f)
    if value_type == 3:
        return read("<h", f)
    if value_type == 4:
        return read("<I", f)
    if value_type == 5:
        return read("<i", f)
    if value_type == 6:
        return read("<f", f)
    if value_type == 7:
        return bool(read("<?", f))
    if value_type == 8:
        return read_string(f)
    if value_type == 10:
        return read("<Q", f)
    if value_type == 11:
        return read("<q", f)
    if value_type == 12:
        return read("<d", f)
​
    # ARRAY
    if value_type == 9:
        elem_type = read("<I", f)
        count = read("<Q", f)
        return [read_value(f, elem_type) for _ in range(count)]
​
    raise ValueError(f"不支持的类型: {value_type}")
​
​
def to_hex(s):
    return " ".join(f"{b:02X}" for b in s.encode("utf-8"))
​
​
def find_six_digits(key, value):
    results = []
​
    if isinstance(value, str):
        for m in re.finditer(r"(?<!\d)\d{6}(?!\d)", value):
            num = m.group()
            results.append((key, num, to_hex(num), value))
​
    elif isinstance(value, int):
        if 100000 <= value <= 999999:
            num = str(value)
            results.append((key, num, to_hex(num), value))
​
    elif isinstance(value, list):
        for i, item in enumerate(value):
            sub_key = f"{key}[{i}]"
            results.extend(find_six_digits(sub_key, item))
​
    return results
​
​
def parse_gguf(path):
    results = []
​
    with open(path, "rb") as f:
        magic = f.read(4)
        if magic != b"GGUF":
            raise ValueError("这不是 GGUF 文件")
​
        version = read("<I", f)
        tensor_count = read("<Q", f)
        metadata_count = read("<Q", f)
​
        print("[+] version:", version)
        print("[+] tensor_count:", tensor_count)
        print("[+] metadata_count:", metadata_count)
​
        for _ in range(metadata_count):
            key = read_string(f)
            value_type = read("<I", f)
            value = read_value(f, value_type)
​
            results.extend(find_six_digits(key, value))
​
    return results
​
​
def main():
    if len(sys.argv) != 2:
        print("用法: python find_6digits.py model.gguf")
        return
​
    path = Path(sys.argv[1])
    results = parse_gguf(path)
​
    print("\n========== 六位数字搜索结果 ==========")
​
    if not results:
        print("[-] 没找到六位数字")
        return
​
    for key, num, hex_value, source in results:
        print("\n[+] 找到六位数字")
        print("metadata key:", key)
        print("数字:", num)
        print("数字十六进制:", hex_value)
​
        if isinstance(source, str):
            print("所在内容片段:", source[:200].replace("\n", "\\n"))
​
​
if __name__ == "__main__":
    main()

全部列出后发现了一个最可疑的 image.png

525252,这就是答案,因为这是tokenizer.ggml.tokens[120818] 里的真实 token 内容 ,即它不是字段数值,也不是 token id,而是token 本身的内容就是 525252

基本上可以确定是故意隐藏的答案,525252

当然我们也可以对ai多拷打拷打 image.png

直接装到LM Studio上运行 image.png

模型傻了,但是输出了六位数字,所以答案是525252