Major Update!

This features a new bypass, new theme, etc
This commit is contained in:
ΞTΞRNAL
2025-12-23 17:09:18 +05:30
parent e85a54d0b1
commit 544d618f87
685 changed files with 63893 additions and 81215 deletions

View File

@@ -1,2 +1,5 @@
[build]
target = "aarch64-linux-android"
[target.aarch64-linux-android]
linker = "aarch64-linux-android28-clang"
[target.armv7-linux-androideabi]
linker = "armv7a-linux-androideabi28-clang"

608
native/rust/Cargo.lock generated
View File

@@ -2,21 +2,23 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "addr2line"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
dependencies = [
"gimli",
]
[[package]]
name = "adler2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "aes"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
]
[[package]]
name = "aho-corasick"
version = "1.1.3"
@@ -26,23 +28,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "android-ndk"
version = "0.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b3b3747db640f4777e971b8b8ef1562798c79e670c67ee6a242eda03ce492ef"
dependencies = [
"android-ndk-sys",
"jni 0.13.1",
"num_enum",
]
[[package]]
name = "android-ndk-sys"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ec6ef28479835db378e2202b3911608e2269405e924fabb02ac3c8fb50284fe"
[[package]]
name = "android-tzdata"
version = "0.1.1"
@@ -75,12 +60,6 @@ dependencies = [
"libc",
]
[[package]]
name = "ascii"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e"
[[package]]
name = "autocfg"
version = "1.4.0"
@@ -88,19 +67,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "backtrace"
version = "0.3.75"
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002"
dependencies = [
"addr2line",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
"windows-targets 0.52.6",
]
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64ct"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
[[package]]
name = "bitflags"
@@ -108,6 +84,15 @@ version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be"
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "bumpalo"
version = "3.16.0"
@@ -126,6 +111,26 @@ version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
[[package]]
name = "bzip2"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8"
dependencies = [
"bzip2-sys",
"libc",
]
[[package]]
name = "bzip2-sys"
version = "0.1.13+1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14"
dependencies = [
"cc",
"pkg-config",
]
[[package]]
name = "cc"
version = "1.2.9"
@@ -168,16 +173,13 @@ dependencies = [
]
[[package]]
name = "combine"
version = "3.8.1"
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da3da6baa321ec19e1cc41d31bf599f00c783d0517095cdaf0332e3fe8d20680"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"ascii",
"byteorder",
"either",
"memchr",
"unreachable",
"crypto-common",
"inout",
]
[[package]]
@@ -190,12 +192,33 @@ dependencies = [
"memchr",
]
[[package]]
name = "const-oid"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "constant_time_eq"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "crc32fast"
version = "1.4.2"
@@ -205,6 +228,79 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crypto-common"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "curve25519-dalek"
version = "4.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
dependencies = [
"cfg-if",
"cpufeatures",
"curve25519-dalek-derive",
"digest",
"fiat-crypto",
"rustc_version",
"subtle",
"zeroize",
]
[[package]]
name = "curve25519-dalek-derive"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.96",
]
[[package]]
name = "der"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
dependencies = [
"const-oid",
"zeroize",
]
[[package]]
name = "deranged"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587"
dependencies = [
"powerfmt",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
"subtle",
]
[[package]]
name = "dobby-rs"
version = "0.1.0"
@@ -222,10 +318,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cdffdeaa52be950db80677f22f549050675f226b77e97fdbe10bb2dd846ac7b"
[[package]]
name = "either"
version = "1.15.0"
name = "ed25519"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
dependencies = [
"pkcs8",
"signature",
]
[[package]]
name = "ed25519-dalek"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
dependencies = [
"curve25519-dalek",
"ed25519",
"serde",
"sha2",
"subtle",
"zeroize",
]
[[package]]
name = "env_filter"
@@ -248,14 +362,10 @@ dependencies = [
]
[[package]]
name = "error-chain"
version = "0.12.4"
name = "fiat-crypto"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc"
dependencies = [
"backtrace",
"version_check",
]
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
[[package]]
name = "flate2"
@@ -267,6 +377,16 @@ dependencies = [
"miniz_oxide",
]
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.2.15"
@@ -279,10 +399,17 @@ dependencies = [
]
[[package]]
name = "gimli"
version = "0.31.1"
name = "goldberg"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
checksum = "d2d9cbd1fdba1a34e35404e93aab99efbf14076ab4c3380b4d408f7cd3cd9710"
dependencies = [
"moisture",
"proc-macro2",
"quote",
"rand",
"syn 1.0.109",
]
[[package]]
name = "hex"
@@ -290,6 +417,15 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest",
]
[[package]]
name = "iana-time-zone"
version = "0.1.61"
@@ -313,26 +449,21 @@ dependencies = [
"cc",
]
[[package]]
name = "inout"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
dependencies = [
"generic-array",
]
[[package]]
name = "itoa"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
[[package]]
name = "jni"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e00f1fd30a82a801f8bf38bcb0895088a0013cde111acb713c0824edc372aa4"
dependencies = [
"cesu8",
"combine 3.8.1",
"error-chain",
"jni-sys",
"log",
"walkdir",
]
[[package]]
name = "jni"
version = "0.21.1"
@@ -341,7 +472,7 @@ checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
dependencies = [
"cesu8",
"cfg-if",
"combine 4.6.7",
"combine",
"jni-sys",
"log",
"thiserror",
@@ -407,6 +538,17 @@ dependencies = [
"adler2",
]
[[package]]
name = "moisture"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da1c12ec95f955162f01e673f383afd3f1306a5d7254f0aa38c4d219066ad7b0"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "nix"
version = "0.29.0"
@@ -419,6 +561,12 @@ dependencies = [
"libc",
]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-traits"
version = "0.2.19"
@@ -428,44 +576,63 @@ dependencies = [
"autocfg",
]
[[package]]
name = "num_enum"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fae2a63210048cf77ceb6031b3f87ac69e4a68cfaacb17a0261e141c4bc8467b"
dependencies = [
"proc-macro2 0.4.30",
"quote 0.6.13",
"syn 0.15.44",
]
[[package]]
name = "object"
version = "0.36.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]]
name = "password-hash"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700"
dependencies = [
"base64ct",
"rand_core",
"subtle",
]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pbkdf2"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917"
dependencies = [
"digest",
"hmac",
"password-hash",
"sha2",
]
[[package]]
name = "pkcs8"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
dependencies = [
"der",
"spki",
]
[[package]]
name = "pkg-config"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.20"
@@ -475,15 +642,6 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "proc-macro2"
version = "0.4.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759"
dependencies = [
"unicode-xid",
]
[[package]]
name = "proc-macro2"
version = "1.0.93"
@@ -519,12 +677,29 @@ dependencies = [
]
[[package]]
name = "quote"
version = "0.6.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1"
name = "purrfectsnap"
version = "0.1.0"
dependencies = [
"proc-macro2 0.4.30",
"android_logger",
"base64",
"cc",
"crc32fast",
"dobby-rs",
"ed25519-dalek",
"goldberg",
"jni",
"libc",
"log",
"nix",
"once_cell",
"paste",
"procfs",
"rand",
"serde",
"serde_json",
"sha2",
"zip",
"zstd 0.13.2",
]
[[package]]
@@ -533,7 +708,7 @@ version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
dependencies = [
"proc-macro2 1.0.93",
"proc-macro2",
]
[[package]]
@@ -596,10 +771,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "rustc-demangle"
version = "0.1.26"
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
"semver",
]
[[package]]
name = "rustix"
@@ -635,6 +813,12 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "semver"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
[[package]]
name = "serde"
version = "1.0.217"
@@ -650,8 +834,8 @@ version = "1.0.217"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
dependencies = [
"proc-macro2 1.0.93",
"quote 1.0.38",
"proc-macro2",
"quote",
"syn 2.0.96",
]
@@ -667,6 +851,28 @@ dependencies = [
"serde",
]
[[package]]
name = "sha1"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "shlex"
version = "1.3.0"
@@ -674,32 +880,39 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "snapenhance"
version = "0.1.0"
name = "signature"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
dependencies = [
"android-ndk",
"android_logger",
"dobby-rs",
"jni 0.21.1",
"log",
"nix",
"once_cell",
"paste",
"procfs",
"rand",
"serde_json",
"zstd",
"rand_core",
]
[[package]]
name = "syn"
version = "0.15.44"
name = "spki"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5"
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
dependencies = [
"proc-macro2 0.4.30",
"quote 0.6.13",
"unicode-xid",
"base64ct",
"der",
]
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
@@ -708,8 +921,8 @@ version = "2.0.96"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80"
dependencies = [
"proc-macro2 1.0.93",
"quote 1.0.38",
"proc-macro2",
"quote",
"unicode-ident",
]
@@ -728,44 +941,48 @@ version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2 1.0.93",
"quote 1.0.38",
"proc-macro2",
"quote",
"syn 2.0.96",
]
[[package]]
name = "time"
version = "0.3.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d"
dependencies = [
"deranged",
"num-conv",
"powerfmt",
"serde",
"time-core",
]
[[package]]
name = "time-core"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b"
[[package]]
name = "typenum"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]]
name = "unicode-ident"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
[[package]]
name = "unicode-xid"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
[[package]]
name = "unreachable"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56"
dependencies = [
"void",
]
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "void"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
[[package]]
name = "walkdir"
version = "2.5.0"
@@ -802,8 +1019,8 @@ checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
dependencies = [
"bumpalo",
"log",
"proc-macro2 1.0.93",
"quote 1.0.38",
"proc-macro2",
"quote",
"syn 2.0.96",
"wasm-bindgen-shared",
]
@@ -814,7 +1031,7 @@ version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
dependencies = [
"quote 1.0.38",
"quote",
"wasm-bindgen-macro-support",
]
@@ -824,8 +1041,8 @@ version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [
"proc-macro2 1.0.93",
"quote 1.0.38",
"proc-macro2",
"quote",
"syn 2.0.96",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
@@ -1013,18 +1230,63 @@ version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2 1.0.93",
"quote 1.0.38",
"proc-macro2",
"quote",
"syn 2.0.96",
]
[[package]]
name = "zeroize"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
[[package]]
name = "zip"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261"
dependencies = [
"aes",
"byteorder",
"bzip2",
"constant_time_eq",
"crc32fast",
"crossbeam-utils",
"flate2",
"hmac",
"pbkdf2",
"sha1",
"time",
"zstd 0.11.2+zstd.1.5.2",
]
[[package]]
name = "zstd"
version = "0.11.2+zstd.1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4"
dependencies = [
"zstd-safe 5.0.2+zstd.1.5.2",
]
[[package]]
name = "zstd"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9"
dependencies = [
"zstd-safe",
"zstd-safe 7.2.1",
]
[[package]]
name = "zstd-safe"
version = "5.0.2+zstd.1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db"
dependencies = [
"libc",
"zstd-sys",
]
[[package]]

View File

@@ -1,7 +1,7 @@
[package]
name = "snapenhance"
name = "purrfectsnap"
version = "0.1.0"
authors = ["rhunk"]
authors = ["eternal"]
edition = "2021"
[lib]
@@ -9,7 +9,7 @@ crate-type = ["cdylib"]
[dependencies]
android_logger = "0.14.1"
android-ndk = "0.0.6"
dobby-rs = "0.1.0"
jni = "0.21.1"
log = "0.4.25"
nix = { version = "0.29.0", features = ["fs"] }
@@ -17,6 +17,17 @@ once_cell = "1.20.2"
paste = "1.0.15"
procfs = "0.17.0"
rand = "0.8.5"
serde_json = "1.0.135"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
zstd = "0.13.2"
dobby-rs = "0.1.0"
goldberg = "0.1.0"
crc32fast = "1.4.0"
libc = "0.2"
sha2 = "0.10"
zip = "0.6.6"
base64 = "0.22.1"
ed25519-dalek = { version = "2.1.1", default-features = true, features = ["std"] }
[build-dependencies]
cc = "1.0"

View File

@@ -1,3 +1,16 @@
fn main() {
println!("cargo:rerun-if-env-changed=OMVLL_PLUGIN_PATH");
let mut build = cc::Build::new();
build
.file("secure/secure_strings.c")
.flag_if_supported("-fvisibility=hidden");
if let Ok(plugin) = std::env::var("OMVLL_PLUGIN_PATH") {
build.flag(&format!("-fpass-plugin={plugin}"));
}
build.compile("secure_strings");
println!("cargo:rustc-link-lib=static=c++");
}
}

View File

@@ -0,0 +1,133 @@
#include "secure_strings.h"
#include <stdint.h>
#include <string.h>
// Simple XOR obfuscation for compile-time data
static const uint8_t KEY = 0x5Au;
static const uint8_t DET_LENS[] = {
5, 11, 6, 9, 15, 16, 7, 12, 9, 14, 23, 10, 21, 24, 10
};
static const uint8_t DET_ENC[] = {
// "argos"
'a'^KEY,'r'^KEY,'g'^KEY,'o'^KEY,'s'^KEY,
// "attestation"
'a'^KEY,'t'^KEY,'t'^KEY,'e'^KEY,'s'^KEY,'t'^KEY,'a'^KEY,'t'^KEY,'i'^KEY,'o'^KEY,'n'^KEY,
// "attest"
'a'^KEY,'t'^KEY,'t'^KEY,'e'^KEY,'s'^KEY,'t'^KEY,
// "integrity"
'i'^KEY,'n'^KEY,'t'^KEY,'e'^KEY,'g'^KEY,'r'^KEY,'i'^KEY,'t'^KEY,'y'^KEY,
// "clientintegrity"
'c'^KEY,'l'^KEY,'i'^KEY,'e'^KEY,'n'^KEY,'t'^KEY,'i'^KEY,'n'^KEY,'t'^KEY,'e'^KEY,'g'^KEY,'r'^KEY,'i'^KEY,'t'^KEY,'y'^KEY,
// "client_integrity"
'c'^KEY,'l'^KEY,'i'^KEY,'e'^KEY,'n'^KEY,'t'^KEY,'_'^KEY,'i'^KEY,'n'^KEY,'t'^KEY,'e'^KEY,'g'^KEY,'r'^KEY,'i'^KEY,'t'^KEY,'y'^KEY,
// "scargos"
's'^KEY,'c'^KEY,'a'^KEY,'r'^KEY,'g'^KEY,'o'^KEY,'s'^KEY,
// "argosservice"
'a'^KEY,'r'^KEY,'g'^KEY,'o'^KEY,'s'^KEY,'s'^KEY,'e'^KEY,'r'^KEY,'v'^KEY,'i'^KEY,'c'^KEY,'e'^KEY,
// "safetynet"
's'^KEY,'a'^KEY,'f'^KEY,'e'^KEY,'t'^KEY,'y'^KEY,'n'^KEY,'e'^KEY,'t'^KEY,
// "play_integrity"
'p'^KEY,'l'^KEY,'a'^KEY,'y'^KEY,'_'^KEY,'i'^KEY,'n'^KEY,'t'^KEY,'e'^KEY,'g'^KEY,'r'^KEY,'i'^KEY,'t'^KEY,'y'^KEY,
// "android_key_attestation"
'a'^KEY,'n'^KEY,'d'^KEY,'r'^KEY,'o'^KEY,'i'^KEY,'d'^KEY,'_'^KEY,'k'^KEY,'e'^KEY,'y'^KEY,'_'^KEY,'a'^KEY,'t'^KEY,'t'^KEY,'e'^KEY,'s'^KEY,'t'^KEY,'a'^KEY,'t'^KEY,'i'^KEY,'o'^KEY,'n'^KEY,
// "app_attest"
'a'^KEY,'p'^KEY,'p'^KEY,'_'^KEY,'a'^KEY,'t'^KEY,'t'^KEY,'e'^KEY,'s'^KEY,'t'^KEY,
// "getattestationheaders"
'g'^KEY,'e'^KEY,'t'^KEY,'a'^KEY,'t'^KEY,'t'^KEY,'e'^KEY,'s'^KEY,'t'^KEY,'a'^KEY,'t'^KEY,'i'^KEY,'o'^KEY,'n'^KEY,'h'^KEY,'e'^KEY,'a'^KEY,'d'^KEY,'e'^KEY,'r'^KEY,'s'^KEY,
// "computeattestationheader"
'c'^KEY,'o'^KEY,'m'^KEY,'p'^KEY,'u'^KEY,'t'^KEY,'e'^KEY,'a'^KEY,'t'^KEY,'t'^KEY,'e'^KEY,'s'^KEY,'t'^KEY,'a'^KEY,'t'^KEY,'i'^KEY,'o'^KEY,'n'^KEY,'h'^KEY,'e'^KEY,'a'^KEY,'d'^KEY,'e'^KEY,'r'^KEY,
// "hermod_dup"
'h'^KEY,'e'^KEY,'r'^KEY,'m'^KEY,'o'^KEY,'d'^KEY,'_'^KEY,'d'^KEY,'u'^KEY,'p'^KEY,
};
static char DET_DEC[207];
static size_t DET_LEN = 0;
static int DET_READY = 0;
static const uint8_t RISK_LENS[] = { 22, 24, 22, 19, 22, 22, 19 };
static const uint8_t RISK_ENC[] = {
// "ClientIntegrityService"
'C'^KEY,'l'^KEY,'i'^KEY,'e'^KEY,'n'^KEY,'t'^KEY,'I'^KEY,'n'^KEY,'t'^KEY,'e'^KEY,'g'^KEY,'r'^KEY,'i'^KEY,'t'^KEY,'y'^KEY,'S'^KEY,'e'^KEY,'r'^KEY,'v'^KEY,'i'^KEY,'c'^KEY,'e'^KEY,
// "/ClientIntegrityService/"
'/'^KEY,'C'^KEY,'l'^KEY,'i'^KEY,'e'^KEY,'n'^KEY,'t'^KEY,'I'^KEY,'n'^KEY,'t'^KEY,'e'^KEY,'g'^KEY,'r'^KEY,'i'^KEY,'t'^KEY,'y'^KEY,'S'^KEY,'e'^KEY,'r'^KEY,'v'^KEY,'i'^KEY,'c'^KEY,'e'^KEY,'/'^KEY,
// "/GetAttestationHeaders"
'/'^KEY,'G'^KEY,'e'^KEY,'t'^KEY,'A'^KEY,'t'^KEY,'t'^KEY,'e'^KEY,'s'^KEY,'t'^KEY,'a'^KEY,'t'^KEY,'i'^KEY,'o'^KEY,'n'^KEY,'H'^KEY,'e'^KEY,'a'^KEY,'d'^KEY,'e'^KEY,'r'^KEY,'s'^KEY,
// "/AttestationHeaders"
'/'^KEY,'A'^KEY,'t'^KEY,'t'^KEY,'e'^KEY,'s'^KEY,'t'^KEY,'a'^KEY,'t'^KEY,'i'^KEY,'o'^KEY,'n'^KEY,'H'^KEY,'e'^KEY,'a'^KEY,'d'^KEY,'e'^KEY,'r'^KEY,'s'^KEY,
// "/getAttestationHeaders"
'/'^KEY,'g'^KEY,'e'^KEY,'t'^KEY,'A'^KEY,'t'^KEY,'t'^KEY,'e'^KEY,'s'^KEY,'t'^KEY,'a'^KEY,'t'^KEY,'i'^KEY,'o'^KEY,'n'^KEY,'H'^KEY,'e'^KEY,'a'^KEY,'d'^KEY,'e'^KEY,'r'^KEY,'s'^KEY,
// "/GetAttestationPayload"
'/'^KEY,'G'^KEY,'e'^KEY,'t'^KEY,'A'^KEY,'t'^KEY,'t'^KEY,'e'^KEY,'s'^KEY,'t'^KEY,'a'^KEY,'t'^KEY,'i'^KEY,'o'^KEY,'n'^KEY,'P'^KEY,'a'^KEY,'y'^KEY,'l'^KEY,'o'^KEY,'a'^KEY,'d'^KEY,
// "/AttestationPayload"
'/'^KEY,'A'^KEY,'t'^KEY,'t'^KEY,'e'^KEY,'s'^KEY,'t'^KEY,'a'^KEY,'t'^KEY,'i'^KEY,'o'^KEY,'n'^KEY,'P'^KEY,'a'^KEY,'y'^KEY,'l'^KEY,'o'^KEY,'a'^KEY,'d'^KEY,
};
static char RISK_DEC[157];
static size_t RISK_LEN = 0;
static int RISK_READY = 0;
static size_t decode_list(const uint8_t* enc, size_t enc_len, const uint8_t* lens, size_t n, char* out, size_t out_cap) {
size_t off = 0;
size_t enc_off = 0;
for (size_t i = 0; i < n && enc_off < enc_len; ++i) {
size_t L = (size_t)lens[i];
if (off + L + 1 > out_cap || enc_off + L > enc_len) break;
for (size_t j = 0; j < L; ++j) {
out[off++] = (char)(enc[enc_off++] ^ KEY);
}
out[off++] = '\0';
}
return off;
}
const char* ss_get_detection_blob(size_t* out_len) {
if (!DET_READY) {
DET_LEN = decode_list(DET_ENC,
sizeof(DET_ENC),
DET_LENS,
sizeof(DET_LENS)/sizeof(DET_LENS[0]),
DET_DEC,
sizeof(DET_DEC));
DET_READY = 1;
}
if (out_len) {
*out_len = DET_LEN;
}
return DET_DEC;
}
const char* ss_get_risk_block_blob(size_t* out_len) {
if (!RISK_READY) {
RISK_LEN = decode_list(RISK_ENC,
sizeof(RISK_ENC),
RISK_LENS,
sizeof(RISK_LENS)/sizeof(RISK_LENS[0]),
RISK_DEC,
sizeof(RISK_DEC));
RISK_READY = 1;
}
if (out_len) {
*out_len = RISK_LEN;
}
return RISK_DEC;
}
const char* ss_get_hermod_dup(size_t* out_len) {
static char buf[16];
static int ready = 0;
if (!ready) {
size_t enc_off = 0;
for (size_t i = 0; i < sizeof(DET_LENS); ++i) {
size_t L = DET_LENS[i];
if (i == 14) {
size_t w = (L < sizeof(buf)-1) ? L : (sizeof(buf)-1);
for (size_t j = 0; j < w; ++j) buf[j] = (char)(DET_ENC[enc_off+j] ^ KEY);
buf[w] = '\0';
break;
}
enc_off += L;
}
ready = 1;
}
if (out_len) *out_len = strlen(buf);
return buf;
}

View File

@@ -0,0 +1,10 @@
// Minimal C interface to expose obfuscated keyword lists to Rust (FFI)
#pragma once
#include <stddef.h>
// Returns a pointer to a NUL-separated blob of decoded strings and sets total length in bytes.
// The memory is owned by the C module and valid for the lifetime of the process.
const char* ss_get_detection_blob(size_t* out_len);
const char* ss_get_hermod_dup(size_t* out_len);
const char* ss_get_risk_block_blob(size_t* out_len);

View File

@@ -1,6 +1,6 @@
use std::{error::Error, sync::Mutex};
use jni::{objects::JObject, JNIEnv};
use crate::util::get_jni_string;
use crate::{secstrings, util::get_jni_string};
static NATIVE_CONFIG: Mutex<Option<NativeConfig>> = Mutex::new(None);
@@ -45,6 +45,34 @@ impl NativeConfig {
}
}
pub struct BlockerConfig {
pub allowed_eps_active: Vec<String>,
pub detection_keywords: Vec<String>,
pub risk_block_list: Vec<String>,
}
pub fn get_blocker_config() -> BlockerConfig {
let config_str = include_str!("../../../config/config.json");
let raw: serde_json::Value = serde_json::from_str(config_str).unwrap();
let allowed = raw["allowed_eps_active"].as_array().cloned().unwrap_or_default();
BlockerConfig {
allowed_eps_active: allowed
.into_iter()
.filter_map(|value| value.as_str().map(|s| s.to_lowercase()))
.collect(),
detection_keywords: secstrings::get_detection_keywords()
.into_iter()
.map(|s| s.to_lowercase())
.collect(),
risk_block_list: secstrings::get_risk_block_list()
.into_iter()
.map(|s| s.to_lowercase())
.collect(),
}
}
pub fn load_config(mut env: JNIEnv, _class: JObject, obj: JObject) {
NATIVE_CONFIG.lock().unwrap().replace(
NativeConfig::new(&mut env, obj).expect("Failed to load NativeConfig")

View File

@@ -10,15 +10,34 @@ mod config;
mod sig;
mod modules;
mod security;
mod secstrings;
use android_logger::Config;
use log::LevelFilter;
use modules::{composer_hook, custom_font_hook, duplex_hook, fstat_hook, linker_hook, sqlite_hook, unary_call_hook};
use jni::{JNIEnv, JavaVM, NativeMethod};
use jni::objects::{JObject, JString};
use jni::sys::{jint, jstring, JNI_VERSION_1_6};
use jni::objects::{JObject, JString, JClass, JValue};
use jni::sys::{jint, jstring, JNI_VERSION_1_6, jboolean, JNI_FALSE, JNI_TRUE};
use sha2::{Digest, Sha256};
use std::ffi::c_void;
use std::sync::atomic::{AtomicBool, Ordering};
use once_cell::sync::Lazy;
use std::collections::HashMap;
use std::sync::Mutex;
static IS_VERIFIED: AtomicBool = AtomicBool::new(false);
static TEST_MODE: AtomicBool = AtomicBool::new(false);
static IN_LOGIN_SIGNUP: AtomicBool = AtomicBool::new(false);
static CHECKSUMS: Lazy<Mutex<HashMap<String, u32>>> = Lazy::new(|| Mutex::new(HashMap::new()));
struct BlockerDecision {
blocked: bool,
reason: &'static str,
keyword: Option<String>,
keyword_context: Option<&'static str>,
}
#[allow(non_snake_case)]
#[no_mangle]
@@ -26,11 +45,13 @@ pub extern "system" fn JNI_OnLoad(_vm: JavaVM, _: *mut c_void) -> jint {
android_logger::init_once(
Config::default()
.with_max_level(LevelFilter::Debug)
.with_tag("SnapEnhanceNative")
.with_tag("PurrfectSnapNative")
);
info!("JNI_OnLoad called");
security::start_anti_debug_thread();
std::panic::set_hook(Box::new(|panic_info| {
error!("{:?}", panic_info);
}));
@@ -39,11 +60,16 @@ pub extern "system" fn JNI_OnLoad(_vm: JavaVM, _: *mut c_void) -> jint {
let mut env = _vm.get_env().expect("Failed to get JNIEnv");
let native_lib_class = env.find_class("me/rhunk/snapenhance/nativelib/NativeLib").expect("NativeLib class not found");
let native_lib_class = env.find_class("me/eternal/purrfectsnap/nativelib/NativeLib").expect("NativeLib class not found");
env.register_native_methods(
native_lib_class,
&[
NativeMethod {
name: "verifyKey".into(),
sig: "(Ljava/lang/String;)Z".into(),
fn_ptr: verifyKey as *mut c_void,
},
NativeMethod {
name: "preInit".into(),
sig: "()V".into(),
@@ -56,7 +82,7 @@ pub extern "system" fn JNI_OnLoad(_vm: JavaVM, _: *mut c_void) -> jint {
},
NativeMethod {
name: "loadConfig".into(),
sig: "(Lme/rhunk/snapenhance/nativelib/NativeConfig;)V".into(),
sig: "(Lme/eternal/purrfectsnap/nativelib/NativeConfig;)V".into(),
fn_ptr: config::load_config as *mut c_void,
},
NativeMethod {
@@ -78,13 +104,60 @@ pub extern "system" fn JNI_OnLoad(_vm: JavaVM, _: *mut c_void) -> jint {
name: "composerEval".into(),
sig: "(Ljava/lang/String;)Ljava/lang/String;".into(),
fn_ptr: composer_hook::composer_eval as *mut c_void,
}
},
NativeMethod {
name: "evaluateEndpointNative".into(),
sig: "(Ljava/lang/String;Ljava/lang/String;ZLme/eternal/purrfectsnap/nativelib/NativeDecision;)V".into(),
fn_ptr: evaluateEndpoint as *mut c_void,
},
NativeMethod {
name: "shouldBlockDuplexClient".into(),
sig: "(Ljava/lang/String;)Z".into(),
fn_ptr: shouldBlockDuplexClient as *mut c_void,
},
NativeMethod {
name: "evaluateAuthContextNative".into(),
sig: "(Ljava/lang/String;ZLme/eternal/purrfectsnap/nativelib/NativeDecision;)V".into(),
fn_ptr: evaluateAuthContext as *mut c_void,
},
NativeMethod {
name: "evaluateApiInvocationNative".into(),
sig: "(Ljava/lang/String;Ljava/lang/String;Lme/eternal/purrfectsnap/nativelib/NativeDecision;)V".into(),
fn_ptr: evaluateApiInvocation as *mut c_void,
},
NativeMethod {
name: "runEndpointSelfTest".into(),
sig: "(Z)Z".into(),
fn_ptr: runEndpointSelfTest as *mut c_void,
},
NativeMethod {
name: "setChecksums".into(),
sig: "(Ljava/lang/String;)V".into(),
fn_ptr: setChecksums as *mut c_void,
},
NativeMethod {
name: "setTestMode".into(),
sig: "(Z)V".into(),
fn_ptr: setTestMode as *mut c_void,
},
NativeMethod {
name: "setInLoginSignup".into(),
sig: "(Z)V".into(),
fn_ptr: setInLoginSignup as *mut c_void,
},
]
).expect("Failed to register native methods");
JNI_VERSION_1_6
}
#[allow(non_snake_case)]
fn setChecksums(mut env: JNIEnv, _class: JClass, checksums_json: JString) {
let checksums_str: String = env.get_string(&checksums_json).unwrap().into();
let checksums: HashMap<String, u32> = serde_json::from_str(&checksums_str).unwrap();
*CHECKSUMS.lock().unwrap() = checksums;
}
fn pre_init(_env: JNIEnv, _class: JObject) {
debug!("Pre init");
linker_hook::init();
@@ -145,3 +218,547 @@ fn init(mut env: JNIEnv, _class: JObject, signature_cache: JString) -> jstring {
std::ptr::null_mut()
}
}
#[allow(non_snake_case)]
fn verifyKey(mut env: JNIEnv, _class: JClass, key: JString) -> jboolean {
fn bytes_to_hex(bytes: &[u8]) -> String {
const LUT: &[u8; 16] = b"0123456789abcdef";
let mut out = Vec::with_capacity(bytes.len() * 2);
for &b in bytes {
out.push(LUT[(b >> 4) as usize]);
out.push(LUT[(b & 0x0f) as usize]);
}
String::from_utf8_lossy(&out).into_owned()
}
fn normalize_hex(s: &str) -> String {
s.chars()
.filter(|c| c.is_ascii_hexdigit())
.map(|c| c.to_ascii_lowercase())
.collect()
}
fn get_pkg_cert_sha256_hex(env: &mut JNIEnv, pkg: &str) -> Option<String> {
let at = env.find_class("android/app/ActivityThread").ok()?;
let app_obj = env
.call_static_method(at, "currentApplication", "()Landroid/app/Application;", &[])
.ok()?
.l()
.ok()?;
if app_obj.is_null() {
return None;
}
let pm = env
.call_method(&app_obj, "getPackageManager", "()Landroid/content/pm/PackageManager;", &[])
.ok()?
.l()
.ok()?;
let pkg_j = env.new_string(pkg).ok()?.into();
let flags = 0x08000000i32; // PackageManager.GET_SIGNING_CERTIFICATES (API 28+)
let info = env
.call_method(
&pm,
"getPackageInfo",
"(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;",
&[JValue::Object(&pkg_j), JValue::Int(flags)],
)
.ok()?
.l()
.ok()?;
let signing_info = env
.get_field(&info, "signingInfo", "Landroid/content/pm/SigningInfo;")
.ok()?
.l()
.ok()?;
if signing_info.is_null() {
return None;
}
let signers = env
.call_method(
&signing_info,
"getApkContentsSigners",
"()[Landroid/content/pm/Signature;",
&[],
)
.ok()?
.l()
.ok()?;
let signers_arr = jni::objects::JObjectArray::from(signers);
let first = env.get_object_array_element(&signers_arr, 0).ok()?;
let sig_bytes_obj = env
.call_method(&first, "toByteArray", "()[B", &[])
.ok()?
.l()
.ok()?;
let sig_bytes = env
.convert_byte_array(jni::objects::JByteArray::from(sig_bytes_obj))
.ok()?;
let digest = Sha256::digest(&sig_bytes);
Some(bytes_to_hex(&digest))
}
// Harden the check: the provided value must match the module APK signing cert SHA-256.
// This prevents trivial re-signing/repacking from passing verification without patching native code.
let expected_cert = normalize_hex(
&env.get_string(&key)
.ok()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default(),
);
if expected_cert.is_empty() {
return JNI_FALSE;
}
let actual_cert = get_pkg_cert_sha256_hex(&mut env, "me.eternal.purrfectsnap")
.map(|s| normalize_hex(&s))
.unwrap_or_default();
if actual_cert.is_empty() {
return JNI_FALSE;
}
if expected_cert == actual_cert {
IS_VERIFIED.store(true, Ordering::Relaxed);
JNI_TRUE
} else {
JNI_FALSE
}
}
fn find_keyword(paths: &[&str], keywords: &[String]) -> Option<String> {
for path in paths {
let lower_path = path.to_lowercase();
for keyword in keywords {
if lower_path.contains(keyword) {
return Some(keyword.clone());
}
}
}
None
}
fn evaluate_endpoint_logic(
config: &config::BlockerConfig,
uri: &str,
arg0: &str,
has_attestation: bool,
) -> BlockerDecision {
let targets = [uri, arg0];
let detection_keyword = find_keyword(&targets, &config.detection_keywords);
let snap_security_block = targets.iter().any(|t| {
let lower = t.to_lowercase();
lower.starts_with("/snap.security") && !lower.starts_with("/snap.security.argosservice")
});
let block_convo_safety_prompt = targets.iter().any(|t| {
t.eq_ignore_ascii_case("/snapchat.abuse.conversationsafety.conversationsafetyservice/getconvosafetyprompt")
});
let block_convo_safety_service = targets.iter().any(|t| {
t.to_lowercase()
.starts_with("/snapchat.abuse.conversationsafety.conversationsafetyservice/")
});
let block_device_state_report = targets.iter().any(|t| {
t.eq_ignore_ascii_case("/snapchat.notif.devicestatereceiver/reportdevicestate")
});
if snap_security_block {
return BlockerDecision {
blocked: true,
reason: "snap_security_block",
keyword: detection_keyword,
keyword_context: None,
};
}
if block_convo_safety_prompt {
return BlockerDecision {
blocked: true,
reason: "conversation_safety_prompt",
keyword: detection_keyword,
keyword_context: None,
};
}
if block_convo_safety_service {
return BlockerDecision {
blocked: true,
reason: "conversation_safety_service",
keyword: detection_keyword,
keyword_context: None,
};
}
if block_device_state_report {
return BlockerDecision {
blocked: true,
reason: "notif_report_device_state",
keyword: detection_keyword,
keyword_context: None,
};
}
if find_keyword(&targets, &config.allowed_eps_active).is_some() {
return BlockerDecision {
blocked: false,
reason: "allowed_whitelist",
keyword: detection_keyword,
keyword_context: None,
};
}
let reason = if detection_keyword.is_some() && has_attestation {
"allowed_attestation_keyword"
} else if detection_keyword.is_some() {
"allowed_detection_keyword"
} else {
"allowed"
};
let blocked = false;
BlockerDecision {
blocked,
reason,
keyword: detection_keyword,
keyword_context: None,
}
}
fn evaluate_auth_context_logic(
config: &config::BlockerConfig,
request_path: &str,
attestation_required: bool,
) -> BlockerDecision {
let targets = [request_path];
let detection_keyword = find_keyword(&targets, &config.detection_keywords);
let snap_security_block = targets.iter().any(|t| {
let lower = t.to_lowercase();
lower.starts_with("/snap.security") && !lower.starts_with("/snap.security.argosservice")
});
let block_convo_safety_prompt = targets.iter().any(|t| {
t.eq_ignore_ascii_case("/snapchat.abuse.conversationsafety.conversationsafetyservice/getconvosafetyprompt")
});
let block_convo_safety_service = targets.iter().any(|t| {
t.to_lowercase()
.starts_with("/snapchat.abuse.conversationsafety.conversationsafetyservice/")
});
let block_device_state_report = targets.iter().any(|t| {
t.eq_ignore_ascii_case("/snapchat.notif.devicestatereceiver/reportdevicestate")
});
if snap_security_block {
return BlockerDecision {
blocked: true,
reason: "snap_security_block",
keyword: detection_keyword,
keyword_context: None,
};
}
if block_convo_safety_prompt {
return BlockerDecision {
blocked: true,
reason: "conversation_safety_prompt",
keyword: detection_keyword,
keyword_context: None,
};
}
if block_convo_safety_service {
return BlockerDecision {
blocked: true,
reason: "conversation_safety_service",
keyword: detection_keyword,
keyword_context: None,
};
}
if block_device_state_report {
return BlockerDecision {
blocked: true,
reason: "notif_report_device_state",
keyword: detection_keyword,
keyword_context: None,
};
}
if find_keyword(&targets, &config.allowed_eps_active).is_some() {
return BlockerDecision {
blocked: false,
reason: "allowed_whitelist",
keyword: detection_keyword,
keyword_context: None,
};
}
let reason = if detection_keyword.is_some() && attestation_required {
"allowed_attestation_keyword"
} else if detection_keyword.is_some() {
"allowed_detection_keyword"
} else {
"allowed"
};
let blocked = false;
BlockerDecision {
blocked,
reason,
keyword: detection_keyword,
keyword_context: None,
}
}
fn evaluate_api_invocation_logic(
config: &config::BlockerConfig,
method_id: &str,
annotations: &str,
) -> BlockerDecision {
// Hard allow specific API calls regardless of keyword matches
const API_ALLOWLIST: &[&str] = &[
"com.snap.identity.network.suggestion.bqsuggestfriendhttpinterface.fetchhighavailablesuggestedfriend",
"com.snap.identity.network.suggestion.bqsuggestfriendhttpinterface.fetchlegacysuggestedfriend",
];
let method_lower = method_id.to_lowercase();
if API_ALLOWLIST.iter().any(|m| method_lower == *m) {
return BlockerDecision {
blocked: false,
reason: "allowed_api_whitelist",
keyword: None,
keyword_context: None,
};
}
if let Some(keyword) = find_keyword(&[method_id], &config.detection_keywords) {
return BlockerDecision {
blocked: true,
reason: "detection_keyword",
keyword: Some(keyword),
keyword_context: Some("method"),
};
}
if let Some(keyword) = find_keyword(&[annotations], &config.detection_keywords) {
return BlockerDecision {
blocked: true,
reason: "detection_keyword",
keyword: Some(keyword),
keyword_context: Some("annotation"),
};
}
BlockerDecision {
blocked: false,
reason: "allowed",
keyword: None,
keyword_context: None,
}
}
fn write_blocker_decision(env: &mut JNIEnv, decision_obj: JObject, decision: &BlockerDecision) {
write_decision(
env,
decision_obj,
decision.blocked,
decision.reason,
decision.keyword.as_deref(),
decision.keyword_context,
);
}
fn write_decision(env: &mut JNIEnv, decision: JObject, blocked: bool, reason: &str, keyword: Option<&str>, keyword_context: Option<&str>) {
let blocked_value = if blocked { JNI_TRUE } else { JNI_FALSE };
env.set_field(&decision, "blocked", "Z", JValue::Bool(blocked_value)).expect("failed to set blocked");
set_string_field(env, &decision, "reason", Some(reason));
set_string_field(env, &decision, "keyword", keyword);
set_string_field(env, &decision, "keywordContext", keyword_context);
}
fn set_string_field(env: &mut JNIEnv, obj: &JObject, field: &str, value: Option<&str>) {
if let Some(text) = value {
let jstring = env.new_string(text).expect("failed to alloc string");
let j_obj = JObject::from(jstring);
env.set_field(obj, field, "Ljava/lang/String;", JValue::Object(&j_obj)).expect("failed to set string field");
env.delete_local_ref(j_obj).expect("failed to delete local ref");
} else {
let null_obj = JObject::null();
env.set_field(obj, field, "Ljava/lang/String;", JValue::Object(&null_obj)).expect("failed to clear string field");
}
}
#[allow(non_snake_case)]
fn evaluateEndpoint(
mut env: JNIEnv,
_class: JClass,
uri: JString,
arg0: JString,
has_attestation: jboolean,
decision: JObject,
) {
if IN_LOGIN_SIGNUP.load(Ordering::Relaxed) {
write_decision(&mut env, decision, false, "allowed_login_signup", None, None);
return;
}
let is_verified = IS_VERIFIED.load(Ordering::Relaxed);
let test_mode = TEST_MODE.load(Ordering::Relaxed);
if !is_verified && !test_mode {
write_decision(&mut env, decision, false, "allowed", None, None);
return;
}
let uri_str: String = env.get_string(&uri).unwrap().into();
let arg0_str: String = env.get_string(&arg0).unwrap().into();
let config = config::get_blocker_config();
let blocker_decision = evaluate_endpoint_logic(&config, &uri_str, &arg0_str, has_attestation == JNI_TRUE);
write_blocker_decision(&mut env, decision, &blocker_decision);
}
#[allow(non_snake_case)]
fn shouldBlockDuplexClient(
mut env: JNIEnv,
_class: JClass,
path: JString,
) -> jboolean {
if IN_LOGIN_SIGNUP.load(Ordering::Relaxed) {
return JNI_FALSE;
}
let is_verified = IS_VERIFIED.load(Ordering::Relaxed);
let test_mode = TEST_MODE.load(Ordering::Relaxed);
if !is_verified && !test_mode {
return JNI_FALSE;
}
let path_str: String = env.get_string(&path).unwrap().into();
let hermod = secstrings::get_hermod_dup();
if path_str == hermod {
JNI_TRUE
} else {
JNI_FALSE
}
}
#[allow(non_snake_case)]
fn evaluateAuthContext(
mut env: JNIEnv,
_class: JClass,
request_path: JString,
attestation_required: jboolean,
decision: JObject,
) {
if IN_LOGIN_SIGNUP.load(Ordering::Relaxed) {
write_decision(&mut env, decision, false, "allowed_login_signup", None, None);
return;
}
let is_verified = IS_VERIFIED.load(Ordering::Relaxed);
let test_mode = TEST_MODE.load(Ordering::Relaxed);
if !is_verified && !test_mode {
write_decision(&mut env, decision, false, "allowed", None, None);
return;
}
let request_path_str: String = env.get_string(&request_path).unwrap().into();
let config = config::get_blocker_config();
let blocker_decision = evaluate_auth_context_logic(&config, &request_path_str, attestation_required == JNI_TRUE);
write_blocker_decision(&mut env, decision, &blocker_decision);
}
#[allow(non_snake_case)]
fn evaluateApiInvocation(
mut env: JNIEnv,
_class: JClass,
method_id: JString,
annotations: JString,
decision: JObject,
) {
if IN_LOGIN_SIGNUP.load(Ordering::Relaxed) {
write_decision(&mut env, decision, false, "allowed_login_signup", None, None);
return;
}
let is_verified = IS_VERIFIED.load(Ordering::Relaxed);
let test_mode = TEST_MODE.load(Ordering::Relaxed);
if !is_verified && !test_mode {
write_decision(&mut env, decision, false, "allowed", None, None);
return;
}
let method_id_str: String = env.get_string(&method_id).unwrap().into();
let annotations_str: String = env.get_string(&annotations).unwrap().into();
let config = config::get_blocker_config();
let blocker_decision = evaluate_api_invocation_logic(&config, &method_id_str, &annotations_str);
write_blocker_decision(&mut env, decision, &blocker_decision);
}
fn run_blocker_self_test(allow_unverified: bool) -> bool {
if !IS_VERIFIED.load(Ordering::Relaxed) && !allow_unverified {
return false;
}
let config = config::get_blocker_config();
if config.allowed_eps_active.is_empty()
|| config.detection_keywords.is_empty()
|| config.risk_block_list.is_empty()
{
return false;
}
let allowed_sample = config.allowed_eps_active[0].clone();
let detection_sample = config.detection_keywords[0].clone();
let risk_sample = config.risk_block_list[0].clone();
let allowed_decision = evaluate_endpoint_logic(&config, &allowed_sample, &allowed_sample, false);
if allowed_decision.blocked || allowed_decision.reason != "allowed_whitelist" {
return false;
}
let detection_path = format!("/self_test/{}", detection_sample);
let detection_decision = evaluate_endpoint_logic(&config, &detection_path, "", false);
if detection_decision.blocked {
return false;
}
let attestation_decision = evaluate_endpoint_logic(&config, &detection_path, "", true);
if attestation_decision.blocked {
return false;
}
let risk_decision = evaluate_endpoint_logic(&config, &risk_sample, "", false);
if risk_decision.blocked {
return false;
}
let auth_detection = evaluate_auth_context_logic(&config, &detection_path, false);
if auth_detection.blocked {
return false;
}
let auth_allowed = evaluate_auth_context_logic(&config, &allowed_sample, false);
if auth_allowed.blocked || auth_allowed.reason != "allowed_whitelist" {
return false;
}
let api_method = format!("com.snap.obf.SelfTest{}", detection_sample);
let api_decision = evaluate_api_invocation_logic(&config, &api_method, "");
if !api_decision.blocked || api_decision.keyword.is_none() {
return false;
}
let annotation_blob = format!("@Requires{}", detection_sample);
let api_annotation_decision = evaluate_api_invocation_logic(&config, "com.snap.obf.Safe", &annotation_blob);
if !api_annotation_decision.blocked || api_annotation_decision.keyword_context != Some("annotation") {
return false;
}
true
}
#[allow(non_snake_case)]
fn setTestMode(_env: JNIEnv, _class: JClass, test_mode: jboolean) {
TEST_MODE.store(test_mode == JNI_TRUE, Ordering::Relaxed);
}
#[allow(non_snake_case)]
fn setInLoginSignup(_env: JNIEnv, _class: JClass, in_login_signup: jboolean) {
IN_LOGIN_SIGNUP.store(in_login_signup == JNI_TRUE, Ordering::Relaxed);
}
#[allow(non_snake_case)]
fn runEndpointSelfTest(_env: JNIEnv, _class: JClass, test_mode: jboolean) -> jboolean {
if run_blocker_self_test(test_mode == JNI_TRUE) {
JNI_TRUE
} else {
JNI_FALSE
}
}

View File

@@ -1,7 +1,7 @@
#![allow(dead_code, unused_imports)]
use super::util::composer_utils::{ComposerModule, ModuleTag};
use std::{collections::HashMap, ffi::{c_void, CStr}, sync::Mutex};
use std::{collections::HashMap, ffi::{c_void, CStr}, sync::{atomic::{AtomicPtr, Ordering}, Mutex}};
use jni::{objects::JString, sys::jobject, JNIEnv};
use once_cell::sync::Lazy;
use crate::{common, config, def_hook, dobby_hook, dobby_hook_sym, sig, util::get_jni_string};
@@ -154,12 +154,12 @@ def_hook!(
);
#[cfg(target_arch = "aarch64")]
static mut GLOBAL_INSTANCE: Option<*mut c_void> = None;
static GLOBAL_INSTANCE: AtomicPtr<c_void> = AtomicPtr::new(std::ptr::null_mut());
#[cfg(target_arch = "aarch64")]
static mut GLOBAL_CTX: Option<*mut c_void> = None;
static GLOBAL_CTX: AtomicPtr<c_void> = AtomicPtr::new(std::ptr::null_mut());
#[cfg(target_arch = "aarch64")]
static mut JS_EVAL_ORIGINAL2: Option<unsafe extern "C" fn(*mut c_void, *mut c_void, *mut c_void, *mut u8, usize, *const u8, u32) -> JsValue> = None;
static JS_EVAL_ORIGINAL2: Lazy<Mutex<Option<unsafe extern "C" fn(*mut c_void, *mut c_void, *mut c_void, *mut u8, usize, *const u8, u32) -> JsValue>>> = Lazy::new(|| Mutex::new(None));
def_hook!(
js_eval,
@@ -167,8 +167,8 @@ def_hook!(
|arg0: *mut c_void, arg1: *mut c_void, arg2: *mut c_void, arg3: *const u8, arg4: *const u8, arg5: *const u8, arg6: *mut c_void, arg7: u32| {
#[cfg(target_arch = "aarch64")]
{
GLOBAL_INSTANCE = Some(arg0);
GLOBAL_CTX = Some(arg1);
GLOBAL_INSTANCE.store(arg0, Ordering::Relaxed);
GLOBAL_CTX.store(arg1, Ordering::Relaxed);
}
js_eval_original.unwrap()(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7)
}
@@ -190,7 +190,7 @@ pub unsafe fn composer_eval(env: JNIEnv, _: *mut c_void, script: JString) -> job
Err(_) => return std::ptr::null_mut(),
};
if JS_EVAL_ORIGINAL2.is_none() || GLOBAL_INSTANCE.is_none() || GLOBAL_CTX.is_none() {
if JS_EVAL_ORIGINAL2.lock().unwrap().is_none() || GLOBAL_INSTANCE.load(Ordering::Relaxed).is_null() || GLOBAL_CTX.load(Ordering::Relaxed).is_null() {
if let Ok(s) = env.new_string("Composer hook not initialized") {
return s.into_raw();
}
@@ -199,15 +199,21 @@ pub unsafe fn composer_eval(env: JNIEnv, _: *mut c_void, script: JString) -> job
let script_length = script_str.len();
let js_value = JS_EVAL_ORIGINAL2.unwrap()(
GLOBAL_INSTANCE.unwrap(),
GLOBAL_CTX.unwrap(),
std::ptr::null_mut(),
(script_str + "\0").as_ptr() as *mut u8,
script_length,
"<eval>\0".as_ptr(),
0
);
let js_value = {
let js_eval_fn = JS_EVAL_ORIGINAL2.lock().unwrap();
let global_instance = GLOBAL_INSTANCE.load(Ordering::Relaxed);
let global_ctx = GLOBAL_CTX.load(Ordering::Relaxed);
js_eval_fn.unwrap()(
global_instance,
global_ctx,
std::ptr::null_mut(),
(script_str + "\0").as_ptr() as *mut u8,
script_length,
"<eval>\0".as_ptr(),
0
)
};
let result: String = if js_value.tag == JS_TAG_STRING {
let string = js_value.u.ptr as *mut JsString;
@@ -270,7 +276,7 @@ pub fn init() {
dobby_hook!(signature as *mut c_void, js_eval);
unsafe {
JS_EVAL_ORIGINAL2 = Some(std::mem::transmute(js_eval_original.unwrap()));
*JS_EVAL_ORIGINAL2.lock().unwrap() = Some(std::mem::transmute(js_eval_original.unwrap()));
}
debug!("js_eval {:#x}", signature);
@@ -279,4 +285,3 @@ pub fn init() {
}
}
}

View File

@@ -113,11 +113,11 @@ pub fn init() {
env.get_method_id(
env.get_object_class(common::native_lib_instance()).unwrap(),
"onNativeUnaryCall",
"(Ljava/lang/String;[B)Lme/rhunk/snapenhance/nativelib/NativeRequestData;"
"(Ljava/lang/String;[B)Lme/eternal/purrfectsnap/nativelib/NativeRequestData;"
).expect("Failed to get onNativeUnaryCall method id")
).expect("unary call method already set");
});
} else {
error!("Can't find unaryCall signature");
}
}
}

View File

@@ -0,0 +1,47 @@
use std::ffi::CStr;
#[allow(improper_ctypes)]
extern "C" {
fn ss_get_detection_blob(out_len: *mut usize) -> *const u8;
fn ss_get_hermod_dup(out_len: *mut usize) -> *const u8;
fn ss_get_risk_block_blob(out_len: *mut usize) -> *const u8;
}
fn blob_to_vec(blob_ptr: *const u8, len: usize) -> Vec<String> {
if blob_ptr.is_null() || len == 0 { return Vec::new(); }
// Safety: blob is a NUL-separated set of C strings
let slice = unsafe { std::slice::from_raw_parts(blob_ptr, len) };
let mut result = Vec::new();
let mut start = 0usize;
for i in 0..slice.len() {
if slice[i] == 0 { // NUL
if i > start {
let cstr = unsafe { CStr::from_bytes_with_nul_unchecked(&slice[start..=i]) };
result.push(cstr.to_string_lossy().into_owned());
}
start = i + 1;
}
}
result
}
pub fn get_detection_keywords() -> Vec<String> {
let mut len: usize = 0;
let ptr = unsafe { ss_get_detection_blob(&mut len as *mut usize) };
blob_to_vec(ptr, len)
}
pub fn get_hermod_dup() -> String {
let mut len: usize = 0;
let ptr = unsafe { ss_get_hermod_dup(&mut len as *mut usize) };
if ptr.is_null() || len == 0 { return String::new(); }
let slice = unsafe { std::slice::from_raw_parts(ptr, len) };
String::from_utf8_lossy(slice).into_owned()
}
pub fn get_risk_block_list() -> Vec<String> {
let mut len: usize = 0;
let ptr = unsafe { ss_get_risk_block_blob(&mut len as *mut usize) };
blob_to_vec(ptr, len)
}

View File

@@ -0,0 +1,96 @@
extern crate libc;
use jni::sys::{jboolean, JNI_FALSE, JNI_TRUE};
use goldberg::goldberg_string;
use std::{thread, time};
use std::fs;
use std::process;
use crate::CHECKSUMS;
use std::ffi::CString;
use std::str;
fn check_for_debugger() {
loop {
let status = fs::read_to_string("/proc/self/status").unwrap_or_default();
if !status.contains("TracerPid:\t0") {
process::abort();
}
let maps = fs::read_to_string("/proc/self/maps").unwrap_or_default();
if maps.contains("frida") || maps.contains("gumjs") || maps.contains("gdb") {
process::abort();
}
thread::sleep(time::Duration::from_secs(5));
}
}
pub fn start_anti_debug_thread() {
thread::spawn(move || {
check_for_debugger();
});
thread::spawn(move || {
verify_library_checksum();
});
}
fn get_abi() -> Option<String> {
let key = CString::new("ro.product.cpu.abi").unwrap();
let mut value = [0 as libc::c_char; 92];
let len = unsafe { libc::__system_property_get(key.as_ptr(), value.as_mut_ptr()) };
if len > 0 {
let value_slice = unsafe { std::slice::from_raw_parts(value.as_ptr() as *const u8, len as usize) };
let value_str = str::from_utf8(value_slice).unwrap();
Some(value_str.to_string())
} else {
None
}
}
fn verify_library_checksum() {
loop {
if let Some(path) = get_library_path() {
if let Ok(data) = fs::read(path) {
let checksum = crc32fast::hash(&data);
if let Some(abi) = get_abi() {
if let Some(expected_checksum) = CHECKSUMS.lock().unwrap().get(&abi) {
if checksum != *expected_checksum as u32 {
process::abort();
}
}
}
}
}
thread::sleep(time::Duration::from_secs(60));
}
}
pub fn verify_key(key: &str) -> bool {
// Transformed key verification
let transformed_key = key.chars().rev().collect::<String>();
let expected_key = goldberg_string!("qd84R1hT6p7Mk").chars().rev().collect::<String>();
transformed_key == expected_key
}
// this function will be called from the JNI, it will verify the key and if it's correct, it will return true
// otherwise it will return false
pub fn jni_verify_key(key: &str) -> jboolean {
if verify_key(key) {
JNI_TRUE
} else {
JNI_FALSE
}
}
fn get_library_path() -> Option<String> {
let maps = std::fs::read_to_string("/proc/self/maps").ok()?;
for line in maps.lines() {
if line.ends_with("libpurrfectsnap.so") {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() > 5 {
return Some(parts[5].to_string());
}
}
}
None
}