2026 精武杯 手机取证部分

作者:正在思考ing 发布:2026-10-20 22:07 收录:2026-10-21 10:47 3 次阅读 约 4759 字
摘要:感觉不如早起王
推荐理由:本文涵盖「手机取证」、「反诈」、「电信网络诈骗」等多个主题,重点关注 手机取证。
这次比赛的赛制设计实在令人叹为观止。主办方别出心裁地采用了“闯关模式”:必须提交完当前检材的题目,才能解锁下一个检材的题目,而且提交后连之前的题目都无法再查看。如此一来,团队分工直接沦为摆设,多人协作硬生生变成了排队做题。能把一场团队赛办成单人接力赛,也算是一种本事

不知道主办方究竟是想考察选手的取证能力,还是想展示自己对团队赛的独特理解

( ̄▽ ̄")

简要案情


近期,某市反诈部门接到多名群众报案,均遭遇“投资理财”类电信网络诈骗,指向的犯罪团伙涉案资金巨大、作案链条隐蔽。办案单位迅速成立专案组开展电子数据取证工作。经初查,该犯罪团伙搭建虚假投资平台,通过社交软件诱骗受害人充值转账,团伙成员分散多地办公,使用多种 AI 工具进行违法犯罪活动,同时发现涉及其他犯罪事实。请根据获取的电子数据还原案件经过。

注:所有检材及相关数据均为虚拟构造,不负法律责任,如有侵权请及时告知,未经允许不得用于商业用途。

检材容器信息


文件名称: 2026 精武杯检材容器

文件大小: 140 GB (150,323,855,360 字节)

MD5: A2EA823942EDE333E8F8D182E49FBC49

SHA256: A3F0BDA2FA8CA97A6E656D52B1105EB52C91214FE349D2E3A338CDE5E5B45E74

容器密码:2026精武杯/中国人民警察大学@CPPU-ISA&拓界科技@飞鹰小队

题目


手机检材-1

手机检材-1 解压出来是个文件夹,里面有大量 app 的包

可以看出手机安装了 vpn 软件 Clash for Android 和 v2rayNG,应该会作为考点,还能确定是小米手机(有 小米运动健康 等 app)

1. 分析嫌疑人在该手机登录的小米账号ID为?(答案格式:以实际为准,如1234567890)

3134129162

查看小米健康可以看到 id

2. 嫌疑人设定了一个每周六循环的闹钟,该闹钟的备注为?(答案格式:以实际为准,如打精武杯)

给老妈汇款

3. 分析嫌疑人使用的VPN软件,配置的节点服务器地址是什么?(答案格式:不含端口,如192.168.1.1)

107.175.0.221

根据左侧的列表可以看出手机安装了至少两个 vpn 软件:Clash 和 v2rayNG

(需要将取证工具更新到最新版,否则只有 Clash 没有 v2rayNG)

首先要确定哪个是机主常用的 vpn,先看 Clash 的配置文件

配置文件的路径是 /Clash for Android(com.github.kr328.clash).bak_2/apps/com.github.kr328.clash/f/imported/6b3f5a87-bfa8-46d8-902f-78297e606186/config.yaml

配置文件中提示 客户端版本过旧,可以猜测机主很久没有使用了,来看 v2rayNG

可以直接在节点信息中看到服务端 IP

4. 接上题,上述VPN软件在该手机的桌面位置是什么?请写出第几页第几行第几列。(答案格式:页-行-列,如1-2-3)

3-1-3

根据上题,vpn 软件是 v2rayNG,要确定该 app 在桌面的位置,开始想到的是找一找相册里有没有桌面截图,翻了一下没找到

最后找了系统桌面 app 的包 com.miui.home,其中有个数据库文件 launcher4x6.db,记录了各个 app 在桌面的位置

  • screen:页数,在第三页
  • cellX:列数,从 0 开始计数,故在第 3 列
  • cellY:行数,也是从 0 开始计数,故在第 1 行

故最终答案为 3-1-3

5. 嫌疑人最近一次登录Google账号的日期是什么时候?(答案格式:2026-09-09)

2026-07-07

查看手机自带浏览器的历史记录

可以直接看到最后一次登录 google 账号的时间

6. 在嫌疑人手机相册中发现一张图片文件名异常,经分析该图片进行了数据隐写,请回答该异常图片的文件名称是什么?(答案格式:XXX.png)

密码.png

找了半天的图片没有看到可疑的

最后发现手机给图片做了备份,即上图中的 backup_image.zip,直接解压

(开始用 windows 自带解压功能解压失败了,换 bandzip 才解压成功)

解压出来发现有两个目录:DCIM 和 无印

DCIM 就是 Digital Camera Images,是手机默认存放照片的地方,我们看这里

显然 密码.png 非常可疑,一片空白还叫密码,猜测是 lsb 隐写,用 StegSolve 看看

隐藏了一张二维码,实锤了

7. 接上题,该异常图片中隐写的内容为?(答案格式:feiying666)

qazokm123

直接扫描上题中得到的二维码

8. 在该手机屏幕第3页第1列第4行的应用是一个伪装的密拍程序,请问机主使用该软件拍了几张图片?(答案格式:9)

1

参考第 4 题,我们通过系统桌面 app 的数据库中去查找对应位置的 app 是什么

已知该 app 在手机屏幕第 3 页第 1 列第 4 行,那么对应 screen = 3,cellX = 0,cellY = 3

定位到该 app 名为计算器,显然名字是伪装的,对应的包名为 com.androidx.calculator

根据包名可以找到两个备份

.bak 的备份中是该应用的 apk base.apk,.bak_38 的备份中是该应用的数据库 stwyuo.db

本来想直接看数据库的,结果打不开,说明可能被加密了

没办法只能逆向 base.apk 了

先看 AndroidManifest.xml

定位到一个关键类名 androidx.appcompat.app.CCActivity(查看源码发现其中有 摄像头打开失败 这样的字符串,猜测这个就是拍照的主要逻辑)

用 jadx 反编译

发现有 sqlcipher 包,说明是用 SQLCipher 对数据库进行的加密,那么我们找到密码就可以解密了

继续定位刚刚在 manifest 中看到的关键类 CCActivity

定位到如下关键代码片段

private void initCamera() {
this.mCameraView.addCameraListener(new CameraListener() { // from class: androidx.appcompat.app.CCActivity.2
@Override// com.otaliastudios.cameraview.CameraListener
public void onCameraOpened(CameraOptions cameraOptions) {
   super.onCameraOpened(cameraOptions);
   CCActivity.this.isCameraOpened = true;
  }

@Override// com.otaliastudios.cameraview.CameraListener
public void onPictureTaken(PictureResult pictureResult) {
   super.onPictureTaken(pictureResult);
   CCActivity.this.mDataHandler.handlerData(CCActivity.this.mPhoneNumber, CCActivity.this.getLastLocation(), pictureResult.getData());
   CCActivity.this.showText("已抓拍");
  }
 });
this.mBtn.setOnClickListener(new View.OnClickListener() { // from class: androidx.appcompat.app.CCActivity$$ExternalSyntheticLambda0
@Override// android.view.View.OnClickListener
public final void onClick(View view) {
   CCActivity.this.m0lambda$initCamera$1$androidxappcompatappCCActivity(view);
  }
 });
}

在 app 完成抓拍时会调用 DataHandler 类,继续跟踪

由于我们要想办法解密数据库,由此可定位到如下片段

public /* synthetic */void m1lambda$handlerData$0$androidxappcompatappDataHandler(String str, String str2, byte[] bArr) {
 File filesDir = this.mContext.getFilesDir();
 String[] list = filesDir.list();
if (list != null && list.length >= 5) {
  FileUtils.deleteAllInDir(filesDir);
 }
 File file = new File(filesDir, String.format("%s/%s", Long.valueOf(getNetworkTimeMillis()), Keys.keyDb()));
if (FileUtils.createOrExistsDir(file.getParentFile())) {
  SQLiteDatabase openOrCreateDatabase = SQLiteDatabase.openOrCreateDatabase(file, Keys.keyBase(), (SQLiteDatabase.CursorFactory) null);
  createTable(openOrCreateDatabase);
  insertData(openOrCreateDatabase, str, str2, bArr);
  openOrCreateDatabase.close();
this.mHandler.sendEmptyMessage(!uploadData(file));
 }
}

关键就这两行数据库操作

File file = new File(filesDir, String.format("%s/%s", Long.valueOf(getNetworkTimeMillis()), Keys.keyDb()));

SQLiteDatabase openOrCreateDatabase = SQLiteDatabase.openOrCreateDatabase(file, Keys.keyBase(), (SQLiteDatabase.CursorFactory) null);

第一行是在创建或打开文件,通过 getNetworkTimeMillis() 获得当前网络时间戳作为目录名,调用 Keys 类的 keyDb() 方法生成文件名

第二行是通过调用 Keys 类的 keyBase() 方法生成密钥打开或创建一个被加密的数据库,数据库路径就是 file,也就是刚刚构建的文件路径

显然下一步我们需要跟踪 Keys 类

class Keys {
    privatestaticfinalchar[] HEX_DIGITS = "0123456789abcdef".toCharArray();
    privatestaticbyte one = 116;
    privatestaticbyte[] ones = {-1, 0, 3, 5, 1, -5};
    staticbyte[] onesss = {49, 48, 51, 46, 50, 55, 46, 49, 48, 56, 46, 55, 56, 58, 49, 50, 53, 54, 51};

    Keys() {
    }

    /* JADX INFO: Access modifiers changed from: package-private */
    public static String keyDb() {
        int length = ones.length;
        byte[] bArr = newbyte[length + 3];
        for (int i = 0; i < length; i++) {
            bArr[i] = (byte) (one + ones[i]);
        }
        bArr[length] = 46;
        bArr[length + 1] = 100;
        bArr[length + 2] = 98;
        returnnew String(bArr);
    }

    /* JADX INFO: Access modifiers changed from: package-private */
    public static String keyBase() {
        try {
            return bytes2Hex(MessageDigest.getInstance("md5").digest(ones)).substring(0, 6);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            return"defkey";
        }
    }

    public static String bytes2Hex(byte[] bArr) {
        int length;
        if (bArr == null || (length = bArr.length) <= 0) {
            returnnull;
        }
        char[] cArr = newchar[length << 1];
        int i = 0;
        for (int i2 = 0; i2 < length; i2++) {
            int i3 = i + 1;
            char[] cArr2 = HEX_DIGITS;
            cArr[i] = cArr2[(bArr[i2] >>> 4) & 15];
            i = i3 + 1;
            cArr[i3] = cArr2[bArr[i2] & 15];
        }
        returnnew String(cArr);
    }
}

代码逻辑很简单,keyDb() 方法通过 one 进行简单的运算生成文件名,keyBase() 方法通过对 ones 数组的 16 进制计算 md5 哈希并取前 6 个字符生成密钥

用 python 重写上述逻辑即可获得数据库文件名和密钥

import hashlib

def bytes2Hex(b: bytes) -> str:
    """对应Java里的bytes2Hex,使用HEX_DIGITS = 0123456789abcdef"""
    HEX_DIGITS = "0123456789abcdef"
    sb = []
    for bt in b:
        # Java byte是有符号,python bytes是无符号,直接取即可
        sb.append(HEX_DIGITS[(bt >> 4) & 0x0F])
        sb.append(HEX_DIGITS[bt & 0x0F])
    return"".join(sb)

def keyDb():
    one = 116
    ones = [-1, 0, 3, 5, 1, -5]
    length = len(ones)
    bArr = bytearray(length + 3)
    for i in range(length):
        # Java byte强转,会截断到8bit
        val = (one + ones[i]) & 0xff
        bArr[i] = val
    bArr[length] = 46    # '.'
    bArr[length+1] = 100# 'd'
    bArr[length+2] = 98# 'b'
    return bArr.decode('ascii')

def keyBase():
    ones = bytes([x & 0xfffor x in [-1, 0, 3, 5, 1, -5]])
    md5_hash = hashlib.md5(ones).digest()
    hex_str = bytes2Hex(md5_hash)
    return hex_str[:6]

if __name__ == "__main__":
    db_name = keyDb()
    base_key = keyBase()
    print(f"keyDb 数据库文件名: {db_name}")
    print(f"keyBase 密钥前缀: {base_key}")

'''
输出内容:
keyDb 数据库文件名: stwyuo.db
keyBase 密钥前缀: c258ba
'''

文件名与刚刚找到的已知,把数据库文件用 DB Browser for SQLCipher 打开,输入密钥即可解密

发现数据库中只有 1 个 picture 表,其中只有 1 条数据,因此只拍摄了 1 张照片

9. 分析使用该密拍软件拍摄的图片,其中显示器的型号为?(答案格式:以实际为准,如TUOJIE)

C34H890WGC

SQLCipher 查看 data 字段可以直接看到照片

导出查看

型号为 C34H890WGC

10. 机主多次通过购票软件查询出行行程,在北京时间2026年6月24日20:21:33查询的目的城市为?(答案格式:南京)

百色

查找了一下手机安装的 app,就下面这个比较符合

app 包名为 com.yipiao,找到对应包的数据库 /智行火车票(com.yipiao).bak_29/apps/com.yipiao/db/zt_userInfo.db,查看 tbl traffic search his 表

把 timestamp 字段进行 Unix 时间戳转换可以看到精确时间

可以看到在该时间机主查询的目的城市为 百色

11. 请找到机主在2026-09-20 13:41:20打开的表格文件,该文件的SM3哈希值后四位是什么?(答案格式:英文大写,如12AB)

37EA

由于是问表格文件,所有去查看了一下 WPS 的历史文件,只看到了一条记录

这条记录是 xml 格式的,具体内容为

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="recent_file_key">[{"cloudCacheFileState":3,"editCount":3,"fileScope":2,"fileState":1,"id":"1a58a8f8-b03f-4fc1-9c0e-25434dd41156","name":"快捷回复-2026-04-14 10-23-48-enterprise.xlsx","filePath":"/storage/emulated/0/Android/data/cn.wps.moffice_eng/.cache/KingsoftOffice/file/download/32342bd4-6be8-46b5-8a4f-5fad2e96aff8/com.android.fileexplorer.myprovider/快捷回复-2026-04-14 10-23-48-enterprise.xlsx","userId":"","modifyDate":1789900864236,"type":0}]</string>
</map>

其中 filePath 字段记录了打开文件的完整路径,文件名为 快捷回复-2026-04-14 10-23-48-enterprise.xlsx,modifyDate 字段记录了文件上次修改的时间戳,应该就是上次打开时间,转换过来刚好是 2026-09-20 18:41:04

直接按照记录中的文件路径找不到文件,在手机做的文档备份 backup_doc.zip 中

想用 CyberChef 直接计算 SM3 哈希的,结果转半天都没算出来

最后写了个 python 脚本计算

import hashlib

def get_file_sm3(file_path: str) -> str:
    """读取文件,计算SM3,返回大写哈希值"""
    sm3_obj = hashlib.new("sm3")
    buf_size = 65536
    with open(file_path, "rb") as f:
        while data := f.read(buf_size):
            sm3_obj.update(data)
    return sm3_obj.hexdigest().upper()


if __name__ == "__main__":
    # 目标文件名
    target_file = "快捷回复-2026-04-14 10-23-48-enterprise.xlsx"
    try:
        result = get_file_sm3(target_file)
        print(f"文件名:{target_file}")
        print(f"SM3哈希:{result}")
    except FileNotFoundError:
        print(f"❌ 文件不存在!请把脚本和xlsx文件放在同一个文件夹,或者填写完整绝对路径。")
    except Exception as err:
        print(f"❌ 计算失败:{err}")

'''
输出内容:
文件名:快捷回复-2026-04-14 10-23-48-enterprise.xlsx
SM3哈希:442A27B13AF23EA3295A388DE8438E2D91D8D50242F4FAF850DE22E414BA37EA
'''

12. 请分析嫌疑人在2026-09-20 17:39:19时播放的抖音链接为?(答案格式:以实际为准,如https://www.baidu.com/abc/123asad)

https://www.douyin.com/video/7641161472526150282

直接看抖音的观看历史

13. 请分析嫌疑人在抖音中共手动提现多少元至银行卡?(答案格式:100)

1003

字节的 app 的状态数大多不存储在数据库中,而是存储在自研的 keva 键值库里面,路径为 /抖音(com.ss.android.ugc.aweme).bak_26/apps/com.ss.android.ugc.aweme/f/keva/repo/notice_detail_cache_1625556299298107/key_notice_group_86.sgv

sgv 文件通常是明文的 json,直接用记事本打开了

直接搜索 提现

找到一条明显提现成功的记录(红色箭头),金额为 1000

但是答案不对,看了网上别的佬的题解,如上图蓝色箭头,这条提现记录中有一条 \"notice_type\":92,进一步搜索可以找到另一条记录

还有个 3 元抖音支付多次立减券,这也算提现,而且只能通过 \"notice_type\":92 定位得到,给了给了

所以答案是 1003

14. 请分析机主抖音头像从哪个应用下载的可能性最大,该应用的包名是?(答案格式:以实际为准)

com.quark.browser

通过抖音账户信息定位到 keva 键值库 /抖音(com.ss.android.ugc.aweme).bak_26/apps/com.ss.android.ugc.aweme/f/keva/repo/aweme_user/1625556299298107_aweme_user_info.sgv

但是没有找到头像相关信息

但是机主在夸克浏览器中搜索过 西装男头像

下载缓存中也有一条 jpg 图片的下载记录,只能猜测是夸克浏览器下载的了,对应包名 com.quark.browser

15. 机主相册中有大量的豪车、别墅信息用来“杀猪盘”,请分析这些照片是使用什么应用进行下载的,写出该应用的包名。(答案格式:以实际为准)

uni.UNI3DFAFCD

在照片备份的 无印 目录下有大量豪车别墅的图片

猜测 无印 是个 app 的名字,果然找到了

16. 接上题,该应用使用的下载链接来自于哪个应用,请写出链接来源应用的包名。(答案格式:以实际为准)

com.ss.android.ugc.aweme

要看应用使用的下载链接,那自然要去翻这个应用的数据库 /无印(uni.UNI3DFAFCD).bak_28/apps/uni.UNI3DFAFCD/db/wuyin_database,查看 download tasks 表

其中记录了大量下载记录,下载文件大多为图片,从 fileUrl、platform 和 courceUrl 字段都能看出下载链接来自抖音

包名为 com.ss.android.ugc.aweme

手机检材-2

解压

看到 Manifest.plist,可以确定是苹果手机

弘连的软件分析不了,下面换美亚

17. 该手机的备份密码是多少?(四位数字,答案格式:1234)

1111

直接把 Manifest.plist 丢去暴力破解,按照题目提示设置 4 位数字密码

18. 该手机的备份时间是什么时候?(答案格式:2026-09-09 09:09:09)

2026-09-23 15:02:50

Manifest.plist 中会记录备份时间

不过是 UTC 时间,我们需要加 8 小时转换为 UTC+8 时间

取证工具中也能直接看

19. 该手机的序列号是什么?(答案格式:以实际为准,英文大写)

FD2W71HVJCM0

序列号在 Manifest.plist 的 SerialNumber 字段

取证工具中也能看到

20. 该手机的蓝牙MAC地址是多少?(答案格式:英文大写,如A0:A1:4E:83:16:B0)

C0:A5:3E:82:06:E0

美亚也没取到,看网上题解都是用美亚的 Qiko 取证智能体取到的,懒得搞了,直接去找原始文件吧

由于是蓝牙 MAC 地址,我们搜索 bluetooth

/WiFi/bluetooth_status.txt 记录了蓝牙 MAC 地址

21. 分析该手机日志文件,该手机电池的激活时间(UTC+8)是什么时候?(答案格式:2022-01-23 15:02:53)

2021-12-26 20:35:27

依旧是全局搜索,问电池就搜 battery

定位到日志文件 /summaries/BatteryBDC.log,往下翻到最底部找到最早的时间,应该就是电池激活时间,注意要加 8 小时把 UTC 时间转换为 UTC+8 时间

22. 分析该手机的充电记录,该手机最近一次被充至 100% 电量的时间是什么?请回答时间戳。(答案格式:以数据库中记录的原始时间戳为准)

1783544086.8534038

由于是充电,我们先全局搜索 power

题目提示要找数据库,我们定位到 /logs/powerlogs/powerlog_2026-09-23_14-24_07CAD94F.PLSQL,疑似是充电记录的数据库

最后在 PLBatteryAgent EventBackward Battery 表中发现了 CurrentCapacity 字段,疑似是当前电量,而且先减少后增多,最后稳定在 100,符合电池电量变化的规律

如上图,最近一次被充至 100% 电量的时间戳为 1783544086.8534038

23. 嫌疑人安装蝙蝠.app的时间(UTC+8)是什么?(答案格式:以首次安装请求时间为准,如2026-09-09 09:09:09)

2026-08-12 18:07:21

开始直接看了弘连取证工具的 APP 安装卸载详情

其中记录的蝙蝠 app 的安装记录时间,但是不太对,可能这不是首次请求安装的时间,还是要看源文件(上图红色箭头)

在源日志文件中搜索蝙蝠 app 的包名 com.zsim.bzychat

按照题目要求找到第一次请求包名的记录,时间为 Aug 12 10:07:21 2026,注意要加 8 小时转换为 UTC+8 时间,即 2026-08-12 18:07:21

24. 嫌疑人手机在 2026-07-09 第一次解锁屏幕是由哪个应用/服务触发的?请写出应用包名。(答案格式:www.tuojieinfo.com)

com.apple.camera

上网找了别的师傅的题解,答案还在之前那个充电记录的数据库 /logs/powerlogs/powerlog_2026-09-23_14-24_07CAD94F.PLSQL 中

首先要定位哪个表可能记录了屏幕状态

select name from sqlite_master
where type = 'table' and name like '%screen%';

定位到 PLScreenStateAgent_EventForward_ScreenState 表很可疑

回到取证工具找到对应表,转换一下时间戳

2026-07-09 当日比较早的屏幕记录主要来自下面三个应用/服务

  • com.apple.lock-screen:锁屏
  • com.apple.camera:相机
  • com.apple.springboard.home-screen:桌面

综合考虑下来相机比较合理


有没有佬知道为什么我第 11 题的 CyberChief 算 sm3 哈希转了这么久

还有第 13 题,有没有什么好的工具可以解析 sgv 文件

( ̄▽ ̄;)ゞ

转载声明:本文转载自原发布平台 (作者:正在思考ing), 原文标题《2026 精武杯 手机取证部分》, 查看原文。 版权归原作者及原发布平台所有,本站仅作收录与展示,未对正文内容作实质性修改; 若涉及侵权请联系本站处理。