首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
Android安全
发新帖
3
30
[原创]某海外电商app设备指纹收集与加密算法分析
发表于: 15小时前
474
[原创]某海外电商app设备指纹收集与加密算法分析
乐子人
4
15小时前
474
# 某海外电商app设备指纹收集与加密算法分析 最近好像比较流行用ai水文章,我也用ai水几篇。这份是用ai自动分析了某电商的设备指纹函数trace。将3GB的trace文件交给ai自动分析后的结果。 我只使用[QTrace](https://bbs.kanxue.com/thread-290574.htm)对关键的指纹收集函数做trace,然后将trace文件直接交给ai自动分析,其余的不做干预。总共分析了不到20分钟  报告包含完整的指纹采集点,源码级实现逻辑,和数据加密方式。 ## 报告说明 本报告中的结论分为三类: 1. **运行时直接观察**:来自 QBDI trace 中的 JNI、libc、字符串和内存操作。 2. **静态确认**:来自 `libriver.so` ARM64 反汇编,以及 APK 中 `ri.bg`、`s13.c` 等 Java 类反编译。 3. **未完整覆盖的上游输入**:trace 仅记录到最终值,但没有覆盖其 producer。源码通过 `AppInputs` 明确接入,绝不使用猜测逻辑冒充原实现。 所谓“源码级等价实现”,指在相同 Android 版本、相同应用 UID、相同私有数据、相同运行时 block list 和相同实时设备状态下,按 trace 观察到的 API、顺序、格式及编码方式重现采集。时间、随机数、内存、电池、网络、sequence 和密文按设计会随会话变化。 --- ## 1. 样本与结论 - Trace:`D:\tomu\tomu_env_9.trace`,3,645,346,354 字节,QBDI ARM64 逐指令 trace。 - 对应 native 模块:`D:\tomu\tomu\split_config.arm64_v8a\lib\arm64-v8a\libriver.so` - SHA-256:`2774ae72fd2c42a0857942a1fe187e46f807d4ee61dfe38dcdbbbbc81d297e08` - trace 入口配置:`D:\tomu\tea_trace\nativelib\src\main\cpp\native_main.cpp:91`,`trace_func = 0x2604ac`。 - 最终明文是 URL query-string;加密结果是一个二进制信封,经标准 Base64 后包装为: ```json {"encryptedData":"<standard-base64>"} ``` 本次实际结果:Base64 长 14,500,解码后 10,873 字节。 **加密算法链已经可以确定为:随机临时密钥材料 + 32 字节共享秘密派生 + SHA-256/HMAC/HKDF 类 KDF + ChaCha20-Poly1305 风格 AEAD 信封。** 最后一层的结构与长度关系完全吻合 `12-byte nonce + ciphertext + 16-byte tag`;加密数据不是 AES 分组密文(总长不是 16 的倍数)。 > 注意:该包高度控制流混淆,静态伪代码中的函数名均为本文按行为命名。算法流程和长度关系为高置信度;固定服务端公钥/协议常量仍应从当前版本运行时内存提取,而不能凭空写死。 --- ## 2. 两层明文组装 ### 2.1 第一层:`p1`~`p21` 实际顺序严格为: ```text p1=...&p2=...&p3=...&p4=...&p5=...&p6=...&p7=...&p8=...&p9=... &p10=...&p11=...&p12=...&p13=...&p14=...&p15=...&p16=... &p17=...&p18=...&p19=...&p20=...&p21=... ``` 每个值先按 `application/x-www-form-urlencoded` 规则转义(空格变 `+`,保留字符按 UTF-8 `%HH` 编码),再以 `&` 连接。 | 参数 | 本次值/含义 | 具体采集方法 | |---|---|---| | `p1` | `{"vInfo":{"exits":0,"id":""},"systemService":"[]"}` | `android.os.ServiceManager.listServices()`;结合厂商/虚拟化关键字检查。trace 中枚举了 284 个 service,并逐个与 `oppo/miui/vivo/.../xposed/libsu` 等比较。| | `p2` | `tomu_secure_v2` Android Keystore 证书链,4 张证书以 `;` 拼接 | `KeyStore.getInstance("AndroidKeyStore")` → `load(null)` → `containsAlias("tomu_secure_v2")` / 回退 `tomu_secure` → `getCertificateChain()` → 每张 `X509Certificate.getEncoded()` → Base64。| | `p3` | `-4` | 通过 `ServiceManager.getService("package")` 及 `IPackageManager.Stub.TRANSACTION_isFirstBoot` 等系统状态检测所得状态码。| | `p4` | JSON,键 `1` 为 `/proc/version`,键 `2` 为 `uname -a` | 读取内核版本;trace 明确执行 `uname -a`。| | `p5` | 空 | 本次未取得。| | `p6` | 空 | 本次未取得。| | `p7` | 空 | 本次未取得。| | `p8` | `429` | `Settings.Global/Secure.getString(contentResolver, "boot_count")`。| | `p9` | `中國移動香港 China Mobile HK` | SIM/运营商信息。| | `p10` | `3240a81b31837bae` | `ContentResolver.call(content://settings/secure, "GET_secure", "android_id", null)`,再从 Bundle 取 `value`。| | `p11` | 空 | 本次未取得。| | `p12` | `包名|标签|签名摘要|版本码|flags` | `Telephony.Sms.getDefaultSmsPackage()`;PackageManager 查询默认短信应用;`Signature.toCharsString()` 后调用 Java `ri/bg.dg(...)` 得到 `C2C258...` 摘要;本次为 Google Messages。| | `p13` | `Built-in Screen|null;` | `DisplayManager.getDisplays()`、`Display.getName()`;反射 `Display.mDisplayInfo.ownerPackageName`。| | `p14` | `null` | 当前包名及 `PackageManager.getInstallerPackageName()`;本次安装来源为空。| | `p15` | `900063915` | 安装来源/包环境派生的整数标识;trace 能确认其位置和值,当前证据不足以给该整数命名为某个公开 Android 字段。| | `p16` | 六类音量 JSON | `AudioManager.getStreamMinVolume/getStreamMaxVolume/getStreamVolume`,包含 `voiceCall/system/ring/music/alarm/notification`。| | `p17` | `en_CA` | Resources → Configuration → LocaleList → 首个 Locale 的 language/country。| | `p18` | `Asia/Shanghai GMT+08:00` | `TimeZone.getDefault().getID()` 和 `getDisplayName()`。| | `p19` | 空 | 尝试反射 `com.android.internal.os.PowerProfile.sPowerItemMap`,本次无值。| | `p20` | `SM7250` | SoC/硬件平台信息。| | `p21` | 空 | 查询 `Settings` 的 `ad_aaid`,本次为空。| ### 2.2 第二层:主环境 query 第一层整体又作为一个字段嵌入第二层。trace 中完整观察到以下字段与顺序: ```text version=35 process_id=8484 availablecapacity=78084268032 input_device=4|sec_touchscreen|0fa9d3d0d40f7f5b316320876742e00ad5eb1ba9|4355; machine_arch=ARM fk_result={"vInfo":{"exits":0,"id":""},"antInfo":{"exits":0}} 0c1999= foreground=true uuid=5951dc94-f0fc-45ee-a235-9befbeb1f365 secure_lock=0 totalcapacity=117153181696 mediaDrm=0:07c0...a2af1|Google|17.0.0|Widevine CDM; c86fee=4.61.0 instrumentation=android.app.Instrumentation u2=<另一个已加密并 Base64 的大字段> ab9de2=1 060a12= net_type=WIFI 01bf33= currentTime=1784689721004 availablememory=3429867520 brightness=44 a5=1 74b954=36 kernelVersion=<Linux /proc/version> eeba5e=en 02b81e=Asia/Shanghai dme=1 ``` 随后继续追加: | 字段 | 实际内容/采集方式 | |---|---| | `platform_type` | `1` | | `sharedpreference_id` | `SharedPreferences("se_config").getString("secure_sharedpreference_id", "")`;本次 `1cda03acb0484d718ac373ad6cd91475`。| | `dpi` | `DisplayMetrics.densityDpi`;本次 440。| | `resolution` | `widthPixels + "x" + heightPixels`;本次 `1080x2340`。| | `uid` | 当前 UID/派生值;本次为空。| | `cookie` | 本次 `Cm3AFGpE44VWp0sh8sa0Ag==`,24 字符标准 Base64,解码 16 字节。| | `root` | 检查 `/product/bin/su`、`/sbin/su`、`/system/bin/su`、`/system/sbin/su`、`/system/xbin/su`、`/vendor/bin/su`;并检查 `/data/misc/riru`。| | `fingerprint` | `Build.FINGERPRINT`;本次 `google/redfin/redfin:14/UP1A.../release-keys`。| | `build_id` | `UP1A.231105.001.B2`。| | `rom_status` | ROM/启动状态。| | `cpuinfo` | CPU 硬件信息;本次可见 `Qualcomm Technologies, Inc LITO`。| | `input_mathod` | `Settings.Secure["default_input_method"]`,本次 Google LatinIME。| | `battery_status` | 注册 `ACTION_BATTERY_CHANGED`,遍历 extras 全部键值;含 technology、health、status、plugged、present、charge_counter、level、scale、temperature、voltage、cycle count 等。| | `install_time` | PackageInfo.firstInstallTime。| | `app_update_time` | PackageInfo.lastUpdateTime。| | `boot_time` | `/proc/sys/kernel/random/boot_id` 及系统时间/文件时间组合。| | `ip_list` | 枚举 `NetworkInterface`,仅取 up 的接口,再枚举非 loopback 地址;本次包括 rmnet_data0/dummy0/wlan0 的 IPv6 和 wlan0 IPv4。| | `total_memory` | `/proc/meminfo`;本次 `7823970304`。| | `sequence` | 本次 0。| | `local_sequence` | `SharedPreferences("se_config")["secure_local_sequence_id"]`,取出后递增并 `apply()`;本次送出 627。| | `did_info` | 多个时间、文件状态、boot/device ID 等以 `;` 拼接并 URL 编码。| | `simState` | SIM 状态;本次 0。| | `bgid` | 设备/业务标识,本次为空。| 此外还观察到: - 可用/总存储、可用内存; - 屏幕亮度; - 当前前后台; - 网络类型; - MediaDrm Widevine vendor/version/description 和设备 ID 摘要; - 输入设备列表及 descriptor/vendor/product; - 当前 Instrumentation 类; - Root/Riru/Xposed/ROM 厂商痕迹; - 包安装、默认短信应用、显示设备、音量、区域、时区、SoC、内核信息。 --- ## 3. 明文组装的准确逻辑 ```cpp string form_encode(const vector<pair<string,string>>& fields) { string out; for (size_t i = 0; i < fields.size(); ++i) { if (i) out += '&'; out += url_encode(fields[i].first); // 字段名本身通常无需变化 out += '='; out += url_encode_utf8(fields[i].second); // 空格 -> '+',其余按 %HH } return out; } string build_p_block() { // 必须保持 p1 ... p21 的原始顺序;空值仍输出 key= return form_encode({ {"p1", collect_service_and_virtualization()}, {"p2", collect_keystore_cert_chain()}, {"p3", collect_first_boot_state()}, {"p4", collect_kernel_json()}, {"p5", ""}, {"p6", ""}, {"p7", ""}, {"p8", settings_boot_count()}, {"p9", sim_operator_display()}, {"p10", android_id_via_content_resolver_call()}, {"p11", ""}, {"p12", default_sms_package_record()}, {"p13", display_record()}, {"p14", installer_package_or_null()}, {"p15", installer_environment_code()}, {"p16", audio_stream_json()}, {"p17", locale_language_country()}, {"p18", timezone_id_and_name()}, {"p19", power_profile_info()}, {"p20", soc_model()}, {"p21", settings_ad_aaid()} }); } string build_plaintext() { vector<pair<string,string>> f; // 按 trace 中固定顺序 push version ... dme // 再 push platform_type ... bgid;空值不能省略。 // 某些复杂子结构先自行用 |、;、, 拼接,再作为一个整体 URL 编码。 return form_encode(f); } ``` 这里最重要的是:**顺序固定、空字段保留、复杂字段先内部拼接后整体 URL 编码**。改变顺序或省略空字段会改变最终密文和服务端解释。 --- ## 4. 加密信封 ### 4.1 二进制布局 trace 显示明文加密前长度为 `0x2A1B = 10779` 字节;固定/AAD 头长度为 `0x52 = 82` 字节。最终长度: ```text 82 + 12 + 10779 + 16 = 10873 ``` 与实际 Base64 解码后的 10,873 字节完全一致。因此布局为: ```text offset size 含义 0 82 protocol header / AAD 82 12 AEAD nonce 94 plaintext_length ciphertext(与明文等长) 94+N 16 authentication tag ``` `sub_2D9794`(`0x2D9794`)直接给出组包公式: ```cpp out_len = plaintext_len + 16 + aad_len + 12; out = new byte[out_len]; AEAD_encrypt(key32, plaintext, plaintext_len, aad82, 82, nonce12, ciphertext_and_tag); memcpy(out, aad82, 82); memcpy(out + 82, nonce12, 12); memcpy(out + 94, ciphertext_and_tag, plaintext_len + 16); ``` ### 4.2 82 字节 header 的内部布局 `sub_2EBF5C`(`0x2EBF5C`)逐字节构造: ```text 0x00 2 协议版本/算法标记(两个固定字节) 0x02 32 临时公钥/公钥材料 A 0x22 32 随机 salt/公钥材料 B 0x42 8 64 位时间/随机数,按固定端序写入 0x4A 8 调用方传入的 64 位请求标识 总计 0x52 (82) ``` 对应伪代码: ```cpp header[0] = PROTOCOL_BYTE_0; header[1] = PROTOCOL_BYTE_1; memcpy(header + 2, ephemeral_public_or_material_A, 32); memcpy(header + 34, random_salt_or_material_B, 32); store_u64_protocol_endian(header + 66, generated_u64); memcpy(header + 74, &request_id_u64, 8); ``` ### 4.3 密钥派生 `sub_2EBF5C` 的数据流: 1. `sub_EABBC(..., 32)` 从安全随机源生成 32 字节临时私钥/随机数;trace 同时观察到 `/dev/urandom`。 2. `sub_E0544` 从该 32 字节值计算 32 字节公钥材料。 3. `sub_E0D40` 用该临时私钥和调用方传入的 32 字节公钥计算 32 字节共享秘密。这组 32-byte 输入/输出及调用关系符合 X25519/ECDH 类交换。 4. 再生成 32 字节随机 salt。 5. `sub_2EC39C` → `sub_EA4AC` / `sub_EA7D4`,基于 SHA-256/HMAC 状态把共享秘密和 salt 派生成最终 32 字节 AEAD key,行为符合 HKDF-SHA256 的 extract/expand。 ```cpp uint8_t eph_private[32] = secure_random_32(); uint8_t eph_public[32] = x25519_public(eph_private); uint8_t shared[32] = x25519(eph_private, server_public_key); uint8_t salt[32] = secure_random_32(); uint8_t key[32] = hkdf_sha256(shared, salt, /*info=*/empty, 32); ``` ### 4.4 AEAD 及 Base64/JSON ```cpp vector<uint8_t> encrypt_envelope( bytes plaintext, uint64_t request_id, bytes32 server_public_key) { bytes32 sk = secure_random_32(); bytes32 pub = x25519_public(sk); bytes32 salt = secure_random_32(); bytes32 ss = x25519(sk, server_public_key); bytes32 key = hkdf_sha256(ss, salt, {}, 32); byte aad[82]; aad[0] = PROTOCOL_BYTE_0; aad[1] = PROTOCOL_BYTE_1; memcpy(aad + 2, pub, 32); memcpy(aad + 34, salt, 32); store_u64_protocol_endian(aad + 66, generated_u64()); memcpy(aad + 74, &request_id, 8); byte nonce[12] = secure_random_12(); auto [ciphertext, tag16] = chacha20_poly1305_encrypt( key, nonce, plaintext, /*AAD=*/aad); return concat(aad, nonce, ciphertext, tag16); } string final_result = "{\"encryptedData\":\"" + base64_standard(envelope) + "\"}"; ``` `sub_150144` / `sub_14EAC0` 是标准 Base64 编码路径;trace 最终明确按 `"encryptedData":"` + Base64 + `"}` 组装。 --- ## 5. 可复核的关键函数地址 | 地址 | 本文命名 | 作用 | |---|---|---| | `0x2EAB50` | top-level encrypt | 调用密钥协商、AEAD、Base64。| | `0x2EBF5C` | build crypto context/header | 生成临时密钥、共享秘密、salt、82-byte header。| | `0x2EC39C` | derive key | 调用 HMAC/HKDF 路径生成 32-byte key。| | `0xEABBC` | secure random | 生成随机字节。| | `0xE0544` | public-key derivation | 32-byte 私钥 → 32-byte 公钥材料。| | `0xE0D40` | shared-secret | 32-byte × 32-byte → 32-byte 共享秘密。| | `0xEA4AC` / `0xEA7D4` | HMAC/HKDF | SHA-256/HMAC 状态与 32-byte 输出。| | `0x2D9794` | AEAD envelope | 12-byte nonce、16-byte tag、AAD 前缀组包。| | `0xD9E38` | AEAD core wrapper | 对明文执行认证加密。| | `0x150144` / `0x14EAC0` | Base64 | 二进制信封编码为标准 Base64。| --- ## 6. 准确性边界 已由 trace **直接观察并确认**:字段名称、字段顺序、大部分字段值、JNI/系统 API、URL 编码后的两层明文、明文长度、82/12/16 字节结构、最终 Base64 和 JSON 包装。 由反汇编与长度关系**高置信度识别**:X25519/ECDH 类 32-byte 密钥交换、HKDF-SHA256 类派生、ChaCha20-Poly1305 风格 AEAD。 当前证据中不能负责任地写死:两个协议字节、服务端长期公钥、header 两个 32-byte 槽位的官方字段名,以及少数混淆字段(如 `p15`)的官方业务命名。它们随版本/会话变化,正确实现应从调用参数和运行时产生,而不是复制本次样本值。 --- # 逐字段源码实现映射 ## p1–p21 | 字段 | Java 等价实现 | |---|---| | p1 | `collectP1ServiceProbe()`:反射 `ServiceManager.listServices()`,按 trace 中关键字扫描,生成带 `exits` 拼写的 JSON。| | p2 | `collectP2KeyStoreChain()`:读取 `AndroidKeyStore` 的 `tomu_secure_v2`,回退 `tomu_secure`,证书 DER 标准 Base64 后用 `;` 连接。| | p3 | `AppInputs.p3()`:trace 只覆盖结果 `-4`,未覆盖其完整 Binder producer,不伪造。| | p4 | `collectP4KernelJson()`:`/proc/version` 与 `uname -a`。| | p5/p6/p7 | `AppInputs`;样本为空。| | p8 | `getSetting(...,"boot_count")`。| | p9 | `AppInputs.simOperatorDisplay()`;样本为运营商显示文本,但 trace 未覆盖其上游 Telephony 调用。| | p10 | `getAndroidIdViaProviderCall()`,与 trace 的 `content://settings/secure` / `GET_secure` / `android_id` / `value` 一致。| | p11 | `AppInputs.p11()`;样本为空。| | p12 | `collectDefaultSms()`;默认短信包、label、`MD5(Signature.toCharsString UTF-8)` 大写十六进制、version、flags。| | p13 | `collectDisplays()`;DisplayManager、名称、反射 `mDisplayInfo.ownerPackageName`。| | p14 | `getInstallerPackage()`。| | p15 | `AppInputs.p15()`;样本值 900063915,但 producer 未在 trace 覆盖。| | p16 | `collectAudioJson()`;六个 AudioManager stream 的 min/max/current。| | p17 | `collectLocale()`。| | p18 | `collectTimeZone()`。| | p19 | `AppInputs.p19PowerProfile()`;trace 尝试 hidden `PowerProfile.sPowerItemMap`,样本为空。| | p20 | `collectSoc()`;优先 `Build.SOC_MODEL`,回退系统属性。| | p21 | `getSetting(...,"ad_aaid")`。| 每个 `pN` 在 native 执行前还通过 APK 方法 `ri.bg.cb("pN")` 查询运行时 block list。调用者如需精确复现字段屏蔽,应在 `buildPBlock` 前应用同一 block list。 ## Java 主 query | 字段 | Java 等价实现/来源 | |---|---| | version | `Build.VERSION.SDK_INT` | | process_id | `Process.myPid()` | | availablecapacity | `StatFs(data).getAvailableBytes()` | | input_device | `collectInputDevices()` | | machine_arch | `Build.SUPPORTED_ABIS[0]` 归一为 ARM/X86 | | fk_result | `AppInputs.fkResult()` | | 0c1999 | `AppInputs.field0c1999()` | | foreground | `ActivityManager.getMyMemoryState()` | | uuid | `AppInputs.uuid()`,必须是 Tomu 持久化 UUID,不是临时随机 UUID | | secure_lock | `KeyguardManager.isDeviceSecure()` | | totalcapacity | `StatFs(data).getTotalBytes()` | | mediaDrm | `collectWidevine()`:Widevine deviceUniqueId SHA-256 小写 hex、vendor/version/description | | c86fee | 当前 app versionName | | instrumentation | `ActivityThread.mInstrumentation.getClass().getName()` | | u2 | `AppInputs.u2()`,独立加密 token producer 不在本 trace | | ab9de2 | 常量 `1` | | 060a12 | `AppInputs.field060a12()` | | net_type | `ConnectivityManager` / `NetworkCapabilities` | | 01bf33 | `AppInputs.field01bf33()` | | currentTime | `System.currentTimeMillis()` | | availablememory | `ActivityManager.MemoryInfo.availMem` | | brightness | `Settings.System.SCREEN_BRIGHTNESS` | | a5 | `AppInputs.fieldA5()` | | 74b954 | `AppInputs.field74b954()` | | kernelVersion | `/proc/version` | | eeba5e | `Locale.getDefault().getLanguage()` | | 02b81e | `TimeZone.getDefault().getID()` | | dme | `AppInputs.dme()` | ## Native 后续字段(trace 中的实际顺序) | 字段 | Java 等价实现/来源 | |---|---| | platform_type | 常量 `1` | | sharedpreference_id | `se_config/secure_sharedpreference_id` | | dpi | `DisplayMetrics.densityDpi` | | resolution | `widthPixels + "x" + heightPixels` | | uid | `AppInputs.uidField()` | | cookie | `AppInputs.cookie()` | | cpuinfo | `/proc/cpuinfo` Hardware/model name | | build_id | `Build.ID` | | fingerprint | `Build.FINGERPRINT` | | battery_status | `collectBattery()`:ACTION_BATTERY_CHANGED extras 的 `key,value,` 串 | | boot_time | `collectBootInfo()`;当前实现覆盖明确观察到的 boot_id,完整复合值属于 did/文件状态路径 | | root | `collectRoot()`:su/Riru 文件检查 | | rom_status | `AppInputs.romStatus()` | | install_time | `PackageInfo.firstInstallTime` | | app_update_time | `PackageInfo.lastUpdateTime` | | input_mathod | `Settings.Secure.DEFAULT_INPUT_METHOD`,字段拼写故意保留 | | ip_list | `collectIpList()`:up interface、非 loopback 地址,`;` 结尾 | | total_memory | `/proc/meminfo` MemTotal × 1024 | | sequence | `AppInputs.sequence()` | | local_sequence | `nextLocalSequence()`:读当前值、写 `current+1`、返回当前值 | | did_info | `AppInputs.didInfo()`;trace 显示复合值,但未覆盖所有 producer | | simState | `TelephonyManager.getSimState()` | | bgid | `AppInputs.bgid()` | ## 为什么部分字段必须作为输入 “能编译”不等于“准确”。对 trace 没有执行到 producer 的字段,写一个看似合理的 Android API 会制造错误结论。因此源码把这些字段显式定义成 `AppInputs`:调用方必须接入原始 Tomu 模块或继续针对相应 producer 补 trace。这样已观察到的采集逻辑可以完全重现,而未知项不会被伪造。 --- # 复现工程使用与限制 ## Files - `EnvCollector.java`: source-level Java equivalents for every field whose producer is observable in `tomu_env_9.trace`. - `../tomu_env_9_analysis.md`: field inventory, native encryption call graph and evidence notes. The source was compiled successfully against Android API 37's `android.jar`. ## Usage ```java EnvCollector.AppInputs appInputs = new EnvCollector.Defaults() { @Override public String uuid() { return loadTomuPersistedUuid(); } @Override public String u2() { return generateU2(); } @Override public String cookie() { return loadCookie(); } @Override public String p15() { return calculateP15(); } @Override public String didInfo() { return buildDidInfo(); } }; String pBlock = EnvCollector.buildPBlock(context, appInputs); String main = EnvCollector.buildMainQuery(context, appInputs); String plaintext = EnvCollector.appendNativeFields(context, main, appInputs); ``` `pBlock` is a separate collected block seen in the trace. Its placement inside the product's higher-level request is controlled by the caller. The final native plaintext in this trace is the `main` query followed by the native fields in `appendNativeFields`. ## Exactness boundary A different application cannot completely reproduce Tomu's values merely by calling public Android APIs: 1. `tomu_secure_v2`, `se_config`, the UUID, cookie, `u2`, and related IDs are private to Tomu's UID/data directory. 2. Package visibility and hidden-API policy change the service/package/display results. 3. `ri.bg.cb(field)` can suppress individual `pN` fields using a runtime block list supplied elsewhere in the app. 4. `p3`, `p15`, `u2`, `cookie`, `did_info`, `rom_status`, `sequence`, `bgid`, and several obfuscated fields are supplied by other app/native subsystems. The trace contains their values but does not contain enough producer execution to reconstruct those producers accurately. They are explicit `AppInputs`, rather than fabricated implementations. 5. Time, memory, battery, storage, IP, random, sequence and cryptographic values are intentionally session-dependent. For exact same-device reproduction, execute this code in the original app process/UID and provide the `AppInputs` from the original app modules. Otherwise, the methods reproduce collection semantics but values legitimately differ. ## Important corrected details - `p12` signature hash is **MD5 of `Signature.toCharsString()` UTF-8 bytes**, uppercase hex. This follows APK class `ri.bg.dg -> s13.c.b`, not MD5 of raw certificate bytes. - URL encoding uses Java `URLEncoder` behavior: UTF-8, space to `+`, percent encoding with uppercase hex. - Empty fields are emitted. - Field order is protocol-significant and represented explicitly in source. - Battery extra order follows `Bundle.keySet().iterator()` exactly as observed; Android does not promise this order across versions. --- # 完整 Java 源码:`EnvCollector.java` 以下代码不是摘要,而是报告对应的完整可编译采集实现。已使用 Android API 37 的 `android.jar` 编译验证。 ```java package tomu.reproduction; import android.app.ActivityManager; import android.app.Application; import android.app.KeyguardManager; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.pm.InstallSourceInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.Signature; import android.hardware.display.DisplayManager; import android.media.AudioManager; import android.media.MediaDrm; import android.net.ConnectivityManager; import android.net.Network; import android.net.NetworkCapabilities; import android.net.Uri; import android.os.BatteryManager; import android.os.Build; import android.os.Bundle; import android.os.Debug; import android.os.Environment; import android.os.LocaleList; import android.os.Process; import android.os.StatFs; import android.provider.Settings; import android.provider.Telephony; import android.util.Base64; import android.util.DisplayMetrics; import android.view.Display; import android.view.InputDevice; import android.view.WindowManager; import org.json.JSONObject; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.io.InputStreamReader; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.net.InetAddress; import java.net.NetworkInterface; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.security.KeyStore; import java.security.MessageDigest; import java.security.cert.Certificate; import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.TimeZone; import java.util.UUID; /** * Source-level equivalent of the collectors observed in tomu_env_9.trace. * * This class deliberately uses LinkedHashMap and explicit append order. The native * implementation does not sort fields. Empty fields are emitted rather than omitted. * * Android hidden APIs and package visibility affect several results. Running this in * another package/UID cannot reproduce Tomu's privileged visibility or private files. */ public final class EnvCollector { private static final String PREF = "se_config"; private static final String PREF_DEVICE_ID = "secure_sharedpreference_id"; private static final String PREF_SEQUENCE = "secure_local_sequence_id"; private static final UUID WIDEVINE = new UUID( 0xedef8ba979d64aceL, 0xa3c827dcd51d21edL); private EnvCollector() {} /** Values owned by other Tomu modules or still opaque in the supplied trace. */ public interface AppInputs { String fkResult(); // sample: {"vInfo":...,"antInfo":...} String uuid(); // persisted Tomu UUID, not a fresh UUID each call String u2(); // separate anti-abuse encrypted token String cookie(); // sample is a 16-byte value in standard Base64 String bgid(); String uidField(); String field0c1999(); String field060a12(); String field01bf33(); String fieldA5(); String field74b954(); String dme(); String p3(); // package-service/first-boot status; sample -4 String p5(); String p6(); String p7(); String p11(); String p15(); // sample 900063915; business derivation remains opaque String simOperatorDisplay(); // sample 中國移動香港 China Mobile HK String socModelOverride(); String p19PowerProfile(); String romStatus(); String didInfo(); String sequence(); } public static final class Defaults implements AppInputs { public String fkResult() { return "{\"vInfo\":{\"exits\":0,\"id\":\"\"},\"antInfo\":{\"exits\":0}}"; } public String uuid() { return ""; } public String u2() { return ""; } public String cookie() { return ""; } public String bgid() { return ""; } public String uidField() { return ""; } public String field0c1999() { return ""; } public String field060a12() { return ""; } public String field01bf33() { return ""; } public String fieldA5() { return "1"; } public String field74b954() { return ""; } public String dme() { return "1"; } public String p3() { return "-4"; } public String p5() { return ""; } public String p6() { return ""; } public String p7() { return ""; } public String p11() { return ""; } public String p15() { return ""; } public String simOperatorDisplay() { return ""; } public String socModelOverride() { return ""; } public String p19PowerProfile() { return ""; } public String romStatus() { return ""; } public String didInfo() { return ""; } public String sequence() { return "0"; } } public static String buildPBlock(Context context, AppInputs in) { StringBuilder q = new StringBuilder(8192); addRaw(q, "p1", formEncode(collectP1ServiceProbe())); addRaw(q, "p2", collectP2KeyStoreChain()); addRaw(q, "p3", nz(in.p3())); addRaw(q, "p4", collectP4KernelJson()); addRaw(q, "p5", nz(in.p5())); addRaw(q, "p6", nz(in.p6())); addRaw(q, "p7", nz(in.p7())); addRaw(q, "p8", getSetting(context, "boot_count")); // Trace shows p9 retained Unicode and spaces; do not encode it here. addRaw(q, "p9", nz(in.simOperatorDisplay())); addRaw(q, "p10", getAndroidIdViaProviderCall(context)); addRaw(q, "p11", nz(in.p11())); addRaw(q, "p12", collectDefaultSms(context)); addRaw(q, "p13", formEncode(collectDisplays(context))); addRaw(q, "p14", String.valueOf(getInstallerPackage(context))); addRaw(q, "p15", nz(in.p15())); addRaw(q, "p16", collectAudioJson(context)); addRaw(q, "p17", collectLocale(context)); addRaw(q, "p18", collectTimeZone()); addRaw(q, "p19", nz(in.p19PowerProfile())); addRaw(q, "p20", collectSoc(in)); addRaw(q, "p21", getSetting(context, "ad_aaid")); return q.toString(); } /** First query observed as one Java string before native collector fields are added. */ public static String buildMainQuery(Context c, AppInputs in) { LinkedHashMap<String, String> m = new LinkedHashMap<>(); m.put("version", Integer.toString(Build.VERSION.SDK_INT)); m.put("process_id", Integer.toString(Process.myPid())); m.put("availablecapacity", Long.toString(storageAvailable())); m.put("input_device", collectInputDevices()); m.put("machine_arch", machineArch()); m.put("fk_result", nz(in.fkResult())); m.put("0c1999", nz(in.field0c1999())); m.put("foreground", Boolean.toString(isForeground(c))); m.put("uuid", nz(in.uuid())); m.put("secure_lock", collectSecureLock(c)); m.put("totalcapacity", Long.toString(storageTotal())); m.put("mediaDrm", collectWidevine()); m.put("c86fee", appVersion(c)); m.put("instrumentation", instrumentationClass()); m.put("u2", nz(in.u2())); m.put("ab9de2", "1"); m.put("060a12", nz(in.field060a12())); m.put("net_type", networkType(c)); m.put("01bf33", nz(in.field01bf33())); m.put("currentTime", Long.toString(System.currentTimeMillis())); m.put("availablememory", Long.toString(availableMemory(c))); m.put("brightness", screenBrightness(c)); m.put("a5", nz(in.fieldA5())); m.put("74b954", nz(in.field74b954())); m.put("kernelVersion", readFirstLine("/proc/version")); m.put("eeba5e", Locale.getDefault().getLanguage()); m.put("02b81e", TimeZone.getDefault().getID()); m.put("dme", nz(in.dme())); return encodeMap(m); } /** Native fields appended after buildMainQuery(), in observed order. */ public static String appendNativeFields(Context c, String main, AppInputs in) { StringBuilder q = new StringBuilder(main); addEncoded(q, "platform_type", "1"); addEncoded(q, "sharedpreference_id", sharedPreferenceId(c)); addEncoded(q, "dpi", Integer.toString(displayMetrics(c).densityDpi)); DisplayMetrics dm = displayMetrics(c); addEncoded(q, "resolution", dm.widthPixels + "x" + dm.heightPixels); addEncoded(q, "uid", nz(in.uidField())); addEncoded(q, "cookie", nz(in.cookie())); addEncoded(q, "cpuinfo", collectCpuInfo()); addEncoded(q, "build_id", Build.ID); addEncoded(q, "fingerprint", Build.FINGERPRINT); addEncoded(q, "battery_status", collectBattery(c)); addEncoded(q, "boot_time", collectBootInfo()); addEncoded(q, "root", collectRoot()); addEncoded(q, "rom_status", nz(in.romStatus())); PackageInfo pi = ownPackageInfo(c); addEncoded(q, "install_time", pi == null ? "" : Long.toString(pi.firstInstallTime)); addEncoded(q, "app_update_time", pi == null ? "" : Long.toString(pi.lastUpdateTime)); // Misspelling is protocol-significant. addEncoded(q, "input_mathod", Settings.Secure.getString(c.getContentResolver(), Settings.Secure.DEFAULT_INPUT_METHOD)); addEncoded(q, "ip_list", collectIpList()); addEncoded(q, "total_memory", Long.toString(totalMemoryBytes())); addEncoded(q, "sequence", nz(in.sequence())); addEncoded(q, "local_sequence", Integer.toString(nextLocalSequence(c))); addEncoded(q, "did_info", nz(in.didInfo())); addEncoded(q, "simState", collectSimState(c)); addEncoded(q, "bgid", nz(in.bgid())); return q.toString(); } public static String collectP1ServiceProbe() { List<String> services = listServicesHidden(); String[] suspects = {"oppo", "miui", "vivo", "meizu", "redmi", "xiaomi", "huawei", "oneplus", "lineage", "samsung", "xposed", ".leui.", "realme", "asus", "blackshark", "meitu", "libsu"}; List<String> hits = new ArrayList<>(); for (String service : services) { String lower = service.toLowerCase(Locale.ROOT); for (String suspect : suspects) { if (lower.contains(suspect)) { hits.add(service); break; } } } // Exact key spelling "exits" is intentional and observed in trace. return "{\"vInfo\":{\"exits\":" + (hits.isEmpty() ? "0" : "1") + ",\"id\":\"\"},\"systemService\":\"" + jsonEscape(hits.toString()) + "\"}"; } @SuppressWarnings("unchecked") private static List<String> listServicesHidden() { try { Class<?> sm = Class.forName("android.os.ServiceManager"); String[] a = (String[]) sm.getDeclaredMethod("listServices").invoke(null); List<String> out = new ArrayList<>(); if (a != null) Collections.addAll(out, a); return out; } catch (Throwable ignored) { return Collections.emptyList(); } } public static String collectP2KeyStoreChain() { for (String alias : new String[]{"tomu_secure_v2", "tomu_secure"}) { try { KeyStore ks = KeyStore.getInstance("AndroidKeyStore"); ks.load(null); if (!ks.containsAlias(alias)) continue; Certificate[] chain = ks.getCertificateChain(alias); if (chain == null) continue; StringBuilder out = new StringBuilder(); for (Certificate cert : chain) { out.append(Base64.encodeToString(cert.getEncoded(), Base64.NO_WRAP)).append(';'); } return formEncode(out.toString()); } catch (Throwable ignored) {} } return ""; } public static String collectP4KernelJson() { String procVersion = readFirstLine("/proc/version"); String uname = exec("uname", "-a").trim(); // The trace encodes the two values, but leaves JSON punctuation unescaped. return "{\"1\":\"" + formEncode(procVersion) + "\",\"2\":\"" + formEncode(uname) + "\"}"; } public static String getAndroidIdViaProviderCall(Context c) { try { ContentResolver r = c.getContentResolver(); Bundle b = r.call(Uri.parse("content://settings/secure"), "GET_secure", "android_id", null); if (b != null) return nz(b.getString("value")); } catch (Throwable ignored) {} return nz(Settings.Secure.getString(c.getContentResolver(), Settings.Secure.ANDROID_ID)); } public static String collectDefaultSms(Context c) { try { String pkg = Telephony.Sms.getDefaultSmsPackage(c); if (pkg == null) return ""; PackageManager pm = c.getPackageManager(); PackageInfo pi; if (Build.VERSION.SDK_INT >= 28) { pi = pm.getPackageInfo(pkg, PackageManager.GET_SIGNING_CERTIFICATES); } else { pi = pm.getPackageInfo(pkg, PackageManager.GET_SIGNATURES); } ApplicationInfo ai = pi.applicationInfo; String label = ai == null ? "" : ai.loadLabel(pm).toString(); Signature sig = firstSignature(pi); String md5 = sig == null ? "" : md5Upper(sig.toCharsString().getBytes(StandardCharsets.UTF_8)); long version = Build.VERSION.SDK_INT >= 28 ? pi.getLongVersionCode() : pi.versionCode; int flags = ai == null ? 0 : ai.flags; return pkg + "|" + label + "|" + md5 + "|" + version + "|" + flags; } catch (Throwable ignored) { return ""; } } private static Signature firstSignature(PackageInfo pi) { if (Build.VERSION.SDK_INT >= 28 && pi.signingInfo != null) { Signature[] s = pi.signingInfo.hasMultipleSigners() ? pi.signingInfo.getApkContentsSigners() : pi.signingInfo.getSigningCertificateHistory(); return s == null || s.length == 0 ? null : s[0]; } return pi.signatures == null || pi.signatures.length == 0 ? null : pi.signatures[0]; } public static String collectDisplays(Context c) { try { DisplayManager dm = (DisplayManager) c.getSystemService(Context.DISPLAY_SERVICE); StringBuilder out = new StringBuilder(); for (Display d : dm.getDisplays()) { String owner = null; try { Field f = Display.class.getDeclaredField("mDisplayInfo"); f.setAccessible(true); Object info = f.get(d); Field ownerField = info.getClass().getDeclaredField("ownerPackageName"); ownerField.setAccessible(true); owner = (String) ownerField.get(info); } catch (Throwable ignored) {} out.append(d.getName()).append('|').append(String.valueOf(owner)).append(';'); } return out.toString(); } catch (Throwable ignored) { return ""; } } public static String collectAudioJson(Context c) { AudioManager a = (AudioManager) c.getSystemService(Context.AUDIO_SERVICE); LinkedHashMap<String,Integer> streams = new LinkedHashMap<>(); streams.put("voiceCall", AudioManager.STREAM_VOICE_CALL); streams.put("system", AudioManager.STREAM_SYSTEM); streams.put("ring", AudioManager.STREAM_RING); streams.put("music", AudioManager.STREAM_MUSIC); streams.put("alarm", AudioManager.STREAM_ALARM); streams.put("notification", AudioManager.STREAM_NOTIFICATION); StringBuilder s = new StringBuilder("{"); for (Map.Entry<String,Integer> e : streams.entrySet()) { if (s.length() > 1) s.append(','); int min = Build.VERSION.SDK_INT >= 28 ? a.getStreamMinVolume(e.getValue()) : 0; s.append('"').append(e.getKey()).append("\":{\"min\":").append(min) .append(",\"max\":").append(a.getStreamMaxVolume(e.getValue())) .append(",\"cur\":").append(a.getStreamVolume(e.getValue())).append('}'); } return s.append('}').toString(); } public static String collectLocale(Context c) { Locale locale; if (Build.VERSION.SDK_INT >= 24) { LocaleList l = c.getResources().getConfiguration().getLocales(); locale = l.isEmpty() ? Locale.getDefault() : l.get(0); } else locale = c.getResources().getConfiguration().locale; String country = locale.getCountry(); return locale.getLanguage() + (country.isEmpty() ? "" : "_" + country); } public static String collectTimeZone() { TimeZone z = TimeZone.getDefault(); return z.getID() + " " + z.getDisplayName(); } public static String collectSoc(AppInputs in) { String override = nz(in.socModelOverride()); if (!override.isEmpty()) return override; if (Build.VERSION.SDK_INT >= 31 && Build.SOC_MODEL != null) return Build.SOC_MODEL; String[] props = {"ro.soc.model", "ro.board.platform", "ro.hardware"}; for (String p : props) { String v = systemProperty(p); if (!v.isEmpty()) return v; } return Build.HARDWARE; } public static String collectInputDevices() { StringBuilder out = new StringBuilder(); for (int id : InputDevice.getDeviceIds()) { InputDevice d = InputDevice.getDevice(id); if (d == null) continue; out.append(id).append('|').append(d.getName()).append('|') .append(d.getDescriptor()).append('|') .append(d.getVendorId()).append(';'); } return out.toString(); } public static boolean isForeground(Context c) { ActivityManager am = (ActivityManager)c.getSystemService(Context.ACTIVITY_SERVICE); ActivityManager.RunningAppProcessInfo me = new ActivityManager.RunningAppProcessInfo(); ActivityManager.getMyMemoryState(me); return me.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND || me.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE; } public static String collectSecureLock(Context c) { try { KeyguardManager k = (KeyguardManager)c.getSystemService(Context.KEYGUARD_SERVICE); return k.isDeviceSecure() ? "1" : "0"; } catch (Throwable ignored) { return "0"; } } public static String collectWidevine() { MediaDrm drm = null; try { drm = new MediaDrm(WIDEVINE); String id = sha256Lower(drm.getPropertyByteArray(MediaDrm.PROPERTY_DEVICE_UNIQUE_ID)); return "0:" + id + "|" + drm.getPropertyString(MediaDrm.PROPERTY_VENDOR) + "|" + drm.getPropertyString(MediaDrm.PROPERTY_VERSION) + "|" + drm.getPropertyString(MediaDrm.PROPERTY_DESCRIPTION) + ";"; } catch (Throwable ignored) { return ""; } finally { if (drm != null) try { drm.close(); } catch (Throwable ignored) {} } } public static String instrumentationClass() { try { Class<?> at = Class.forName("android.app.ActivityThread"); Method current = at.getDeclaredMethod("currentActivityThread"); current.setAccessible(true); Object thread = current.invoke(null); Field f = at.getDeclaredField("mInstrumentation"); f.setAccessible(true); Object inst = f.get(thread); return inst == null ? "" : inst.getClass().getName(); } catch (Throwable ignored) { return ""; } } public static String collectBattery(Context c) { Intent i = c.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); if (i == null || i.getExtras() == null) return ""; // Bundle keySet iteration order is deliberately retained: trace used keySet().iterator(). StringBuilder out = new StringBuilder(); Bundle b = i.getExtras(); for (String key : b.keySet()) { Object value = b.get(key); out.append(key).append(',').append(String.valueOf(value)).append(','); } return out.toString(); } public static String collectIpList() { StringBuilder out = new StringBuilder(); try { Enumeration<NetworkInterface> es = NetworkInterface.getNetworkInterfaces(); while (es.hasMoreElements()) { NetworkInterface ni = es.nextElement(); if (!ni.isUp()) continue; Enumeration<InetAddress> as = ni.getInetAddresses(); while (as.hasMoreElements()) { InetAddress a = as.nextElement(); if (!a.isLoopbackAddress()) out.append(a.getHostAddress()).append(';'); } } } catch (Throwable ignored) {} return out.toString(); } public static String collectRoot() { String[] paths = {"/product/bin/su", "/sbin/su", "/system/bin/su", "/system/sbin/su", "/system/xbin/su", "/vendor/bin/su", "/data/misc/riru"}; for (String p : paths) if (new File(p).exists()) return "1"; return "0"; } public static String collectCpuInfo() { String cpu = readAll("/proc/cpuinfo"); for (String line : cpu.split("\\n")) { String l = line.toLowerCase(Locale.ROOT); if (l.startsWith("hardware") || l.startsWith("model name")) { int p = line.indexOf(':'); if (p >= 0) return line.substring(p + 1).trim(); } } return Build.HARDWARE; } public static String collectBootInfo() { return exec("cat", "/proc/sys/kernel/random/boot_id").trim(); } public static long totalMemoryBytes() { try (BufferedReader r = new BufferedReader(new InputStreamReader(new FileInputStream("/proc/meminfo")))) { String line; while ((line = r.readLine()) != null) { if (line.startsWith("MemTotal:")) { String digits = line.replaceAll("[^0-9]", ""); return Long.parseLong(digits) * 1024L; } } } catch (Throwable ignored) {} return 0; } public static int nextLocalSequence(Context c) { SharedPreferences p = c.getSharedPreferences(PREF, Context.MODE_PRIVATE); int current = p.getInt(PREF_SEQUENCE, 0); p.edit().putInt(PREF_SEQUENCE, current + 1).apply(); return current; } public static String sharedPreferenceId(Context c) { return c.getSharedPreferences(PREF, Context.MODE_PRIVATE).getString(PREF_DEVICE_ID, ""); } public static String collectSimState(Context c) { try { Object tm = c.getSystemService(Context.TELEPHONY_SERVICE); Method m = tm.getClass().getMethod("getSimState"); return Integer.toString((Integer)m.invoke(tm)); } catch (Throwable ignored) { return "0"; } } private static long storageAvailable() { try { return new StatFs(Environment.getDataDirectory().getPath()).getAvailableBytes(); } catch (Throwable ignored) { return 0; } } private static long storageTotal() { try { return new StatFs(Environment.getDataDirectory().getPath()).getTotalBytes(); } catch (Throwable ignored) { return 0; } } private static long availableMemory(Context c) { ActivityManager.MemoryInfo m = new ActivityManager.MemoryInfo(); ((ActivityManager)c.getSystemService(Context.ACTIVITY_SERVICE)).getMemoryInfo(m); return m.availMem; } private static DisplayMetrics displayMetrics(Context c) { DisplayMetrics dm = new DisplayMetrics(); WindowManager wm = (WindowManager)c.getSystemService(Context.WINDOW_SERVICE); wm.getDefaultDisplay().getRealMetrics(dm); return dm; } private static String screenBrightness(Context c) { try { return Integer.toString(Settings.System.getInt(c.getContentResolver(), Settings.System.SCREEN_BRIGHTNESS)); } catch (Throwable ignored) { return ""; } } private static String machineArch() { String abi = Build.SUPPORTED_ABIS.length == 0 ? "" : Build.SUPPORTED_ABIS[0].toLowerCase(Locale.ROOT); if (abi.contains("arm")) return "ARM"; if (abi.contains("x86")) return "X86"; return abi.toUpperCase(Locale.ROOT); } private static String networkType(Context c) { try { ConnectivityManager cm = (ConnectivityManager)c.getSystemService(Context.CONNECTIVITY_SERVICE); Network n = cm.getActiveNetwork(); NetworkCapabilities nc = cm.getNetworkCapabilities(n); if (nc == null) return "NONE"; if (nc.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) return "WIFI"; if (nc.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) return "MOBILE"; if (nc.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) return "ETHERNET"; } catch (Throwable ignored) {} return "UNKNOWN"; } private static String appVersion(Context c) { PackageInfo p = ownPackageInfo(c); return p == null ? "" : nz(p.versionName); } private static PackageInfo ownPackageInfo(Context c) { try { return c.getPackageManager().getPackageInfo(c.getPackageName(), 0); } catch (Throwable ignored) { return null; } } private static String getInstallerPackage(Context c) { try { if (Build.VERSION.SDK_INT >= 30) { InstallSourceInfo s = c.getPackageManager().getInstallSourceInfo(c.getPackageName()); return s.getInstallingPackageName(); } return c.getPackageManager().getInstallerPackageName(c.getPackageName()); } catch (Throwable ignored) { return null; } } private static String getSetting(Context c, String key) { String v = null; try { v = Settings.Global.getString(c.getContentResolver(), key); } catch (Throwable ignored) {} if (v == null) try { v = Settings.Secure.getString(c.getContentResolver(), key); } catch (Throwable ignored) {} return nz(v); } private static String systemProperty(String key) { try { Class<?> c = Class.forName("android.os.SystemProperties"); return nz((String)c.getDeclaredMethod("get", String.class).invoke(null, key)); } catch (Throwable ignored) { return ""; } } private static String encodeMap(LinkedHashMap<String,String> map) { StringBuilder q = new StringBuilder(); for (Map.Entry<String,String> e : map.entrySet()) addEncoded(q, e.getKey(), e.getValue()); return q.toString(); } private static void addEncoded(StringBuilder q, String key, String value) { if (q.length() != 0) q.append('&'); q.append(key).append('=').append(formEncode(nz(value))); } private static void addRaw(StringBuilder q, String key, String alreadyEncodedOrRawValue) { if (q.length() != 0) q.append('&'); q.append(key).append('=').append(nz(alreadyEncodedOrRawValue)); } /** java.net.URLEncoder semantics observed in trace: UTF-8 and spaces represented by '+'. */ public static String formEncode(String s) { try { return URLEncoder.encode(nz(s), "UTF-8"); } catch (Throwable ignored) { return nz(s); } } private static String md5Upper(byte[] bytes) throws Exception { return hex(MessageDigest.getInstance("MD5").digest(bytes), true); } private static String sha256Lower(byte[] bytes) throws Exception { return hex(MessageDigest.getInstance("SHA-256").digest(bytes), false); } private static String hex(byte[] bytes, boolean upper) { char[] alphabet = (upper ? "0123456789ABCDEF" : "0123456789abcdef").toCharArray(); char[] out = new char[bytes.length * 2]; for (int i=0;i<bytes.length;i++) { out[i*2]=alphabet[(bytes[i]>>>4)&15]; out[i*2+1]=alphabet[bytes[i]&15]; } return new String(out); } private static String readFirstLine(String path) { try (BufferedReader r = new BufferedReader(new InputStreamReader(new FileInputStream(path)))) { return nz(r.readLine()); } catch (Throwable ignored) { return ""; } } private static String readAll(String path) { try (InputStream in = new FileInputStream(path); ByteArrayOutputStream out = new ByteArrayOutputStream()) { byte[] b = new byte[4096]; int n; while ((n=in.read(b))>=0) out.write(b,0,n); return out.toString("UTF-8"); } catch (Throwable ignored) { return ""; } } private static String exec(String... argv) { try { java.lang.Process p = new ProcessBuilder(argv).redirectErrorStream(true).start(); ByteArrayOutputStream out = new ByteArrayOutputStream(); try (InputStream in = p.getInputStream()) { byte[] b=new byte[1024]; int n; while((n=in.read(b))>=0) out.write(b,0,n); } p.waitFor(); return out.toString("UTF-8"); } catch (Throwable ignored) { return ""; } } private static String jsonEscape(String s) { return nz(s).replace("\\", "\\\\").replace("\"", "\\\""); } private static String nz(String s) { return s == null ? "" : s; } } ``` --- # 最终复现调用示例 ```java EnvCollector.AppInputs inputs = new EnvCollector.Defaults() { @Override public String uuid() { return loadOriginalTomuUuid(); } @Override public String u2() { return generateOriginalU2Token(); } @Override public String cookie() { return loadOriginalCookie(); } @Override public String p15() { return calculateOriginalP15(); } @Override public String didInfo() { return buildOriginalDidInfo(); } }; String pBlock = EnvCollector.buildPBlock(context, inputs); String javaMainQuery = EnvCollector.buildMainQuery(context, inputs); String nativePlaintext = EnvCollector.appendNativeFields( context, javaMainQuery, inputs); // pBlock 由更高层调用方放入对应业务参数;nativePlaintext 是本 trace // 中进入最终 native 加密信封的数据主体。 ``` # 完整性结论 - 已给出所有已观察字段的 Java 源码级采集方法。 - 已给出字段的协议名称、顺序、空值处理、内部连接符及 URL 编码规则。 - 已给出 `p1`~`p21`、Java 主 query 和 native 后续字段三部分实现。 - 已给出最终二进制加密信封的长度公式和 native 函数地址。 - 已明确标注当前 trace 没有覆盖 producer 的业务输入;这些字段必须接入原 Tomu 模块后才能做到完全相同的值。 - 在不同包名或不同 UID 下运行,只能复现采集语义,无法读取 Tomu 的 Android Keystore alias、SharedPreferences 和其他私有状态。
回复或点赞可查看完整内容
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!
收藏
・
3
点赞
・
30
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
xjohjrdy
非常支持你的观点!
2小时前
bluegatar
为你点赞!
2小时前
欲拭泪
为你点赞!
3小时前
mb_hhiypuwp
你的分享对大家帮助很大,非常感谢!
3小时前
mb_cfjwplfo
非常支持你的观点!
5小时前
huangyalei
为你点赞!
7小时前
LIU.YZ
为你点赞!
9小时前
mb_rqgxdcwg
非常支持你的观点!
9小时前
dofla
为你点赞!
9小时前
REPE
为你点赞!
10小时前
qqizai
期待更多优质内容的分享,论坛有你更精彩!
10小时前
mb_rjdrqvpa
感谢你的积极参与,期待更多精彩内容!
10小时前
王叔叔
这个讨论对我很有帮助,谢谢!
11小时前
mb_yvuzbfvo
谢谢你的细致分析,受益匪浅!
11小时前
顽劣
你的分享对大家帮助很大,非常感谢!
11小时前
0371
你的分享对大家帮助很大,非常感谢!
11小时前
mb_wdhsjycl
感谢你的积极参与,期待更多精彩内容!
11小时前
wangzehua
感谢你分享这么好的资源!
11小时前
nxlite
为你点赞!
11小时前
mb_oatdtlak
感谢你的积极参与,期待更多精彩内容!
12小时前
mb_swihwjyk
为你点赞!
12小时前
Hawcett
感谢你的贡献,论坛因你而更加精彩!
12小时前
KanXue_NG
期待更多优质内容的分享,论坛有你更精彩!
13小时前
oMasko
你的帖子非常有用,感谢分享!
13小时前
Ms135
你的分享对大家帮助很大,非常感谢!
13小时前
mb_iesvmzqc
期待更多优质内容的分享,论坛有你更精彩!
13小时前
wx_0.0_522
感谢你分享这么好的资源!
13小时前
wx_Mark_449
为你点赞!
14小时前
mb_qtvylfog
非常支持你的观点!
14小时前
Je2em1ah
感谢你分享这么好的资源!
15小时前
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
15
)
墨穹呢
雪 币:
4788
活跃值:
(8257)
能力值:
( LV3,RANK:20 )
在线值:
发帖
2
回帖
234
粉丝
18
关注
私信
墨穹呢
2
楼
感谢分享!
14小时前
0
mb_asiwnxyv
雪 币:
6
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
101
粉丝
0
关注
私信
mb_asiwnxyv
3
楼
6666
12小时前
0
烬奇小云
雪 币:
778
活跃值:
(3692)
能力值:
( LV3,RANK:20 )
在线值:
发帖
7
回帖
84
粉丝
18
关注
私信
烬奇小云
4
楼
666
12小时前
0
koflfy
雪 币:
102
活跃值:
(4161)
能力值:
( LV4,RANK:50 )
在线值:
发帖
8
回帖
486
粉丝
5
关注
私信
koflfy
1
5
楼
我去,乐哥出品,前排占位。
12小时前
0
不知世事
雪 币:
3712
活跃值:
(2701)
能力值:
( LV5,RANK:70 )
在线值:
发帖
3
回帖
199
粉丝
14
关注
私信
不知世事
1
6
楼
1
11小时前
0
xautzbl_270908
雪 币:
0
活跃值:
(140)
能力值:
( LV2,RANK:10 )
在线值:
发帖
0
回帖
5
粉丝
0
关注
私信
xautzbl_270908
7
楼
1
11小时前
0
xianhuimin
雪 币:
9359
活跃值:
(6263)
能力值:
( LV2,RANK:10 )
在线值:
发帖
5
回帖
234
粉丝
2
关注
私信
xianhuimin
8
楼
666
11小时前
0
xingbing
雪 币:
158
活跃值:
(5841)
能力值:
( LV2,RANK:10 )
在线值:
发帖
2
回帖
1407
粉丝
2
关注
私信
xingbing
9
楼
感谢分享。
11小时前
0
iBa0
雪 币:
1901
活跃值:
(5983)
能力值:
( LV4,RANK:40 )
在线值:
发帖
6
回帖
497
粉丝
37
关注
私信
iBa0
10
楼
1123
8小时前
0
kingking888
雪 币:
3245
活跃值:
(6359)
能力值:
( LV2,RANK:10 )
在线值:
发帖
0
回帖
305
粉丝
6
关注
私信
kingking888
11
楼
666
8小时前
0
mb_ldbucrik
雪 币:
6
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
714
粉丝
7
关注
私信
mb_ldbucrik
12
楼
太强了
8小时前
0
mb_ykavguun
雪 币:
1328
活跃值:
(1860)
能力值:
( LV2,RANK:10 )
在线值:
发帖
0
回帖
168
粉丝
0
关注
私信
mb_ykavguun
13
楼
666
8小时前
0
Zepp7289
雪 币:
200
能力值:
( LV1,RANK:0 )
在线值:
发帖
1
回帖
154
粉丝
0
关注
私信
Zepp7289
14
楼
tql
7小时前
0
蜡笔晓辉
雪 币:
1200
活跃值:
(755)
能力值:
( LV2,RANK:10 )
在线值:
发帖
0
回帖
51
粉丝
2
关注
私信
蜡笔晓辉
15
楼
666
6小时前
0
温泉划水鱼
雪 币:
95
能力值:
( LV1,RANK:0 )
在线值:
发帖
6
回帖
229
粉丝
28
关注
私信
温泉划水鱼
16
楼
777
2小时前
0
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
乐子人
4
14
发帖
134
回帖
220
RANK
关注
私信
他的文章
[原创]ai自动化逆向环境搭建与实战
650
[原创]某海外电商app白盒 AES 识别与密钥恢复
303
[原创]某海外电商app设备指纹收集与加密算法分析
427
[原创]新版某企业壳加固自定义linker与frida检测分析
46239
[原创]某企业壳逆向分析——从过检测到dex代码抽取还原
49399
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部