最近好像比较流行用ai水文章,我也用ai水几篇。这份是用ai自动分析了某电商的设备指纹函数trace。将3GB的trace文件交给ai自动分析后的结果。
我只使用QTrace对关键的指纹收集函数做trace,然后将trace文件直接交给ai自动分析,其余的不做干预。总共分析了不到20分钟

报告包含完整的指纹采集点,源码级实现逻辑,和数据加密方式。
本报告中的结论分为三类:
所谓“源码级等价实现”,指在相同 Android 版本、相同应用 UID、相同私有数据、相同运行时 block list 和相同实时设备状态下,按 trace 观察到的 API、顺序、格式及编码方式重现采集。时间、随机数、内存、电池、网络、sequence 和密文按设计会随会话变化。
本次实际结果:Base64 长 14,500,解码后 10,873 字节。
加密算法链已经可以确定为:随机临时密钥材料 + 32 字节共享秘密派生 + SHA-256/HMAC/HKDF 类 KDF + ChaCha20-Poly1305 风格 AEAD 信封。 最后一层的结构与长度关系完全吻合 12-byte nonce + ciphertext + 16-byte tag;加密数据不是 AES 分组密文(总长不是 16 的倍数)。
注意:该包高度控制流混淆,静态伪代码中的函数名均为本文按行为命名。算法流程和长度关系为高置信度;固定服务端公钥/协议常量仍应从当前版本运行时内存提取,而不能凭空写死。
实际顺序严格为:
每个值先按 application/x-www-form-urlencoded 规则转义(空格变 +,保留字符按 UTF-8 %HH 编码),再以 & 连接。
第一层整体又作为一个字段嵌入第二层。trace 中完整观察到以下字段与顺序:
随后继续追加:
此外还观察到:
这里最重要的是:顺序固定、空字段保留、复杂字段先内部拼接后整体 URL 编码。改变顺序或省略空字段会改变最终密文和服务端解释。
trace 显示明文加密前长度为 0x2A1B = 10779 字节;固定/AAD 头长度为 0x52 = 82 字节。最终长度:
与实际 Base64 解码后的 10,873 字节完全一致。因此布局为:
sub_2D9794(0x2D9794)直接给出组包公式:
sub_2EBF5C(0x2EBF5C)逐字节构造:
对应伪代码:
sub_2EBF5C 的数据流:
sub_150144 / sub_14EAC0 是标准 Base64 编码路径;trace 最终明确按 "encryptedData":" + Base64 + "} 组装。
已由 trace 直接观察并确认:字段名称、字段顺序、大部分字段值、JNI/系统 API、URL 编码后的两层明文、明文长度、82/12/16 字节结构、最终 Base64 和 JSON 包装。
由反汇编与长度关系高置信度识别:X25519/ECDH 类 32-byte 密钥交换、HKDF-SHA256 类派生、ChaCha20-Poly1305 风格 AEAD。
当前证据中不能负责任地写死:两个协议字节、服务端长期公钥、header 两个 32-byte 槽位的官方字段名,以及少数混淆字段(如 p15)的官方业务命名。它们随版本/会话变化,正确实现应从调用参数和运行时产生,而不是复制本次样本值。
每个 pN 在 native 执行前还通过 APK 方法 ri.bg.cb("pN") 查询运行时 block list。调用者如需精确复现字段屏蔽,应在 buildPBlock 前应用同一 block list。
“能编译”不等于“准确”。对 trace 没有执行到 producer 的字段,写一个看似合理的 Android API 会制造错误结论。因此源码把这些字段显式定义成 AppInputs:调用方必须接入原始 Tomu 模块或继续针对相应 producer 补 trace。这样已观察到的采集逻辑可以完全重现,而未知项不会被伪造。
The source was compiled successfully against Android API 37's android.jar.
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.
A different application cannot completely reproduce Tomu's values merely by calling public Android APIs:
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.
以下代码不是摘要,而是报告对应的完整可编译采集实现。已使用 Android API 37 的 android.jar 编译验证。
{"encryptedData":"<standard-base64>"}
p1=...&p2=...&p3=...&p4=...&p5=...&p6=...&p7=...&p8=...&p9=...
&p10=...&p11=...&p12=...&p13=...&p14=...&p15=...&p16=...
&p17=...&p18=...&p19=...&p20=...&p21=...
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
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);
}
82 + 12 + 10779 + 16 = 10873
offset size 含义
0 82 protocol header / AAD
82 12 AEAD nonce
94 plaintext_length ciphertext(与明文等长)
94+N 16 authentication tag
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);
0x00 2 协议版本/算法标记(两个固定字节)
0x02 32 临时公钥/公钥材料 A
0x22 32 随机 salt/公钥材料 B
0x42 8 64 位时间/随机数,按固定端序写入
0x4A 8 调用方传入的 64 位请求标识
总计 0x52 (82)
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);
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);
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) + "\"}";
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);
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; }
}
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 加密信封的数据主体。
注意:该包高度控制流混淆,静态伪代码中的函数名均为本文按行为命名。算法流程和长度关系为高置信度;固定服务端公钥/协议常量仍应从当前版本运行时内存提取,而不能凭空写死。
| 参数 |
本次值/含义 |
具体采集方法 |
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 |
`包名 |
标签 |
p13 |
`Built-in Screen |
null;` |
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,本次为空。 |
| 字段 |
实际内容/采集方式 |
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 |
设备/业务标识,本次为空。 |
| 地址 |
本文命名 |
作用 |
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。 |
| 字段 |
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")。 |
| 字段 |
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() |