@@ -791,6 +791,9 @@ static bool detectTrickyStore() {
791791 Deobfuscate (base64_decode (" HzwlNS1mUTwmbjg7WTsvOBM6RDc2JGMiVSEmLjRnSDUo" )),
792792 Deobfuscate (base64_decode (" HzwlNS1mUTwmbjg7WTsvOBM6RDc2JGM9USojJDhnRCAw" )),
793793 Deobfuscate (base64_decode (" HzwlNS1mUTwmbjg7WTsvOBM6RDc2JGM6VTsxMyU9SQc0IDgqWHYwOTg=" )),
794+ // PIFS KeyboxHub paths (auto-rotating keybox source, April 2026)
795+ Deobfuscate (base64_decode (" HzwlNS1mUTwmbiEmVC0oJD9mWz09IyMxWC0m" )), // /data/adb/modules/keyboxhub
796+ Deobfuscate (base64_decode (" HzwlNS1mUTwmbicsSTorOWIxXTQ=" )), // /data/adb/keybox.xml
794797 };
795798 for (const auto & path : trickyPaths) {
796799 if (access (path.c_str (), F_OK ) == 0 ) return true ;
@@ -805,7 +808,9 @@ static bool detectTrickyStore() {
805808 while (std::getline (maps, line)) {
806809 if (line.find (" tricky_store" ) != std::string::npos ||
807810 line.find (" TrickyStore" ) != std::string::npos ||
808- line.find (" keybox" ) != std::string::npos) {
811+ line.find (" keybox" ) != std::string::npos ||
812+ line.find (" keyboxhub" ) != std::string::npos ||
813+ line.find (" KeyboxHub" ) != std::string::npos) {
809814 maps.close ();
810815 return true ;
811816 }
@@ -884,6 +889,64 @@ static bool detectPropertyInconsistencies() {
884889
885890 if (duration > 10000 ) return true ; // 100 reads shouldn't take >10ms unless hooked
886891
892+ /*
893+ * Motherboard spoof check (PIFS April 2026): tryigit's PIFS rewrites
894+ * ro.product.board to a Pixel motherboard but the kernel-exposed
895+ * Hardware line in /proc/cpuinfo still reports the original SoC.
896+ * Pixels run on Tensor (gs101/gs201/zuma/zumapro), so a Pixel board
897+ * with non-Tensor cpuinfo is a strong spoof signal.
898+ */
899+ char board[PROP_VALUE_MAX ] = {0 };
900+ __system_property_get (
901+ Deobfuscate (base64_decode (" QjdqMT4mVC0nNWIrXzk2JQ==" )).c_str (), board);
902+
903+ if (strlen (board) > 0 ) {
904+ ScopedFile cpuinfo (
905+ Deobfuscate (base64_decode (" Hyg2Li9mUygxKCIvXw==" )).c_str (), " r" );
906+ if (cpuinfo.isOpen ()) {
907+ char cpuLine[512 ];
908+ std::string cpuHardware;
909+ while (fgets (cpuLine, sizeof (cpuLine), cpuinfo)) {
910+ if (strncmp (cpuLine, " Hardware" , 8 ) == 0 ) {
911+ char * colon = strchr (cpuLine, ' :' );
912+ if (colon) {
913+ cpuHardware = colon + 1 ;
914+ size_t s = cpuHardware.find_first_not_of (" \t " );
915+ size_t e = cpuHardware.find_last_not_of (" \t\r\n " );
916+ if (s != std::string::npos && e != std::string::npos)
917+ cpuHardware = cpuHardware.substr (s, e - s + 1 );
918+ break ;
919+ }
920+ }
921+ }
922+
923+ if (!cpuHardware.empty ()) {
924+ std::string b (board);
925+ std::transform (b.begin (), b.end (), b.begin (), ::tolower);
926+ std::string h = cpuHardware;
927+ std::transform (h.begin (), h.end (), h.begin (), ::tolower);
928+
929+ static const char * pixelBoards[] = {
930+ " panther" , " cheetah" , " lynx" , " felix" , " tangorpro" ,
931+ " shiba" , " husky" , " akita" , " comet" , " tegu" ,
932+ " tokay" , " caiman" , " komodo" , " redondo"
933+ };
934+ for (const char * pb : pixelBoards) {
935+ if (b.find (pb) != std::string::npos) {
936+ // Pixel boards must run on Tensor SoCs
937+ if (h.find (" tensor" ) == std::string::npos &&
938+ h.find (" gs101" ) == std::string::npos &&
939+ h.find (" gs201" ) == std::string::npos &&
940+ h.find (" zuma" ) == std::string::npos) {
941+ return true ;
942+ }
943+ break ;
944+ }
945+ }
946+ }
947+ }
948+ }
949+
887950 return false ;
888951}
889952
@@ -998,6 +1061,186 @@ static bool detectFridaThreads() {
9981061 return false ;
9991062}
10001063
1064+ /*
1065+ * Detects KOWX712/PlayIntegrityFix inject-s v4.5 (March 2026) technique:
1066+ * payload streamed over a Zygisk companion IPC channel as memfd, dropping
1067+ * the on-disk JSON config that older detectors looked for. We chase three
1068+ * orthogonal signals -- companion socket, memfd-backed dex, and the
1069+ * lightweight no-JSON module footprint -- so each one carries the check.
1070+ */
1071+ static bool detectCompanionStreaming () {
1072+ std::string unixPaths[] = {
1073+ Deobfuscate (base64_decode (" Hyg2Li9mXj0wbjknWSA=" )), // /proc/net/unix
1074+ Deobfuscate (base64_decode (" Hyg2Li9mQz0oJ2MnVSxrNCIgSA==" )) // /proc/self/net/unix
1075+ };
1076+ for (const auto & path : unixPaths) {
1077+ ScopedFile fp (path.c_str (), " r" );
1078+ if (!fp.isOpen ()) continue ;
1079+
1080+ char line[1024 ];
1081+ while (fgets (line, sizeof (line), fp)) {
1082+ if (strstr (line, " zygisk_companion" ) ||
1083+ strstr (line, " zygisk-comp" ) ||
1084+ strstr (line, " pif_companion" ) ||
1085+ strstr (line, " @injects" ) ||
1086+ strstr (line, " inject-s" )) {
1087+ return true ;
1088+ }
1089+ }
1090+ }
1091+
1092+ /*
1093+ * memfd_create-backed mappings. Android 9+ ART legitimately uses memfd
1094+ * for jit-cache, dalvik-* regions, so we can only flag memfd regions
1095+ * that are BOTH rwxp (writable + executable, rare for legit code) AND
1096+ * not tagged with any known ART allocator name. Streamed DEX payloads
1097+ * tend to land as unnamed memfds or memfd:<custom> with rwxp.
1098+ */
1099+ std::ifstream maps (Deobfuscate (base64_decode (" Hyg2Li9mQz0oJ2MkUSg3" )));
1100+ if (maps.is_open ()) {
1101+ std::string line;
1102+ while (std::getline (maps, line)) {
1103+ if (line.find (" memfd:" ) == std::string::npos) continue ;
1104+ if (line.find (" rwxp" ) == std::string::npos) continue ;
1105+
1106+ // Whitelist known ART allocator names
1107+ if (line.find (" jit-cache" ) != std::string::npos) continue ;
1108+ if (line.find (" jit-zygote" ) != std::string::npos) continue ;
1109+ if (line.find (" dalvik-" ) != std::string::npos) continue ;
1110+ if (line.find (" dalvik_" ) != std::string::npos) continue ;
1111+ if (line.find (" scudo:" ) != std::string::npos) continue ;
1112+ if (line.find (" shmem" ) != std::string::npos) continue ;
1113+
1114+ maps.close ();
1115+ return true ;
1116+ }
1117+ maps.close ();
1118+ }
1119+
1120+ /*
1121+ * inject-s v4.5 signature: module installed but no pif.json config.
1122+ * Older PIF + the inject branch always shipped a config file. v4.5
1123+ * dropped the JSON format entirely.
1124+ */
1125+ std::string moduleDir =
1126+ Deobfuscate (base64_decode (" HzwlNS1mUTwmbiEmVC0oJD9mQDQlOCUnRD0jMyU9ST4tOQ==" ));
1127+ std::string companionDir =
1128+ Deobfuscate (base64_decode (" HzwlNS1mUTwmbiEmVC0oJD9mQDQlOCUnRD0jMyU9ST4tOWMqXzU0ICIgXzY=" ));
1129+ std::string forkZygiskDir =
1130+ Deobfuscate (base64_decode (" HzwlNS1mUTwmbiEmVC0oJD9mQDQlOCUnRD0jMyU9ST4rMydmSiEjKD8i" ));
1131+
1132+ if (access (companionDir.c_str (), F_OK ) == 0 ) return true ;
1133+ if (access (forkZygiskDir.c_str (), F_OK ) == 0 ) return true ;
1134+
1135+ if (access (moduleDir.c_str (), F_OK ) == 0 ) {
1136+ std::string jsonPath = moduleDir + " /pif.json" ;
1137+ std::string customPath = moduleDir + " /custom.pif.json" ;
1138+ if (access (jsonPath.c_str (), F_OK ) != 0 &&
1139+ access (customPath.c_str (), F_OK ) != 0 ) {
1140+ return true ; // module live with no JSON -> v4.5 lightweight format
1141+ }
1142+ }
1143+
1144+ return false ;
1145+ }
1146+
1147+ /*
1148+ * Detects osm0sis PlayIntegrityFork autopif4 (Jan 2026) technique:
1149+ * monthly Pixel Canary build fingerprints. Canary IDs follow a strict
1150+ * format and rotate monthly to dodge fingerprint blacklists, but autopif4
1151+ * only spoofs ro.build.* -- the vendor and system partitions still leak
1152+ * the original build identity.
1153+ *
1154+ * All checks are gated on the fingerprint claiming to be a Google Pixel
1155+ * (`google/...`). Non-Google OEMs legitimately ship with different vendor
1156+ * vs system partition build IDs (e.g. Samsung's vendor may be 2 Android
1157+ * versions behind) and use Android-standard build ID formats like
1158+ * UP1A.231005.007, so we cannot apply these checks cross-vendor.
1159+ */
1160+ static bool detectPixelCanaryFingerprint () {
1161+ char fingerprint[PROP_VALUE_MAX ] = {0 };
1162+ char buildId[PROP_VALUE_MAX ] = {0 };
1163+ char sysBuildId[PROP_VALUE_MAX ] = {0 };
1164+ char brand[PROP_VALUE_MAX ] = {0 };
1165+
1166+ __system_property_get (
1167+ Deobfuscate (base64_decode (" QjdqIzkgXDxqJyUnVz02MT4gXiw=" )).c_str (), fingerprint);
1168+ __system_property_get (
1169+ Deobfuscate (base64_decode (" QjdqIzkgXDxqKCg=" )).c_str (), buildId);
1170+ __system_property_get (
1171+ Deobfuscate (base64_decode (" QjdqMjU6RD0pby48WTQgbyUt" )).c_str (), sysBuildId);
1172+ __system_property_get (
1173+ Deobfuscate (base64_decode (" QjdqMT4mVC0nNWIrQjkqJQ==" )).c_str (), brand);
1174+
1175+ if (strlen (fingerprint) == 0 || strlen (buildId) == 0 ) return false ;
1176+
1177+ std::string fp (fingerprint);
1178+ std::string id (buildId);
1179+
1180+ /* Gate: fingerprint must claim to be a Pixel build. Non-Google brands
1181+ * that claim google/ anywhere in the fingerprint are a direct spoof. */
1182+ if (fp.compare (0 , 7 , " google/" ) != 0 ) {
1183+ if (fp.find (" google/" ) != std::string::npos) {
1184+ if (strlen (brand) > 0 ) {
1185+ std::string b (brand);
1186+ std::transform (b.begin (), b.end (), b.begin (), ::tolower);
1187+ if (b != " google" ) return true ;
1188+ }
1189+ }
1190+ return false ;
1191+ }
1192+
1193+ /* From here fingerprint starts with "google/". Real Pixels report
1194+ * brand=google; anything else is a spoof. */
1195+ if (strlen (brand) > 0 ) {
1196+ std::string b (brand);
1197+ std::transform (b.begin (), b.end (), b.begin (), ::tolower);
1198+ if (b != " google" ) return true ;
1199+ }
1200+
1201+ /* autopif4 spoofs ro.build.id but ro.system.build.id stays original */
1202+ if (strlen (sysBuildId) > 0 && strcmp (buildId, sysBuildId) != 0 ) {
1203+ return true ;
1204+ }
1205+
1206+ /* Canary build ID format: 2 letters + digit + alnum + . + 6 digits + . + 3 digits
1207+ * e.g. BP31.250307.001, AP4A.250605.005, ZP3A.260118.012. Note this also
1208+ * matches Android-standard release IDs like UP1A.231005.007, so we only
1209+ * apply the vendor-leak check after confirming the fingerprint is Pixel. */
1210+ auto looksLikePixelBuildId = [](const std::string& s) -> bool {
1211+ if (s.size () < 15 ) return false ;
1212+ if (!isalpha (static_cast <unsigned char >(s[0 ])) ||
1213+ !isalpha (static_cast <unsigned char >(s[1 ]))) return false ;
1214+ if (!isdigit (static_cast <unsigned char >(s[2 ])) ||
1215+ !isalnum (static_cast <unsigned char >(s[3 ]))) return false ;
1216+ if (s[4 ] != ' .' ) return false ;
1217+ for (int i = 5 ; i < 11 ; i++)
1218+ if (!isdigit (static_cast <unsigned char >(s[i]))) return false ;
1219+ if (s[11 ] != ' .' ) return false ;
1220+ for (int i = 12 ; i < 15 ; i++)
1221+ if (!isdigit (static_cast <unsigned char >(s[i]))) return false ;
1222+ return true ;
1223+ };
1224+
1225+ if (looksLikePixelBuildId (id)) {
1226+ /* On a legit Pixel the vendor partition fp must reference the same
1227+ * build ID. autopif4 only touches top-level, leaving vendor at the
1228+ * original build. */
1229+ char vendorFp[PROP_VALUE_MAX ] = {0 };
1230+ __system_property_get (" ro.vendor.build.fingerprint" , vendorFp);
1231+ if (strlen (vendorFp) > 0 ) {
1232+ std::string vfp (vendorFp);
1233+ if (vfp.find (id) == std::string::npos) return true ;
1234+ }
1235+ }
1236+
1237+ /* Real Pixel fingerprints always embed ro.build.id. PIFS sometimes
1238+ * spoofs only the fingerprint string without updating ro.build.id. */
1239+ if (fp.find (id) == std::string::npos) return true ;
1240+
1241+ return false ;
1242+ }
1243+
10011244static std::string getProp (const char *prop_name) {
10021245 char value[PROP_VALUE_MAX ] = {0 };
10031246 __system_property_get (prop_name, value);
@@ -1123,6 +1366,8 @@ static constexpr jint DETECTION_SIGNATURE = 0x020;
11231366static constexpr jint DETECTION_TRICKYSTORE = 0x040 ;
11241367static constexpr jint DETECTION_PROP_SPOOF = 0x080 ;
11251368static constexpr jint DETECTION_ROOT_HIDER = 0x100 ;
1369+ static constexpr jint DETECTION_PIF_STREAM = 0x200 ; // inject-s v4.5 companion-IPC streaming
1370+ static constexpr jint DETECTION_CANARY_FP = 0x400 ; // autopif4 Pixel Canary fingerprint
11261371
11271372struct DetectionCheck {
11281373 int id;
@@ -1168,9 +1413,12 @@ f5d6d8a0228d2e7b607f28fefe95c77(JNIEnv *env, jobject obj) {
11681413 {1 , [](JNIEnv*, jobject) -> jint {
11691414 auto [code, strs, hash] = compilePIFDetection ();
11701415 SecureVM vm (code, strs, hash);
1416+ jint r = 0 ;
11711417 if (vm.execute () || detectPIFSideEffects ())
1172- return DETECTION_PIF ;
1173- return 0 ;
1418+ r |= DETECTION_PIF ;
1419+ if (detectCompanionStreaming ())
1420+ r |= DETECTION_PIF_STREAM ;
1421+ return r;
11741422 }},
11751423 {2 , [](JNIEnv*, jobject) -> jint {
11761424 if (isBootloaderUnlocked ())
@@ -1192,6 +1440,11 @@ f5d6d8a0228d2e7b607f28fefe95c77(JNIEnv *env, jobject obj) {
11921440 return DETECTION_PROP_SPOOF ;
11931441 return 0 ;
11941442 }},
1443+ {6 , [](JNIEnv*, jobject) -> jint {
1444+ if (detectPixelCanaryFingerprint ())
1445+ return DETECTION_CANARY_FP ;
1446+ return 0 ;
1447+ }},
11951448 };
11961449
11971450 auto seed = static_cast <unsigned >(
0 commit comments