一、手机取证
1.请分析韦明辉手机,韦明辉手机的安卓ID是?[答案格式:1153f6447b96d02a]

问韦明辉的手机安卓ID,直接看基本信息就好了,直接就能看到
2.请分析韦明辉手机,韦明辉曾搜索过关于电脑远控的记录,请问使用的什么浏览器?[答案格式:谷歌]

这边也没什么别的浏览器,这边一看就是百度
而且包名也是百度的,所以本题答案为百度
3.请分析韦明辉手机,韦明辉曾用过一款笔记app,请问笔记应用的包名是?[答案格式:com.app.app]

要说的话这边笔记软件真是多的离谱,我给全部仿真了一遍,拿到了很多碎片化的内容
不过最后确定这一个的原因还是因为apk一开始一直在问这个笔记应用

不难看出这个笔记应用起码得是个需要登录的软件


因此别的上边这些都能排除

锁定apk应该是这个NoteVault,唯一一个需要登录的笔记apk软件,所以本题的包名为com.notevault.app
4.分析陈志鹏手机,手机的IMEI是?[答案格式:2342342323232]
依旧签到题

锁定是陈的手机之后直接查看基本信息即可
所以本题为7da9f4e846c8f493
5.分析陈志鹏手机,陈志鹏曾使用过一款隐私笔记app,该应用的包名是?[答案格式:com.app.app]
问隐私笔记,也是找了很久

毕竟软件是真的不少
直到最后一个个试完发现都没什么差别,不应该是这样子
最后发现有一个apk叫hidden

还是个flutter做的,很符合出题的情况,我们看看内容

发现还真是个加密的笔记数据库

符合apk中对于隐私笔记的描述,这个数据库确实是一个加密的,而且是一个隐私app

还能找到一个密码提示文档

更能坐实,本题的答案就是这个com.hidden.calculator
二、APK取证
1.请分析韦明辉手机,笔记应用的用户密码是?[答案格式:Abc123456]
这题其实隐含着这个笔记应用需要写账密的意思

因此我们可以从这一大堆的笔记软件中锁定这题说的到底是哪一个
因为别的打开仿真完基本上都能直接看见内容,工作笔记有一个加密部分但是不存在账密这一说
于是我们最终锁定这个应用NoteVault

然后就是找账密了,其实不难找,但是难爆破

我们在配置文件轻松找到盐和密码的哈希,用户名是weiweiwei,但是密码是什么呢,显然没有,需要我们爆破
这边比赛确实没爆出来,数字都尝试过了,最后发现是和参考格式一样的1大写+2小写+6数字
hashcat -m 1410 -a 3 hash.txt ?u?l?l?d?d?d?d?d?d

得到密码是Wei123123

直接仿真后登录即可,所以本题答案是Wei123123
2.请分析韦明辉手机,笔记应用中,公共笔记有几条?[答案格式:1]
要是前边爆破了的话,直接看看就直接知道了

显然是3条
但是我们还是以没有成功看一遍
先分析该应用,刚刚已经定位了,我们直接去文件夹看看,发现存在database数据库

但是是加密数据库,我们也找不到更多的信息,只能先去Jadx进行分析

直接搜索文件名,可以定位到SqliteDbManager
信息量很大这个地方

这直接把数据库密码明文标注出来了,那我们解密试试看NoteVault_DB_SecureKey_2024

成功解密,所以这边我们只要看看有几条就好了

明显是3条,答案为3
3.请分析韦明辉手机,笔记应用公共笔记数据库名称是?[答案格式:adb.db]

上一题已经确定了数据库名字是notevault.db了
4.请分析韦明辉手机,存储公开笔记的数据库密码是?[答案格式:Abc_ABC_1234]
第二题也分析过了,就明文写在Jadx里边

而且我们试过的确可以解密,所以密码就是NoteVault_DB_SecureKey_2024
5.分析韦明辉手机中笔记应用,给出电脑c盘的恢复秘钥的前6位?[答案格式:1234565]

笔记直接写了
但是万一没有爆破成功呢?我们看看没爆破出来的做法

根据题目意思,明显就是数据库内容,不难发现笔记内容都被加密了,我们先解密
根据数据库里边的内容其实有点怀疑是AES加密,像是AES-GCM,我们直接搜索看看(其实软件打开写了是AES加密)

定位到主要的加密逻辑类AESUtil


在这边我们可以看到加密的过程,确实是AES-GCM,看上边这张图片就可以发现密文其实是salt + iv + encryptedBytes的base64这样子存起来的

decrypt写的很清楚了需要切片
salt = raw[:16]
iv = raw[16:28]
ciphertext_tag = raw[28:]
所以我们大概解密的流程就是
数据库字段密文
↓
Base64 解码
↓
前 16 字节 = salt
↓
第 16 到 28 字节 = iv
↓
第 28 字节到最后 = ciphertext + GCM tag
↓
用 NoteVault_SecureKey_2024 + salt
↓
PBKDF2WithHmacSHA256 迭代 10000 次
↓
生成 256 bit AES key
↓
AES/GCM/NoPadding 解密
↓
得到 UTF-8 明文
由此写得脚本
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
NoteVault DB + AES-GCM encrypted-field batch decryptor.
For this sample:
DB password = NoteVault_DB_SecureKey_2024
field master key = NoteVault_SecureKey_2024
It first decrypts the SQLCipher-v4 database into a temporary normal SQLite DB,
then decrypts TEXT columns whose names end with '_encrypted'.
"""
import argparse
import base64
import csv
import json
import os
import sqlite3
import tempfile
from pathlib import Path
from typing import Dict, List, Any
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
def pbkdf2(password: bytes, salt: bytes, iterations: int, length: int, algorithm) -> bytes:
return PBKDF2HMAC(
algorithm=algorithm,
length=length,
salt=salt,
iterations=iterations,
backend=default_backend(),
).derive(password)
def decrypt_sqlcipher_v4(
encrypted_db: str,
db_password: str,
out_db: str,
page_size: int = 4096,
reserve: int = 80,
kdf_iter: int = 256000,
) -> None:
"""
Pure-Python SQLCipher v4 default decrypt.
Defaults used by SQLCipher 4:
KDF: PBKDF2-HMAC-SHA512, 256000 iterations, 32-byte key
Cipher: AES-256-CBC
page reserve: 80 bytes, 16-byte IV at the page trailer
The first 16 bytes of page 1 are the SQLCipher salt. The normal SQLite
header 'SQLite format 3\0' is restored after decryption.
"""
data = Path(encrypted_db).read_bytes()
if len(data) < page_size or len(data) % page_size != 0:
raise ValueError(f"File size {len(data)} is not a multiple of page_size={page_size}")
salt = data[:16]
key = pbkdf2(
db_password.encode("utf-8"),
salt,
kdf_iter,
32,
hashes.SHA512(),
)
out = bytearray()
for pgno, off in enumerate(range(0, len(data), page_size), start=1):
page = data[off : off + page_size]
if pgno == 1:
enc_offset = 16
prefix = b"SQLite format 3\x00"
enc_len = page_size - 16 - reserve
else:
enc_offset = 0
prefix = b""
enc_len = page_size - reserve
if enc_len <= 0 or enc_len % 16 != 0:
raise ValueError("Invalid encrypted page length; check page_size/reserve")
ciphertext = page[enc_offset : enc_offset + enc_len]
iv = page[enc_offset + enc_len : enc_offset + enc_len + 16]
decryptor = Cipher(
algorithms.AES(key),
modes.CBC(iv),
backend=default_backend(),
).decryptor()
plaintext_part = decryptor.update(ciphertext) + decryptor.finalize()
plain_page = prefix + plaintext_part + bytes(reserve)
if len(plain_page) != page_size:
raise RuntimeError(f"Bad page length at page {pgno}: {len(plain_page)}")
out.extend(plain_page)
Path(out_db).write_bytes(out)
# Verify the result is a readable SQLite database.
con = sqlite3.connect(out_db)
try:
result = con.execute("PRAGMA integrity_check").fetchone()[0]
if result.lower() != "ok":
raise ValueError(f"SQLite integrity_check failed: {result}")
finally:
con.close()
def decrypt_field_aes_gcm(value: str, master_password: str) -> str:
"""
Field format:
base64( salt[0:16] || iv[16:28] || ciphertext+tag[28:] )
KDF:
PBKDF2WithHmacSHA256, iterations=10000, key length=256 bits
Cipher:
AES/GCM/NoPadding
"""
raw = base64.b64decode(value)
if len(raw) < 16 + 12 + 16:
raise ValueError("ciphertext too short")
salt = raw[:16]
iv = raw[16:28]
ciphertext_and_tag = raw[28:]
key = pbkdf2(
master_password.encode("utf-8"),
salt,
10000,
32,
hashes.SHA256(),
)
plaintext = AESGCM(key).decrypt(iv, ciphertext_and_tag, None)
return plaintext.decode("utf-8", errors="replace")
def get_tables(con: sqlite3.Connection) -> List[str]:
rows = con.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
).fetchall()
return [r[0] for r in rows]
def batch_decrypt(sqlite_db: str, master_password: str) -> List[Dict[str, Any]]:
con = sqlite3.connect(sqlite_db)
con.row_factory = sqlite3.Row
output: List[Dict[str, Any]] = []
try:
for table in get_tables(con):
info = list(con.execute(f'PRAGMA table_info("{table}")'))
columns = [row[1] for row in info]
col_types = {row[1]: (row[2] or "").upper() for row in info}
encrypted_cols = [
c for c in columns
if c.endswith("_encrypted") and ("TEXT" in col_types.get(c, "") or "CHAR" in col_types.get(c, ""))
]
if not encrypted_cols:
continue
for row in con.execute(f'SELECT * FROM "{table}"'):
item: Dict[str, Any] = {"table": table}
for c in columns:
item[c] = row[c]
for c in encrypted_cols:
plain_name = c[: -len("_encrypted")]
if not isinstance(row[c], str):
continue
try:
item[plain_name] = decrypt_field_aes_gcm(row[c], master_password)
except Exception as e:
item[plain_name] = f"<DECRYPT_FAILED: {type(e).__name__}: {e}>"
output.append(item)
finally:
con.close()
return output
def write_csv(rows: List[Dict[str, Any]], csv_path: str) -> None:
fieldnames: List[str] = []
for row in rows:
for key in row.keys():
if key not in fieldnames:
fieldnames.append(key)
with open(csv_path, "w", newline="", encoding="utf-8-sig") as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(rows)
def main() -> None:
parser = argparse.ArgumentParser(description="Decrypt NoteVault SQLCipher DB and AES-GCM fields")
parser.add_argument("db", help="encrypted notevault.db path")
parser.add_argument("--db-pass", default="NoteVault_DB_SecureKey_2024", help="SQLCipher database password")
parser.add_argument("--master-pass", default="NoteVault_SecureKey_2024", help="AES-GCM field master password")
parser.add_argument("--plain-db", default="notevault_plain.db", help="output decrypted SQLite database path")
parser.add_argument("--csv", default="notevault_decrypted.csv", help="output CSV path")
parser.add_argument("--json", default="notevault_decrypted.json", help="output JSON path")
args = parser.parse_args()
decrypt_sqlcipher_v4(args.db, args.db_pass, args.plain_db)
rows = batch_decrypt(args.plain_db, args.master_pass)
write_csv(rows, args.csv)
Path(args.json).write_text(json.dumps(rows, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"[+] decrypted sqlite: {args.plain_db}")
print(f"[+] decrypted csv: {args.csv}")
print(f"[+] decrypted json: {args.json}")
print(json.dumps(rows, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()

解密得到三条公开笔记的内容
恢复秘钥:C盘恢复秘钥:067474-555071-622369-111650-651354-121858-406439-542289
password:数字
虚拟币地址:6273XY7F87XX
所以这题答案就是067474
6.请分析韦明辉手机,笔记应用隐私空间的密码是?[答案格式:123456]

其实私密空间密码在配置文件里边直接就能看见一个字符串,但是显然不会那么简单

直接搜索发现在UserPreference

进来发现是走的AESUtil
也就是说我们在刚刚的类AESUtil往下划,直接就能看见对于隐私空间的加解密

发现了密文格式在这边是Base64(salt+iv+encryptedBytes)

我们在数据库里还看见了config_key和config_value,所以
密文 = private_space_password
解密口令 = private_space_key
看这个Jadx也能明确,密钥派生依旧是PBKDF2WithHmacSHA256然后迭代10000次
import base64
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
def decrypt_with_password(cipher_b64: str, password: str) -> str:
raw = base64.b64decode(cipher_b64)
salt = raw[:16]
iv = raw[16:28]
ciphertext_and_tag = raw[28:]
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32, # 256 bit = 32 bytes
salt=salt,
iterations=10000,
)
key = kdf.derive(password.encode("utf-8"))
aesgcm = AESGCM(key)
plaintext = aesgcm.decrypt(iv, ciphertext_and_tag, None)
return plaintext.decode("utf-8")
if __name__ == "__main__":
private_space_password = (
"zkTkORgxvbRQI9ilBSelZ172slPhlMGkkZFy7oCOb+NJxOgV0OHi5GK9hKoV0hNsD3s/"
)
private_space_key = "e7GVttnoeahmWeFc"
result = decrypt_with_password(private_space_password, private_space_key)
print("[+] private_space_password =", result)

直接解得密码是8374723
7.请分析韦明辉手机,隐私空间笔记内容使用的什么数据库存储?[答案格式:sqlite]

我们很容易定位到这个隐私部分的数据库
![](/editor-assets/2026/06/