手机取证
这套题的手机取证真难啊,感谢羊羊羊和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. 分析手机检材,该手机设备名称为
第一题就来个下马威了

看起来火眼这边分析出来的是Xbox Wireless Controller
这明显不是手机设备的名称,因为这是Xbox无线控制器的意思,是手柄的名字

可以看到火眼这边解析的是蓝牙属性,很奇怪,因此这边都不太对
我们需要去找手机设置的路径,可以通过爆搜如device_name等内容找一下
在/data/system/users/0/settings_global.xml我们可以看到真正的设备名称

得到设备名称为REDMAGIC 9 Pro+
比赛的时候没找到这块,做不出去手机相册闲逛了

能看见屏幕截图,正好截图截到了本机的设备名称
2. 分析手机检材,该手机系统magisk【环境版本】为

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

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

得到最终答案,环境版本为30600
3. 分析手机检材,嫌疑人通过盖世游戏app安装的《最终幻想》游戏版本是

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

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

通过对fantasy进行的搜索,我们最终在data_15/media/0/Android/data/com.xiaoji.egggame/files/Documents/XiaoKunLogcat/XiaoKunLogInfo-5.txt
找到了下的是版本号XIII的最终幻想
4. 分析手机检材,5月6日,嫌疑人最后一次使用谷歌套件中的某个app,其包名是

根据题目说法,将最后使用时间过滤为2026-05-06
再过滤包名含有google
只剩两个了
根据下一题,这个app是会推送新闻的
因此确定是com.google.android.googlequicksearchbox
因为另一个是Googleplay服务,不会推新闻,一般都是推送基础服务,而这一个搜索的更会推新闻一点
如果不放心我们可以由下一题进行验证(虽然比赛的时候根本就没做出第五题
5. 分析上述app5月6日推送新闻的相关痕迹和缓存,新闻《男子拾获钱包以为天降横财》中事件发生的地点是

先根据包名搜索确定路径

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

发现有一个叫做image的文件夹,很显然在这里边
遍历一下就能看到本题答案了

地址是柔佛麻坡
6. 分析该手机关机信息情况,最近一次因电池电池异常过热导致关机的北京时间为
关机信息情况,直接搜索shutdown看看

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

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

寻找一番,发现是这个文件,里边明确写了
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

或者文件里边其实也写了这个时间,不用转化直接复制粘贴也一样
7. 分析手机检材,北京时间2026-05-06 10:43:38左右那些应用的通知被查看了?
这题真的很难,比赛的时候几乎毫无思路,跟着mumuzi老师走一下
这边需要我们查看使用统计数据库
在/data/system_ce/0/usagestats

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

题目说的时间差不多是1778035418000
所以说左右,最接近的就是第一个文件1778031702128,应该会包含到题目的时间那个区间
接下来就是对里边内容进行了解
这些文件,明文看什么都看看不出,因为他们把很多内容都是保存成了数字token,而映射表是mappings文件

就在同路径下
因此我们可以写脚本读取,整合为一个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()

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

最后得到选AC
8. 该手机曾进行过一次备份,使用的工具是

在data路径发现了一个backup文件夹
里边存在有一个包名的文件夹com.stevesoltys.seedvault

得到包名和路径
所以工具是seedvault
9. 使用上述工具进行备份的具体日期是

上一题已经找到了备份的工具包名
步进查看文件
可以看到上一次lastBackup是1778307867843
转时间即可

所以是2026/5/9
10. 手机重启过程中,屏幕上除厂商logo外,还可以看到以下哪些内容
A. 二维码
B. 密码相关提示
C. FIC
D. 助记词
Android的开机动画文件一般就叫bootanimation.zip

直接搜索即可

打开就可以看到开机动画了
重点就是上边有一个密码1-81-8

还有一个二维码
别的都没有所以答案是AB
11. 分析上述除logo外的信息,发现与加密后的助记词有关,这里用来加密助记词使用的对称加密算法为
除logo以外就一个密码,一个二维码
扫描二维码得到
+XTcxmYcgCkSTMTeURBAIOqg7Bz+xq8qlFIzY6SdJ0wl+RSh3g7VvrJRiG9/LwEaKc7I4bTjWzU51wQAwUSNOA==
明显先是一个base64,后边就需要密码了
所以明显现在有一个密文,一个密码,看看能不能解密
说真的,这真的能想到吗()
你需要想到这个1-81-1是两个1到8的意思,也就是1234567812345678

没有找到提示,只能猜测几个需要密码的简单的对称加密算法
最后确定是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))

所以最后是9个残缺,答案是9
13. 嫌疑人近期使用了一款笔记软件,该应用数据加密使用的主要加密算法为

先锁定软件,基本上可以确定是这个Standard Notes
要看加密算法,那就不能急着仿真,先根据包名去找路径


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

这边其实很明显开头写了004,Standard Notes的004加密协议就是 XChaCha20+Poly1305
可是比赛的时候哪里知道这是什么协议(
或者就只能看nonce是48位十六进制,即24字节,itemskey是64位十六进制,即32字节,所以得到XChaCha20+Poly1305算法
14. 分析该笔记软件,其数据库一次解密密钥为

写在数据库了已经,这个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

我们知道了是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日的收入为
我们可以直接仿真来看

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

很方便就能得到5.7收入为84826.90
17. 笔记软件中记录5月6日的收入为
在笔记软件转了好几圈,垃圾桶里也翻过了就是没有5.6的收入,数据库里都没写
实在是难,因为这一题需要我们联系前边的备份来解密,我们需要恢复原来的笔记软件,找到旧版的数据库再进行解密
我们回到之前创建了备份的seedvault

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

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

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

这边需要写一个mnemoic phrase,即我们刚刚恢复的助记词
depth acid cluster record journey horse rookie sign sand cancel will bag

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

直接提取即可

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

里边有个压缩包,提取解压

找到了新的数据库

根据之前的思路解密即可
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()

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

ai软件还是比较好锁定的,就是这一个Operit AI
与此同时我们可以在下载处发现一个Operit文件夹

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

可以看到访问了com.tencent.mm之后,AI助手会执行/sdcard的动作
即删除本地共享存储目录
19. 分析AI助手程序调用的模型,结合笔记软件中的记录,用来隐藏银行卡密码的模型文件名为

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

直接就能看见,隐藏银行卡密码的模型文件名为ultraman-663M-BF16.gguf
(其实截图里边也有
20. 嫌疑人曾自行修改上题中的模型,他是通过什么原始模型修改而来的?
先定位上题的模型ultraman-663M-BF16.gguf


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

所以直接搜索Hunyuan即可搜到模型全称
答案是Hunyuan-7B-Instruct-MNN
21. 被修改后的模型量化精度为
首先是模型里直接写了是BF16
其次这个会写在十六进制,我们只需要搜索general.file_type即可

在 GGUF / llama.cpp 的 general.file_type 枚举里这一个32就是MOSTLY_BF16的意思
所以答案是BF16

装LM Studio里还会自动解析
22. 能够提升修改后模型对话能力的虚拟token为
作为对话,我们可以搜索 chat_template 来定位
chat_template是ai把一段聊天记录转化为模型能读懂的输入格式的地方
所以如果有能提升修改后模型对话能力的虚拟token的话,大概率在这边

发现多了很多,其中每段助手恢复后的结束符都是<|hy_place▁holder▁no▁8|>,这会使其及时停下,提升对话能力
所以答案是<|hy_place▁holder▁no▁8|>
当然也可以去搜一下operit.log
在日志中查找一下对话记录

同样能找到,并发现是每句话末尾必带的token
即为答案
23. 分析AI助手与笔记软件中的记录,找出银行卡6位数字密码为
这边让我们分析AI助手和笔记软件的记录
我们其实都仿真了

笔记里说密码就是秘密

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

所以直接看这些几乎没有头绪
但是题目给了提示,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()
全部列出后发现了一个最可疑的

525252,这就是答案,因为这是tokenizer.ggml.tokens[120818] 里的真实 token 内容 ,即它不是字段数值,也不是 token id,而是token 本身的内容就是 525252
基本上可以确定是故意隐藏的答案,525252
当然我们也可以对ai多拷打拷打

直接装到LM Studio上运行

模型傻了,但是输出了六位数字,所以答案是525252
后边慢慢做()