This commit is contained in:
2026-02-26 16:59:30 +08:00
commit 3339111ff2
31 changed files with 4635 additions and 0 deletions

15
.cargo/config.toml Normal file
View File

@@ -0,0 +1,15 @@
[env]
PKG_CONFIG_ALLOW_CROSS = "1"
PKG_CONFIG_PATH = { value = ".cargo/pkgconfig", relative = true }
[target.wasm32-unknown-emscripten]
rustflags = [
"-C",
"link-arg=--no-entry",
"-C",
"link-arg=-sSTANDALONE_WASM=1",
"-C",
"link-arg=-sALLOW_MEMORY_GROWTH=1",
"-C",
"link-arg=-sINITIAL_MEMORY=268435456",
]

View File

@@ -0,0 +1,10 @@
prefix=${pcfiledir}/../../../unicorn
exec_prefix=${prefix}
libdir=${exec_prefix}/build
includedir=${exec_prefix}/include
Name: unicorn
Description: Unicorn engine static library (wasm build)
Version: 2.1.1
Libs: -L${libdir} -lunicorn -lunicorn-common -laarch64-softmmu -larm-softmmu
Cflags: -I${includedir}

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
target/
test/

27
Cargo.toml Normal file
View File

@@ -0,0 +1,27 @@
[package]
name = "anisette-rs"
version = "0.1.0"
edition = "2024"
build = "build.rs"
[lib]
crate-type = ["cdylib", "rlib", "staticlib"]
[[example]]
name = "anisette"
path = "example/anisette.rs"
[dependencies]
anyhow = "1.0.100"
base64 = "0.22.1"
chrono = { version = "0.4.42", default-features = false, features = ["clock"] }
goblin = "0.10.4"
plist = "1.8.0"
rand = "0.8.5"
reqwest = { version = "0.12.24", default-features = false, features = ["blocking", "rustls-tls"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145"
thiserror = "2.0.17"
# unicorn-engine = { version = "=2.1.1", default-features = false, features = ["arch_arm", "arch_aarch64"] }
unicorn-engine = { path = "../unicorn" }
uuid = { version = "1.18.1", features = ["v4"] }

13
README.md Normal file
View File

@@ -0,0 +1,13 @@
# anisette.js
use anisette on browser locally! no more third-party server worries.
## usage
see examples/
should download apple music apk from https://apps.mzstatic.com/content/android-apple-music-apk/applemusic.apk and unzip to get arm64 abi.

62
build.rs Normal file
View File

@@ -0,0 +1,62 @@
use std::env;
use std::path::PathBuf;
fn main() {
println!("cargo:rerun-if-env-changed=UNICORN_DIR");
println!("cargo:rerun-if-env-changed=UNICORN_BUILD_DIR");
println!("cargo:rerun-if-env-changed=UNICORN_INCLUDE_DIR");
let target = env::var("TARGET").unwrap_or_default();
if target != "wasm32-unknown-emscripten" {
return;
}
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap_or_default());
let unicorn_dir = env::var("UNICORN_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| manifest_dir.join("../unicorn"));
let unicorn_build_dir = env::var("UNICORN_BUILD_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| unicorn_dir.join("build"));
let unicorn_include_dir = env::var("UNICORN_INCLUDE_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| unicorn_dir.join("include"));
let required_libs = [
"libunicorn.a",
"libunicorn-common.a",
"libaarch64-softmmu.a",
"libarm-softmmu.a",
];
for lib in required_libs {
let path = unicorn_build_dir.join(lib);
if !path.exists() {
panic!(
"missing unicorn static library: {}. run `bash test/rebuild-unicorn.sh` first",
path.display()
);
}
}
println!("cargo:rustc-link-arg=--no-entry");
println!("cargo:rustc-link-arg=-sSTANDALONE_WASM=1");
println!(
"cargo:rustc-link-search=native={}",
unicorn_build_dir.display()
);
if !unicorn_include_dir.exists() {
println!(
"cargo:warning=unicorn include dir not found at {}",
unicorn_include_dir.display()
);
}
for lib in [
"unicorn",
"unicorn-common",
"aarch64-softmmu",
"arm-softmmu",
] {
println!("cargo:rustc-link-lib=static={lib}");
}
}

143
bun.lock Normal file
View File

@@ -0,0 +1,143 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"dependencies": {
"@bjorn3/browser_wasi_shim": "^0.4.2",
},
"devDependencies": {
"vite": "^7.3.1",
},
},
},
"packages": {
"@bjorn3/browser_wasi_shim": ["@bjorn3/browser_wasi_shim@0.4.2", "", {}, "sha512-/iHkCVUG3VbcbmEHn5iIUpIrh7a7WPiwZ3sHy4HZKZzBdSadwdddYDZAII2zBvQYV0Lfi8naZngPCN7WPHI/hA=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="],
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="],
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="],
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="],
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="],
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="],
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
"rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
}
}

89
example/anisette.rs Normal file
View File

@@ -0,0 +1,89 @@
use std::fs;
use std::path::{Path, PathBuf};
use anisette_rs::{Adi, AdiInit, Device, ProvisioningSession, init_idbfs_for_path, sync_idbfs};
use anyhow::{Context, Result};
use base64::{Engine as _, engine::general_purpose::STANDARD};
use serde_json::json;
fn main() -> Result<()> {
// Usage:
// cargo run --example anisette -- <libstoreservicescore.so> <libCoreADI.so> <library_path> [dsid] [apple_root_pem]
let storeservices_path = std::env::args()
.nth(1)
.unwrap_or_else(|| "libstoreservicescore.so".to_string());
let coreadi_path = std::env::args()
.nth(2)
.unwrap_or_else(|| "libCoreADI.so".to_string());
let library_path = std::env::args()
.nth(3)
.unwrap_or_else(|| "./anisette/".to_string());
let dsid_raw = std::env::args().nth(4).unwrap_or_else(|| "-2".to_string());
let apple_root_pem = std::env::args().nth(5).map(PathBuf::from);
let _mount_path = init_idbfs_for_path(&library_path)
.map_err(|e| anyhow::anyhow!("failed to initialize IDBFS: {e}"))?;
fs::create_dir_all(&library_path)
.with_context(|| format!("failed to create library path: {library_path}"))?;
let device_path = Path::new(&library_path).join("device.json");
let mut device = Device::load(&device_path)?;
let storeservicescore = fs::read(&storeservices_path)
.with_context(|| format!("failed to read {storeservices_path}"))?;
let coreadi =
fs::read(&coreadi_path).with_context(|| format!("failed to read {coreadi_path}"))?;
let mut adi = Adi::new(AdiInit {
storeservicescore,
coreadi,
library_path: library_path.clone(),
provisioning_path: Some(library_path.clone()),
identifier: None,
})?;
if !device.initialized {
println!("Initializing device");
device.initialize_defaults();
device.persist()?;
} else {
println!(
"(Device initialized: server-description='{}' device-uid='{}' adi='{}' user-uid='{}')",
device.data.server_friendly_description,
device.data.unique_device_identifier,
device.data.adi_identifier,
device.data.local_user_uuid
);
}
adi.set_identifier(&device.data.adi_identifier)?;
let dsid = if let Some(hex) = dsid_raw.strip_prefix("0x") {
u64::from_str_radix(hex, 16)?
} else {
let signed: i64 = dsid_raw.parse()?;
signed as u64
};
let is_provisioned = adi.is_machine_provisioned(dsid)?;
if !is_provisioned {
println!("Provisioning...");
let mut provisioning_session =
ProvisioningSession::new(&mut adi, &device.data, apple_root_pem)?;
provisioning_session.provision(dsid)?;
} else {
println!("(Already provisioned)");
}
let otp = adi.request_otp(dsid)?;
let headers = json!({
"X-Apple-I-MD": STANDARD.encode(otp.otp),
"X-Apple-I-MD-M": STANDARD.encode(otp.machine_id),
});
let _ = sync_idbfs(false);
println!("{}", serde_json::to_string_pretty(&headers)?);
Ok(())
}

790
example/browser-run.js Normal file
View File

@@ -0,0 +1,790 @@
// Set up Module configuration before loading libcurl
if (typeof window !== 'undefined') {
window.Module = window.Module || {};
window.Module.preRun = window.Module.preRun || [];
window.Module.preRun.push(function() {
// Get the default CA certs from libcurl (will be available after WASM loads)
// We'll create the extended CA file here
console.log('[preRun] Setting up extended CA certificates');
});
}
import { libcurl } from './libcurl.mjs';
const DEFAULT_CONFIG = {
glueUrl: './anisette/anisette_rs.js',
storeservicesUrl: './arm64-v8a/libstoreservicescore.so',
coreadiUrl: './arm64-v8a/libCoreADI.so',
libraryPath: './anisette/',
provisioningPath: './anisette/',
identifier: '',
dsid: '-2',
assetVersion: '',
rustBacktrace: 'full',
rustLibBacktrace: '1',
};
const state = {
module: null,
storeservicesBytes: null,
coreadiBytes: null,
};
const CONFIG = loadConfig();
const logEl = document.getElementById('log');
function log(message) {
const line = `${message}`;
console.log(line);
logEl.textContent += `${line}\n`;
}
function loadConfig() {
const cfg = { ...DEFAULT_CONFIG };
const params = new URLSearchParams(window.location.search);
for (const key of Object.keys(cfg)) {
const value = params.get(key);
if (value !== null) {
cfg[key] = value;
}
}
if (!cfg.assetVersion) {
cfg.assetVersion = String(Date.now());
}
return cfg;
}
function assert(condition, message) {
if (!condition) {
throw new Error(message);
}
}
function normalizeMountPath(path) {
const trimmed = (path || '').trim();
if (!trimmed || trimmed === '/' || trimmed === './' || trimmed === '.') {
return '/';
}
const noTrailing = trimmed.replace(/\/+$/, '');
const noDot = noTrailing.startsWith('./') ? noTrailing.slice(1) : noTrailing;
if (noDot.startsWith('/')) {
return noDot;
}
return `/${noDot}`;
}
function bytesToBase64(bytes) {
let s = '';
for (let i = 0; i < bytes.length; i += 1) {
s += String.fromCharCode(bytes[i]);
}
return btoa(s);
}
function base64ToBytes(text) {
const clean = (text || '').trim();
if (!clean) {
return new Uint8Array();
}
const s = atob(clean);
const out = new Uint8Array(s.length);
for (let i = 0; i < s.length; i += 1) {
out[i] = s.charCodeAt(i);
}
return out;
}
function dsidToU64(value) {
return BigInt.asUintN(64, BigInt(value.trim()));
}
async function fetchBytes(url, label) {
const res = await fetch(url);
assert(res.ok, `${label} fetch failed: HTTP ${res.status} (${url})`);
return new Uint8Array(await res.arrayBuffer());
}
function ensureExport(name) {
const fn = state.module[name];
if (typeof fn !== 'function') {
throw new Error(`missing export ${name}`);
}
return fn;
}
function allocBytes(bytes) {
if (bytes.length === 0) {
return 0;
}
const malloc = ensureExport('_malloc');
const ptr = Number(malloc(bytes.length));
state.module.HEAPU8.set(bytes, ptr);
return ptr;
}
function allocCString(text) {
const value = text || '';
const size = state.module.lengthBytesUTF8(value) + 1;
const malloc = ensureExport('_malloc');
const ptr = Number(malloc(size));
state.module.stringToUTF8(value, ptr, size);
return ptr;
}
function readBytes(ptr, len) {
return state.module.HEAPU8.slice(Number(ptr), Number(ptr) + Number(len));
}
function readRustError() {
const getPtr = ensureExport('_anisette_last_error_ptr');
const getLen = ensureExport('_anisette_last_error_len');
const ptr = Number(getPtr());
const len = Number(getLen());
if (len === 0) {
return '';
}
return new TextDecoder().decode(readBytes(ptr, len));
}
function call(name, fn) {
let ret;
try {
ret = fn();
} catch (e) {
log(`${name}: trap=${String(e)}`);
throw e;
}
const err = readRustError();
log(`${name}: ret=${ret}${err ? ` err=${err}` : ''}`);
if (ret < 0) {
throw new Error(`${name} failed: ${err || `ret=${ret}`}`);
}
return { ret, err };
}
function resolveWasmUrl(jsUrl) {
const url = new URL(jsUrl, window.location.origin);
if (!url.pathname.endsWith('.js')) {
throw new Error(`invalid glue path (expect .js): ${url.href}`);
}
url.pathname = url.pathname.slice(0, -3) + '.wasm';
return url.href;
}
async function initModule() {
log(`config: ${JSON.stringify(CONFIG)}`);
state.storeservicesBytes = await fetchBytes(CONFIG.storeservicesUrl, 'libstoreservicescore.so');
state.coreadiBytes = await fetchBytes(CONFIG.coreadiUrl, 'libCoreADI.so');
const moduleUrl = new URL(CONFIG.glueUrl, window.location.origin);
moduleUrl.searchParams.set('v', CONFIG.assetVersion);
const createModule = (await import(moduleUrl.href)).default;
const wasmUrl = resolveWasmUrl(moduleUrl.href);
log(`glue_url=${moduleUrl.href}`);
log(`wasm_url=${wasmUrl}`);
const wasmRes = await fetch(wasmUrl, { cache: 'no-store' });
assert(wasmRes.ok, `wasm fetch failed: HTTP ${wasmRes.status} (${wasmUrl})`);
const wasmBinary = new Uint8Array(await wasmRes.arrayBuffer());
assert(wasmBinary.length >= 8, `wasm too small: ${wasmBinary.length} bytes`);
const magicOk =
wasmBinary[0] === 0x00 &&
wasmBinary[1] === 0x61 &&
wasmBinary[2] === 0x73 &&
wasmBinary[3] === 0x6d;
if (!magicOk) {
const head = Array.from(wasmBinary.slice(0, 8))
.map((b) => b.toString(16).padStart(2, '0'))
.join(' ');
throw new Error(`invalid wasm magic at ${wasmUrl}, first8=${head}`);
}
state.module = await createModule({
noInitialRun: true,
wasmBinary,
ENV: {
RUST_BACKTRACE: CONFIG.rustBacktrace,
RUST_LIB_BACKTRACE: CONFIG.rustLibBacktrace,
},
print: (msg) => log(`${msg}`),
printErr: (msg) => log(`${msg}`),
locateFile: (path) => {
if (path.includes('.wasm')) {
return wasmUrl;
}
return path;
},
});
log('emscripten module instantiated');
}
async function syncIdbfs(populate) {
const FS = state.module.FS;
await new Promise((resolve, reject) => {
FS.syncfs(populate, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
async function initIdbfs() {
const FS = state.module.FS;
const IDBFS = FS.filesystems?.IDBFS;
const mountPath = normalizeMountPath(CONFIG.libraryPath);
if (!IDBFS) {
throw new Error('IDBFS unavailable on FS.filesystems');
}
if (mountPath !== '/') {
try {
FS.mkdirTree(mountPath);
} catch (_) {
// ignore existing path
}
}
try {
FS.mount(IDBFS, {}, mountPath);
} catch (_) {
// ignore already mounted
}
await syncIdbfs(true);
log(`idbfs mounted: ${mountPath}`);
}
async function persistIdbfs() {
await syncIdbfs(false);
log('idbfs sync: flushed');
}
// ===== HTTP Request helpers using libcurl =====
const USER_AGENT = 'akd/1.0 CFNetwork/1404.0.5 Darwin/22.3.0';
async function httpGet(url, extraHeaders = {}) {
log(`GET ${url}`);
const headers = {
'User-Agent': USER_AGENT,
...extraHeaders,
};
const resp = await libcurl.fetch(url, {
method: 'GET',
headers,
redirect: 'manual',
_libcurl_http_version: 1.1,
insecure: true,
});
const body = await resp.text();
log(`GET ${url} -> ${resp.status}`);
return { status: resp.status, body };
}
async function httpPost(url, data, extraHeaders = {}) {
log(`POST ${url}`);
const headers = {
'User-Agent': USER_AGENT,
'Content-Type': 'application/x-www-form-urlencoded',
'Connection': 'keep-alive',
...extraHeaders,
};
const resp = await libcurl.fetch(url, {
method: 'POST',
headers,
body: data,
redirect: 'manual',
insecure: true,
_libcurl_http_version: 1.1,
});
const body = await resp.text();
log(`POST ${url} -> ${resp.status}`);
return { status: resp.status, body };
}
// Simple plist parsing for the specific format we need
function parsePlist(xmlText) {
const parser = new DOMParser();
const doc = parser.parseFromString(xmlText, 'text/xml');
function parseNode(node) {
if (!node) return null;
const tag = node.tagName;
if (tag === 'dict') {
const result = {};
let key = null;
for (const child of node.children) {
if (child.tagName === 'key') {
key = child.textContent;
} else if (key !== null) {
result[key] = parseNode(child);
key = null;
}
}
return result;
} else if (tag === 'array') {
return Array.from(node.children).map(parseNode);
} else if (tag === 'string') {
return node.textContent || '';
} else if (tag === 'integer') {
return parseInt(node.textContent, 10);
} else if (tag === 'true') {
return true;
} else if (tag === 'false') {
return false;
} else if (tag === 'data') {
// base64 encoded data
const text = node.textContent || '';
return text.replace(/\s/g, '');
}
return null;
}
const plist = doc.querySelector('plist > dict, plist > array');
return parseNode(plist);
}
// ===== Device Management =====
class Device {
constructor() {
this.uniqueDeviceIdentifier = '';
this.serverFriendlyDescription = '';
this.adiIdentifier = '';
this.localUserUuid = '';
this.initialized = false;
}
generate() {
// Generate UUID
this.uniqueDeviceIdentifier = crypto.randomUUID().toUpperCase();
// Pretend to be a MacBook Pro like in index.py
this.serverFriendlyDescription = '<MacBookPro13,2> <macOS;13.1;22C65> <com.apple.AuthKit/1 (com.apple.dt.Xcode/3594.4.19)>';
// Generate 16 hex chars (8 bytes) for ADI identifier
const bytes = new Uint8Array(8);
crypto.getRandomValues(bytes);
this.adiIdentifier = Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
// Generate 64 hex chars (32 bytes) for local user UUID
const luBytes = new Uint8Array(32);
crypto.getRandomValues(luBytes);
this.localUserUuid = Array.from(luBytes, b => b.toString(16).toUpperCase().padStart(2, '0')).join('');
this.initialized = true;
log(`Device generated: UDID=${this.uniqueDeviceIdentifier}, ADI=${this.adiIdentifier}`);
}
}
function deviceFilePath() {
const mountPath = normalizeMountPath(CONFIG.libraryPath);
if (mountPath === '/') {
return '/device.json';
}
return `${mountPath}/device.json`;
}
function parseDeviceRecord(record) {
if (!record || typeof record !== 'object') {
return null;
}
const device = new Device();
device.uniqueDeviceIdentifier = String(record.UUID || '');
device.serverFriendlyDescription = String(record.clientInfo || '');
device.adiIdentifier = String(record.identifier || '');
device.localUserUuid = String(record.localUUID || '');
device.initialized = Boolean(
device.uniqueDeviceIdentifier &&
device.serverFriendlyDescription &&
device.adiIdentifier &&
device.localUserUuid,
);
return device;
}
function serializeDevice(device) {
return {
UUID: device.uniqueDeviceIdentifier,
clientInfo: device.serverFriendlyDescription,
identifier: device.adiIdentifier,
localUUID: device.localUserUuid,
};
}
function readDeviceFromFs() {
const FS = state.module.FS;
const path = deviceFilePath();
try {
const text = FS.readFile(path, { encoding: 'utf8' });
const parsed = JSON.parse(text);
const device = parseDeviceRecord(parsed);
if (!device || !device.initialized) {
return null;
}
log(`Device loaded: UDID=${device.uniqueDeviceIdentifier}, ADI=${device.adiIdentifier}`);
return device;
} catch (e) {
return null;
}
}
function persistDevice(device) {
const FS = state.module.FS;
const path = deviceFilePath();
const text = JSON.stringify(serializeDevice(device), null, 2);
FS.writeFile(path, text);
log(`Device persisted: ${path}`);
}
function loadOrCreateDevice() {
let device = readDeviceFromFs();
let shouldPersist = false;
if (!device) {
device = new Device();
device.generate();
shouldPersist = true;
}
const override = (CONFIG.identifier || '').trim();
if (override && override !== device.adiIdentifier) {
device.adiIdentifier = override;
device.initialized = true;
shouldPersist = true;
log(`Device identifier overridden: ${override}`);
}
if (shouldPersist) {
persistDevice(device);
}
return device;
}
// ===== Provisioning Session =====
class ProvisioningSession {
constructor(device) {
this.device = device;
this.urlBag = {};
}
getBaseHeaders() {
return {
'X-Mme-Device-Id': this.device.uniqueDeviceIdentifier,
'X-MMe-Client-Info': this.device.serverFriendlyDescription,
'X-Apple-I-MD-LU': this.device.localUserUuid,
'X-Apple-Client-App-Name': 'Setup',
};
}
getClientTime() {
// ISO format without milliseconds, like Python's isoformat()
return new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
}
async loadUrlBag() {
if (Object.keys(this.urlBag).length > 0) {
return;
}
const url = 'https://gsa.apple.com/grandslam/GsService2/lookup';
const { body } = await httpGet(url, this.getBaseHeaders());
const plist = parsePlist(body);
if (plist && plist.urls) {
this.urlBag = plist.urls;
log(`URL bag loaded: ${Object.keys(this.urlBag).join(', ')}`);
} else {
throw new Error('Failed to parse URL bag');
}
}
async provision(dsId) {
log('Starting provisioning...');
await this.loadUrlBag();
// Step 1: Start provisioning - get spim from Apple
const startProvisioningPlist = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Header</key>
<dict/>
<key>Request</key>
<dict/>
</dict>
</plist>`;
const extraHeadersStart = {
...this.getBaseHeaders(),
'X-Apple-I-Client-Time': this.getClientTime(),
};
const { body: startBody } = await httpPost(
this.urlBag.midStartProvisioning,
startProvisioningPlist,
extraHeadersStart
);
const spimPlist = parsePlist(startBody);
const spimStr = spimPlist?.Response?.spim;
if (!spimStr) {
throw new Error('Failed to get spim from start provisioning');
}
const spim = base64ToBytes(spimStr);
log(`Got spim: ${spim.length} bytes`);
// Step 2: Call ADI start_provisioning
const cpim = await this.adiStartProvisioning(dsId, spim);
log(`Got cpim: ${cpim.length} bytes`);
// Step 3: End provisioning - send cpim to Apple
const endProvisioningPlist = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Header</key>
<dict/>
<key>Request</key>
<dict>
<key>cpim</key>
<string>${bytesToBase64(cpim)}</string>
</dict>
</dict>
</plist>`;
const extraHeadersEnd = {
...this.getBaseHeaders(),
'X-Apple-I-Client-Time': this.getClientTime(),
};
const { body: endBody } = await httpPost(
this.urlBag.midFinishProvisioning,
endProvisioningPlist,
extraHeadersEnd
);
const endPlist = parsePlist(endBody);
const response = endPlist?.Response;
if (!response) {
throw new Error('Failed to get response from end provisioning');
}
const ptm = base64ToBytes(response.ptm);
const tk = base64ToBytes(response.tk);
log(`Got ptm: ${ptm.length} bytes, tk: ${tk.length} bytes`);
// Step 4: Call ADI end_provisioning
await this.adiEndProvisioning(ptm, tk);
log('Provisioning completed successfully');
}
async adiStartProvisioning(dsId, spim) {
const pSpim = allocBytes(spim);
const startFn = ensureExport('_anisette_start_provisioning');
const start = call('anisette_start_provisioning', () =>
Number(startFn(dsId, pSpim, spim.length)),
);
if (start.ret !== 0) {
throw new Error(`start_provisioning failed: ${start.err || 'unknown error'}`);
}
const getCpimPtr = ensureExport('_anisette_get_cpim_ptr');
const getCpimLen = ensureExport('_anisette_get_cpim_len');
const getSession = ensureExport('_anisette_get_session');
const cpimPtr = Number(getCpimPtr());
const cpimLen = Number(getCpimLen());
state.session = Number(getSession());
return readBytes(cpimPtr, cpimLen);
}
async adiEndProvisioning(ptm, tk) {
const pPtm = allocBytes(ptm);
const pTk = allocBytes(tk);
const endFn = ensureExport('_anisette_end_provisioning');
const end = call('anisette_end_provisioning', () =>
Number(endFn(state.session, pPtm, ptm.length, pTk, tk.length)),
);
if (end.ret !== 0) {
throw new Error(`end_provisioning failed: ${end.err || 'unknown error'}`);
}
}
}
// ===== Main Flow =====
async function initAnisette(identifier) {
const pStores = allocBytes(state.storeservicesBytes);
const pCore = allocBytes(state.coreadiBytes);
const pLibrary = allocCString(CONFIG.libraryPath);
const pProvisioning = allocCString(CONFIG.provisioningPath);
const pIdentifier = allocCString(identifier);
const initFromBlobs = ensureExport('_anisette_init_from_blobs');
const init = call('anisette_init_from_blobs', () =>
Number(
initFromBlobs(
pStores,
state.storeservicesBytes.length,
pCore,
state.coreadiBytes.length,
pLibrary,
pProvisioning,
pIdentifier,
),
),
);
if (init.ret !== 0) {
throw new Error('init failed');
}
}
async function isMachineProvisioned(dsId) {
const isProvisionedFn = ensureExport('_anisette_is_machine_provisioned');
const provisioned = call('anisette_is_machine_provisioned', () =>
Number(isProvisionedFn(dsId)),
);
return provisioned.ret !== 0; // ret === 0 means provisioned (no error)
}
async function requestOtp(dsId) {
const otpFn = ensureExport('_anisette_request_otp');
const otp = call('anisette_request_otp', () => Number(otpFn(dsId)));
if (otp.ret !== 0) {
throw new Error(`request_otp failed: ${otp.err || 'unknown error'}`);
}
const getOtpPtr = ensureExport('_anisette_get_otp_ptr');
const getOtpLen = ensureExport('_anisette_get_otp_len');
const getMidPtr = ensureExport('_anisette_get_mid_ptr');
const getMidLen = ensureExport('_anisette_get_mid_len');
const otpPtr = Number(getOtpPtr());
const otpLen = Number(getOtpLen());
const midPtr = Number(getMidPtr());
const midLen = Number(getMidLen());
const otpBytes = readBytes(otpPtr, otpLen);
const midBytes = readBytes(midPtr, midLen);
return {
oneTimePassword: otpBytes,
machineIdentifier: midBytes,
};
}
async function initLibcurl() {
log('initializing libcurl...');
const wsProto = location.protocol === 'https:' ? 'wss:' : 'ws:';
libcurl.set_websocket(`${wsProto}//${location.host}/wisp/`);
// Capture libcurl verbose output
libcurl.stderr = (text) => {
log(`[libcurl] ${text}`);
};
await libcurl.load_wasm('./libcurl.wasm');
// // Get default CA certs and append Apple CA certs
// const defaultCacert = libcurl.get_cacert();
// const extendedCacert = defaultCacert + '\n' + APPLE_CA_CERTS;
// Create a file with extended CA certs in the Emscripten FS
log('libcurl initialized');
}
function dumpFs(path = '/') {
const FS = state.module.FS;
const entries = FS.readdir(path).filter((name) => name !== '.' && name !== '..');
for (const name of entries) {
const full = path === '/' ? `/${name}` : `${path}/${name}`;
const stat = FS.stat(full);
if (FS.isDir(stat.mode)) {
console.log(`dir ${full}`);
dumpFs(full);
} else {
const data = FS.readFile(full);
console.log(`file ${full} size=${data.length}`);
// 如果要看内容(文本)
// console.log(new TextDecoder().decode(data));
// base64
function bytesToHex(bytes) {
return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join(' ');
}
console.log(bytesToBase64(data))
}
}
}
async function main() {
try {
// Initialize libcurl for HTTP requests
await initLibcurl();
// Load WASM module
await initModule();
// Initialize IDBFS
await initIdbfs();
// Load device info or generate new one
const device = loadOrCreateDevice();
// Initialize anisette with device identifier
await initAnisette(device.adiIdentifier);
const dsid = dsidToU64(CONFIG.dsid);
// Check if machine is provisioned
const isProvisioned = await isMachineProvisioned(dsid);
if (!isProvisioned) {
log('Machine not provisioned, starting provisioning...');
const session = new ProvisioningSession(device);
await session.provision(dsid);
} else {
log('Machine already provisioned');
}
// Request OTP
log('Requesting OTP...');
const otp = await requestOtp(dsid);
// Output the result
const result = {
'X-Apple-I-MD': bytesToBase64(otp.oneTimePassword),
'X-Apple-I-MD-M': bytesToBase64(otp.machineIdentifier),
};
log(`OTP result: ${JSON.stringify(result, null, 2)}`);
// Persist IDBFS
await persistIdbfs();
// dumpFs("/anisette/");
log('done');
} catch (e) {
log(`fatal: ${String(e)}`);
console.error(e);
}
}
main();

260
example/run-node.mjs Normal file
View File

@@ -0,0 +1,260 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const distDir = path.join(__dirname, 'dist');
const modulePath = path.join(distDir, 'anisette_rs.node.js');
const wasmPath = path.join(distDir, 'anisette_rs.node.wasm');
function usage() {
console.log('usage: bun test/run-node.mjs <libstoreservicescore.so> <libCoreADI.so> [library_path] [dsid] [identifier] [trace_window_start]');
console.log('note: library_path should contain adi.pb/device.json when available');
}
function allocBytes(module, bytes) {
const ptr = module._malloc(bytes.length);
module.HEAPU8.set(bytes, ptr);
return ptr;
}
function allocCString(module, value) {
if (!value) {
return 0;
}
const size = module.lengthBytesUTF8(value) + 1;
const ptr = module._malloc(size);
module.stringToUTF8(value, ptr, size);
return ptr;
}
function readLastError(module) {
const ptr = module._anisette_last_error_ptr();
const len = module._anisette_last_error_len();
if (!ptr || !len) {
return '';
}
const bytes = module.HEAPU8.subarray(ptr, ptr + len);
return new TextDecoder('utf-8').decode(bytes);
}
function normalizeLibraryRoot(input) {
const trimmed = input.trim();
if (!trimmed) {
return '.';
}
const normalized = trimmed.replace(/\/+$/, '');
return normalized || '.';
}
function ensureTrailingSlash(input) {
if (!input) {
return './';
}
return input.endsWith('/') ? input : `${input}/`;
}
function joinLibraryFile(root, fileName) {
if (root === '/') {
return `/${fileName}`;
}
if (root.endsWith('/')) {
return `${root}${fileName}`;
}
return `${root}/${fileName}`;
}
function writeVirtualFile(module, filePath, buffer) {
const pathPtr = allocCString(module, filePath);
const dataPtr = allocBytes(module, buffer);
const result = module._anisette_fs_write_file(pathPtr, dataPtr, buffer.length);
module._free(pathPtr);
module._free(dataPtr);
if (result !== 0) {
const message = readLastError(module);
throw new Error(message || 'virtual fs write failed');
}
}
function readBytes(module, ptr, len) {
if (!ptr || !len) {
return new Uint8Array();
}
return module.HEAPU8.slice(ptr, ptr + len);
}
function toBase64(bytes) {
if (!bytes.length) {
return '';
}
return Buffer.from(bytes).toString('base64');
}
function toAppleClientTime(date = new Date()) {
return date.toISOString().replace(/\.\d{3}Z$/, 'Z');
}
function detectAppleLocale() {
const locale = Intl.DateTimeFormat().resolvedOptions().locale || 'en-US';
return locale.replace('-', '_');
}
async function fileExists(filePath) {
try {
await fs.access(filePath);
return true;
} catch {
return false;
}
}
const args = process.argv.slice(2);
const defaultStoreservicesPath = path.join(__dirname, 'arm64-v8a', 'libstoreservicescore.so');
const defaultCoreadiPath = path.join(__dirname, 'arm64-v8a', 'libCoreADI.so');
const storeservicesPath = args[0] ?? defaultStoreservicesPath;
const coreadiPath = args[1] ?? defaultCoreadiPath;
const libraryPath = args[2] ?? './anisette/';
const dsidRaw = args[3] ?? '-2';
let identifier = args[4] ?? '';
const traceWindowStartRaw = args[5] ?? '0';
const silent = process.env.ANISETTE_SILENT === '1';
if (!(await fileExists(storeservicesPath)) || !(await fileExists(coreadiPath))) {
usage();
process.exit(1);
}
const libraryRoot = normalizeLibraryRoot(libraryPath);
const libraryArg = ensureTrailingSlash(libraryRoot);
const resolvedLibraryPath = path.resolve(libraryRoot);
const devicePath = path.join(resolvedLibraryPath, 'device.json');
const adiPath = path.join(resolvedLibraryPath, 'adi.pb');
let deviceData = null;
if (await fileExists(devicePath)) {
try {
deviceData = JSON.parse(await fs.readFile(devicePath, 'utf8'));
} catch {}
}
if (!identifier && deviceData) {
try {
if (deviceData && typeof deviceData.identifier === 'string' && deviceData.identifier) {
identifier = deviceData.identifier;
}
} catch {}
}
const moduleFactory = (await import(pathToFileURL(modulePath).href)).default;
const module = await moduleFactory({
locateFile(file) {
if (file.endsWith('.wasm')) {
return wasmPath;
}
return file;
}
});
const storeservices = await fs.readFile(storeservicesPath);
const coreadi = await fs.readFile(coreadiPath);
if (await fileExists(adiPath)) {
const adiData = await fs.readFile(adiPath);
try {
writeVirtualFile(module, joinLibraryFile(libraryRoot, 'adi.pb'), adiData);
} catch (err) {
console.error('anisette_fs_write_file failed:', err.message || err);
process.exit(1);
}
}
if (await fileExists(devicePath)) {
const deviceData = await fs.readFile(devicePath);
try {
writeVirtualFile(module, joinLibraryFile(libraryRoot, 'device.json'), deviceData);
} catch (err) {
console.error('anisette_fs_write_file failed:', err.message || err);
process.exit(1);
}
}
const storeservicesPtr = allocBytes(module, storeservices);
const coreadiPtr = allocBytes(module, coreadi);
const libraryPtr = allocCString(module, libraryArg);
const provisioningPtr = allocCString(module, libraryArg);
const identifierPtr = allocCString(module, identifier);
const initResult = module._anisette_init_from_blobs(
storeservicesPtr,
storeservices.length,
coreadiPtr,
coreadi.length,
libraryPtr,
provisioningPtr,
identifierPtr
);
module._free(storeservicesPtr);
module._free(coreadiPtr);
if (libraryPtr) {
module._free(libraryPtr);
}
if (provisioningPtr) {
module._free(provisioningPtr);
}
if (identifierPtr) {
module._free(identifierPtr);
}
if (initResult !== 0) {
console.error('anisette_init_from_blobs failed:', readLastError(module));
process.exit(1);
}
const traceWindowStart = BigInt(traceWindowStartRaw);
if (traceWindowStart > 0n && typeof module._anisette_set_trace_window_start === 'function') {
const traceResult = module._anisette_set_trace_window_start(traceWindowStart);
if (traceResult !== 0) {
console.error('anisette_set_trace_window_start failed:', readLastError(module));
process.exit(1);
}
}
const dsid = BigInt(dsidRaw);
const provisioned = module._anisette_is_machine_provisioned(dsid);
if (provisioned < 0) {
console.error('anisette_is_machine_provisioned failed:', readLastError(module));
process.exit(1);
}
if (provisioned !== 1 && !silent) {
console.warn('device not provisioned, request_otp may fail');
}
const otpResult = module._anisette_request_otp(dsid);
if (otpResult !== 0) {
console.error('anisette_request_otp failed:', readLastError(module));
process.exit(1);
}
const otpBytes = readBytes(module, module._anisette_get_otp_ptr(), module._anisette_get_otp_len());
const midBytes = readBytes(module, module._anisette_get_mid_ptr(), module._anisette_get_mid_len());
const localUserUuid = (deviceData && typeof deviceData.localUUID === 'string') ? deviceData.localUUID : '';
const mdLu = process.env.ANISETTE_MD_LU_BASE64 === '1'
? Buffer.from(localUserUuid, 'utf8').toString('base64')
: localUserUuid;
const headers = {
'X-Apple-I-Client-Time': toAppleClientTime(),
'X-Apple-I-MD': toBase64(otpBytes),
'X-Apple-I-MD-LU': mdLu,
'X-Apple-I-MD-M': toBase64(midBytes),
'X-Apple-I-MD-RINFO': process.env.ANISETTE_MD_RINFO ?? '17106176',
'X-Apple-I-SRL-NO': process.env.ANISETTE_SRL_NO ?? '0',
'X-Apple-I-TimeZone': process.env.ANISETTE_TIMEZONE ?? 'UTC',
'X-Apple-Locale': process.env.ANISETTE_LOCALE ?? detectAppleLocale(),
'X-MMe-Client-Info': (deviceData && typeof deviceData.clientInfo === 'string') ? deviceData.clientInfo : '',
'X-Mme-Device-Id': (deviceData && typeof deviceData.UUID === 'string') ? deviceData.UUID : ''
};
if (!silent) {
console.log(JSON.stringify(headers, null, 2));
}

9
package.json Normal file
View File

@@ -0,0 +1,9 @@
{
"private": true,
"scripts": {
"build:unicorn": "bash script/rebuild-unicorn.sh",
"build:glue": "bash script/build-glue.sh",
"build:release": "bash script/build-glue.sh --release",
"run:node": "bun test/run-node.mjs"
}
}

99
script/build-glue.sh Executable file
View File

@@ -0,0 +1,99 @@
#!/usr/bin/env bash
set -euo pipefail
BUILD_MODE="debug"
if [[ "${1:-}" == "--release" ]]; then
BUILD_MODE="release"
fi
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TARGET_DIR="${ROOT_DIR}/../target/wasm32-unknown-emscripten/${BUILD_MODE}"
DIST_DIR="${ROOT_DIR}/test/dist"
EMSDK_DIR="${EMSDK:-/Users/libr/Desktop/Life/emsdk}"
UNICORN_BUILD_DIR="${UNICORN_BUILD_DIR:-${ROOT_DIR}/../unicorn/build}"
NODE_DIST_JS="${DIST_DIR}/anisette_rs.node.js"
NODE_DIST_WASM="${DIST_DIR}/anisette_rs.node.wasm"
WEB_EXPORTED_FUNCTIONS='["_malloc","_free","_anisette_init_from_blobs","_anisette_is_machine_provisioned","_anisette_start_provisioning","_anisette_end_provisioning","_anisette_request_otp","_anisette_get_cpim_ptr","_anisette_get_cpim_len","_anisette_get_session","_anisette_get_otp_ptr","_anisette_get_otp_len","_anisette_get_mid_ptr","_anisette_get_mid_len","_anisette_last_error_ptr","_anisette_last_error_len","_anisette_fs_write_file","_anisette_idbfs_init","_anisette_idbfs_sync","_anisette_set_identifier","_anisette_set_provisioning_path"]'
NODE_EXPORTED_FUNCTIONS='["_malloc","_free","_anisette_init_from_blobs","_anisette_is_machine_provisioned","_anisette_start_provisioning","_anisette_end_provisioning","_anisette_request_otp","_anisette_get_cpim_ptr","_anisette_get_cpim_len","_anisette_get_session","_anisette_get_otp_ptr","_anisette_get_otp_len","_anisette_get_mid_ptr","_anisette_get_mid_len","_anisette_last_error_ptr","_anisette_last_error_len","_anisette_fs_write_file","_anisette_set_identifier","_anisette_set_provisioning_path"]'
WEB_EXPORTED_RUNTIME_METHODS='["FS","HEAPU8","UTF8ToString","stringToUTF8","lengthBytesUTF8"]'
NODE_EXPORTED_RUNTIME_METHODS='["HEAPU8","UTF8ToString","stringToUTF8","lengthBytesUTF8"]'
if [[ -f "${EMSDK_DIR}/emsdk_env.sh" ]]; then
# shellcheck disable=SC1090
source "${EMSDK_DIR}/emsdk_env.sh" >/dev/null
else
echo "emsdk_env.sh not found at ${EMSDK_DIR}/emsdk_env.sh"
exit 1
fi
mkdir -p "${DIST_DIR}"
# if [[ "${SKIP_UNICORN_REBUILD:-0}" != "1" ]]; then
# bash "${ROOT_DIR}/test/rebuild-unicorn.sh"
# fi
pushd "${ROOT_DIR}" >/dev/null
if [[ "${BUILD_MODE}" == "release" ]]; then
cargo build --release --target wasm32-unknown-emscripten
else
cargo build --target wasm32-unknown-emscripten
fi
popd >/dev/null
EMCC_INPUTS=(
"${TARGET_DIR}/libanisette_rs.a"
"${UNICORN_BUILD_DIR}/libunicorn.a"
"${UNICORN_BUILD_DIR}/libunicorn-common.a"
"${UNICORN_BUILD_DIR}/libaarch64-softmmu.a"
"${UNICORN_BUILD_DIR}/libarm-softmmu.a"
)
for f in "${EMCC_INPUTS[@]}"; do
if [[ ! -f "${f}" ]]; then
echo "missing input: ${f}"
exit 1
fi
done
emcc \
"${EMCC_INPUTS[@]}" \
-lidbfs.js \
-o "${DIST_DIR}/anisette_rs.js" \
-sMODULARIZE=1 \
-sEXPORT_ES6=1 \
-sENVIRONMENT=web \
-sWASM=1 \
-sALLOW_MEMORY_GROWTH=1 \
-sINITIAL_MEMORY=268435456 \
-sWASM_BIGINT=1 \
-sFORCE_FILESYSTEM=1 \
-sASSERTIONS=1 \
-sEXPORTED_FUNCTIONS="${WEB_EXPORTED_FUNCTIONS}" \
-sEXPORTED_RUNTIME_METHODS="${WEB_EXPORTED_RUNTIME_METHODS}"
emcc \
"${EMCC_INPUTS[@]}" \
-o "${NODE_DIST_JS}" \
-sMODULARIZE=1 \
-sEXPORT_ES6=1 \
-sENVIRONMENT=node \
-sWASM=1 \
-sALLOW_MEMORY_GROWTH=1 \
-sINITIAL_MEMORY=268435456 \
-sWASM_BIGINT=1 \
-sFORCE_FILESYSTEM=0 \
-sASSERTIONS=1 \
-sEXPORTED_FUNCTIONS="${NODE_EXPORTED_FUNCTIONS}" \
-sEXPORTED_RUNTIME_METHODS="${NODE_EXPORTED_RUNTIME_METHODS}"
echo "glue build done:"
echo " ${DIST_DIR}/anisette_rs.js"
echo " ${DIST_DIR}/anisette_rs.wasm"
echo " ${NODE_DIST_JS}"
echo " ${NODE_DIST_WASM}"
cp "${DIST_DIR}/anisette_rs.js" "${ROOT_DIR}/../../frontend/public/anisette/anisette_rs.js"
cp "${DIST_DIR}/anisette_rs.wasm" "${ROOT_DIR}/../../frontend/public/anisette/anisette_rs.wasm"

View File

@@ -0,0 +1,66 @@
diff --git a/qemu/tcg/ffi.inc.c b/qemu/tcg/ffi.inc.c
index f0300a76..68ea4ebc 100644
--- a/qemu/tcg/ffi.inc.c
+++ b/qemu/tcg/ffi.inc.c
@@ -19,6 +19,7 @@ static int debug_info(TCGHelperInfo *info) {
printf("sizemask: 0x%x\n", info->sizemask);
printf("n_args: %d\n", info->n_args);
printf("t0: %lu\n", (uintptr_t)info->func);
+ return 0;
}
static uint64_t do_op_call(tcg_target_ulong *regs, tcg_target_ulong t0) {
@@ -32,20 +33,50 @@ static uint64_t do_op_call(tcg_target_ulong *regs, tcg_target_ulong t0) {
// Manual ABI interventions (wasm32 requires very specific conventions for uint64_t)
#if TCG_TARGET_REG_BITS == 32
- if (info->flags & dh_callflag_void && info->sizemask == 0x10 && info->n_args == 4) {
- ((void (*)(uint32_t, uint64_t, uint32_t, uint32_t))t0)(tci_read_reg(regs, TCG_REG_R0), tci_read_reg_ext(regs, TCG_REG_R1), tci_read_reg(regs, TCG_REG_R3), tci_read_reg(regs, TCG_REG_R4));
+ if (info->name && strcmp(info->name, "uc_tracecode") == 0) {
+ uint64_t trace_addr = tci_read_reg_ext(regs, TCG_REG_R3);
+ // printf("ffi wasm32 fastpath: uc_tracecode r0=%u r1=%u r2=%lu addr=0x%llx\n",
+ // (unsigned)tci_read_reg(regs, TCG_REG_R0),
+ // (unsigned)tci_read_reg(regs, TCG_REG_R1),
+ // (unsigned long)tci_read_reg(regs, TCG_REG_R2),
+ // (unsigned long long)trace_addr);
+ ((void (*)(uint32_t, uint32_t, uintptr_t, uint64_t))t0)(
+ tci_read_reg(regs, TCG_REG_R0),
+ tci_read_reg(regs, TCG_REG_R1),
+ (uintptr_t)tci_read_reg(regs, TCG_REG_R2),
+ trace_addr
+ );
+ return 0;
+ }
+
+ if (info->flags & dh_callflag_void && info->sizemask == 0x10 && info->n_args == 4) {
+ ((void (*)(uint32_t, uint32_t, uint64_t, uint32_t))t0)(
+ tci_read_reg(regs, TCG_REG_R0),
+ tci_read_reg(regs, TCG_REG_R1),
+ tci_read_reg_ext(regs, TCG_REG_R2),
+ tci_read_reg(regs, TCG_REG_R4)
+ );
return 0;
} else if (info->sizemask == 0x255 && info->n_args == 4) {
return ((uint64_t (*)(uint64_t, uint64_t, uint64_t, uint32_t))t0)(tci_read_reg_ext(regs, TCG_REG_R0), tci_read_reg_ext(regs, TCG_REG_R2), tci_read_reg_ext(regs, TCG_REG_R4), tci_read_reg(regs, TCG_REG_R7));
} else if (info->sizemask == 4 && info->n_args == 3) {
return ((uint32_t (*)(uint64_t, uint32_t, uint32_t))t0)(tci_read_reg_ext(regs, TCG_REG_R0), tci_read_reg(regs, TCG_REG_R2), tci_read_reg(regs, TCG_REG_R3));
} else if ((info->sizemask == 0x15 || info->sizemask == 0x3f) && info->n_args == 2) {
- return ((uint64_t (*)(uint64_t, uint64_t))t0)(tci_read_reg_ext(regs, TCG_REG_R0), tci_read_reg(regs, TCG_REG_R2));
+ return ((uint64_t (*)(uint64_t, uint64_t))t0)(tci_read_reg_ext(regs, TCG_REG_R0), tci_read_reg_ext(regs, TCG_REG_R2));
} else if (info->sizemask == 0x40 && info->n_args == 3) {
((void (*)(uintptr_t, uintptr_t, uint64_t))t0)(tci_read_reg(regs, TCG_REG_R0), tci_read_reg(regs, TCG_REG_R1), tci_read_reg_ext(regs, TCG_REG_R2));
return 0;
} else if (info->sizemask == 0x15 && info->n_args == 3) {
return ((uint64_t (*)(uint64_t, uint64_t, uint32_t))t0)(tci_read_reg_ext(regs, TCG_REG_R0), tci_read_reg_ext(regs, TCG_REG_R2), tci_read_reg(regs, TCG_REG_R4));
+ } else if (info->sizemask == 0x4 && info->n_args == 1) {
+ // helper with arg shape (i64) -> i32, e.g., neon_narrow_u16, neon_narrow_high_u8, iwmmxt_setpsr_nz
+ return ((uint32_t (*)(uint64_t))t0)(tci_read_reg_ext(regs, TCG_REG_R0));
+ } else if (info->sizemask == 0x4 && info->n_args == 2) {
+ // helper with arg shape (env, i64) -> i32, e.g., neon_narrow_sat_u8 (env is ptr)
+ return ((uint32_t (*)(uintptr_t, uint64_t))t0)(
+ tci_read_reg(regs, TCG_REG_R0),
+ tci_read_reg_ext(regs, TCG_REG_R1)
+ );
}
for (int i = 1; i < 15; i++) {

191
script/patches/ffi.rs.diff Normal file
View File

@@ -0,0 +1,191 @@
diff --git a/bindings/rust/src/ffi.rs b/bindings/rust/src/ffi.rs
index 7f7a205b..d812e8cd 100644
--- a/bindings/rust/src/ffi.rs
+++ b/bindings/rust/src/ffi.rs
@@ -121,9 +121,15 @@ pub unsafe extern "C" fn mmio_read_callback_proxy<D, F>(
where
F: FnMut(&mut crate::Unicorn<D>, u64, usize) -> u64,
{
+ if user_data.is_null() {
+ return 0;
+ }
let user_data = &mut *user_data;
+ let Some(inner) = user_data.uc.upgrade() else {
+ return 0;
+ };
let mut user_data_uc = Unicorn {
- inner: user_data.uc.upgrade().unwrap(),
+ inner,
};
debug_assert_eq!(uc, user_data_uc.get_handle());
(user_data.callback)(&mut user_data_uc, offset, size)
@@ -138,9 +144,15 @@ pub unsafe extern "C" fn mmio_write_callback_proxy<D, F>(
) where
F: FnMut(&mut crate::Unicorn<D>, u64, usize, u64),
{
+ if user_data.is_null() {
+ return;
+ }
let user_data = &mut *user_data;
+ let Some(inner) = user_data.uc.upgrade() else {
+ return;
+ };
let mut user_data_uc = Unicorn {
- inner: user_data.uc.upgrade().unwrap(),
+ inner,
};
debug_assert_eq!(uc, user_data_uc.get_handle());
(user_data.callback)(&mut user_data_uc, offset, size, value);
@@ -154,9 +166,15 @@ pub unsafe extern "C" fn code_hook_proxy<D, F>(
) where
F: FnMut(&mut crate::Unicorn<D>, u64, u32),
{
+ if user_data.is_null() {
+ return;
+ }
let user_data = &mut *user_data;
+ let Some(inner) = user_data.uc.upgrade() else {
+ return;
+ };
let mut user_data_uc = Unicorn {
- inner: user_data.uc.upgrade().unwrap(),
+ inner,
};
debug_assert_eq!(uc, user_data_uc.get_handle());
(user_data.callback)(&mut user_data_uc, address, size);
@@ -170,9 +188,15 @@ pub unsafe extern "C" fn block_hook_proxy<D, F>(
) where
F: FnMut(&mut crate::Unicorn<D>, u64, u32),
{
+ if user_data.is_null() {
+ return;
+ }
let user_data = &mut *user_data;
+ let Some(inner) = user_data.uc.upgrade() else {
+ return;
+ };
let mut user_data_uc = Unicorn {
- inner: user_data.uc.upgrade().unwrap(),
+ inner,
};
debug_assert_eq!(uc, user_data_uc.get_handle());
(user_data.callback)(&mut user_data_uc, address, size);
@@ -189,9 +213,15 @@ pub unsafe extern "C" fn mem_hook_proxy<D, F>(
where
F: FnMut(&mut crate::Unicorn<D>, MemType, u64, usize, i64) -> bool,
{
+ if user_data.is_null() {
+ return false;
+ }
let user_data = &mut *user_data;
+ let Some(inner) = user_data.uc.upgrade() else {
+ return false;
+ };
let mut user_data_uc = Unicorn {
- inner: user_data.uc.upgrade().unwrap(),
+ inner,
};
debug_assert_eq!(uc, user_data_uc.get_handle());
(user_data.callback)(&mut user_data_uc, mem_type, address, size as usize, value)
@@ -204,9 +234,15 @@ pub unsafe extern "C" fn intr_hook_proxy<D, F>(
) where
F: FnMut(&mut crate::Unicorn<D>, u32),
{
+ if user_data.is_null() {
+ return;
+ }
let user_data = &mut *user_data;
+ let Some(inner) = user_data.uc.upgrade() else {
+ return;
+ };
let mut user_data_uc = Unicorn {
- inner: user_data.uc.upgrade().unwrap(),
+ inner,
};
debug_assert_eq!(uc, user_data_uc.get_handle());
(user_data.callback)(&mut user_data_uc, value);
@@ -221,9 +257,15 @@ pub unsafe extern "C" fn insn_in_hook_proxy<D, F>(
where
F: FnMut(&mut crate::Unicorn<D>, u32, usize) -> u32,
{
+ if user_data.is_null() {
+ return 0;
+ }
let user_data = &mut *user_data;
+ let Some(inner) = user_data.uc.upgrade() else {
+ return 0;
+ };
let mut user_data_uc = Unicorn {
- inner: user_data.uc.upgrade().unwrap(),
+ inner,
};
debug_assert_eq!(uc, user_data_uc.get_handle());
(user_data.callback)(&mut user_data_uc, port, size)
@@ -236,9 +278,15 @@ pub unsafe extern "C" fn insn_invalid_hook_proxy<D, F>(
where
F: FnMut(&mut crate::Unicorn<D>) -> bool,
{
+ if user_data.is_null() {
+ return false;
+ }
let user_data = &mut *user_data;
+ let Some(inner) = user_data.uc.upgrade() else {
+ return false;
+ };
let mut user_data_uc = Unicorn {
- inner: user_data.uc.upgrade().unwrap(),
+ inner,
};
debug_assert_eq!(uc, user_data_uc.get_handle());
(user_data.callback)(&mut user_data_uc)
@@ -253,9 +301,15 @@ pub unsafe extern "C" fn insn_out_hook_proxy<D, F>(
) where
F: FnMut(&mut crate::Unicorn<D>, u32, usize, u32),
{
+ if user_data.is_null() {
+ return;
+ }
let user_data = &mut *user_data;
+ let Some(inner) = user_data.uc.upgrade() else {
+ return;
+ };
let mut user_data_uc = Unicorn {
- inner: user_data.uc.upgrade().unwrap(),
+ inner,
};
debug_assert_eq!(uc, user_data_uc.get_handle());
(user_data.callback)(&mut user_data_uc, port, size, value);
@@ -265,9 +319,15 @@ pub unsafe extern "C" fn insn_sys_hook_proxy<D, F>(uc: uc_handle, user_data: *mu
where
F: FnMut(&mut crate::Unicorn<D>),
{
+ if user_data.is_null() {
+ return;
+ }
let user_data = &mut *user_data;
+ let Some(inner) = user_data.uc.upgrade() else {
+ return;
+ };
let mut user_data_uc = Unicorn {
- inner: user_data.uc.upgrade().unwrap(),
+ inner,
};
debug_assert_eq!(uc, user_data_uc.get_handle());
(user_data.callback)(&mut user_data_uc);
@@ -283,9 +343,15 @@ pub unsafe extern "C" fn tlb_lookup_hook_proxy<D, F>(
where
F: FnMut(&mut crate::Unicorn<D>, u64, MemType) -> Option<TlbEntry>,
{
+ if user_data.is_null() {
+ return false;
+ }
let user_data = &mut *user_data;
+ let Some(inner) = user_data.uc.upgrade() else {
+ return false;
+ };
let mut user_data_uc = Unicorn {
- inner: user_data.uc.upgrade().unwrap(),
+ inner,
};
debug_assert_eq!(uc, user_data_uc.get_handle());
let r = (user_data.callback)(&mut user_data_uc, vaddr, mem_type);

View File

@@ -0,0 +1,16 @@
diff --git a/qemu/accel/tcg/translate-all.c b/qemu/accel/tcg/translate-all.c
index 0524fefd..9bc1fd39 100644
--- a/qemu/accel/tcg/translate-all.c
+++ b/qemu/accel/tcg/translate-all.c
@@ -862,6 +862,11 @@ static inline void *alloc_code_gen_buffer(struct uc_struct *uc)
return buf;
}
+
+void free_code_gen_buffer(struct uc_struct *uc)
+{
+ (void)uc;
+}
#elif defined(_WIN32)
#define COMMIT_COUNT (1024) // Commit 4MB per exception
#define CLOSURE_SIZE (4096)

54
script/rebuild-unicorn.sh Executable file
View File

@@ -0,0 +1,54 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
EMSDK_DIR="${EMSDK:-/Users/libr/Desktop/Life/emsdk}"
UNICORN_DIR="${UNICORN_DIR:-${ROOT_DIR}/../unicorn}"
UNICORN_BUILD_DIR="${UNICORN_BUILD_DIR:-${UNICORN_DIR}/build}"
JOBS="${JOBS:-8}"
PATCH_DIR="${PATCH_DIR:-${ROOT_DIR}/script/patches}"
if [[ ! -d "${UNICORN_DIR}" ]]; then
echo "unicorn directory not found: ${UNICORN_DIR}"
exit 1
fi
if [[ -f "${EMSDK_DIR}/emsdk_env.sh" ]]; then
# shellcheck disable=SC1090
source "${EMSDK_DIR}/emsdk_env.sh" >/dev/null
else
echo "emsdk_env.sh not found at ${EMSDK_DIR}/emsdk_env.sh"
exit 1
fi
# if [[ -d "${PATCH_DIR}" ]]; then
# for patch_file in "${PATCH_DIR}"/*.diff; do
# if [[ ! -f "${patch_file}" ]]; then
# continue
# fi
# echo "applying patch: ${patch_file}"
# if ! git -C "${UNICORN_DIR}" apply "${patch_file}"; then
# echo "skip failed patch: ${patch_file}"
# fi
# done
# fi
# rm -rf "${UNICORN_BUILD_DIR}"
mkdir -p "${UNICORN_BUILD_DIR}"
pushd "${UNICORN_BUILD_DIR}" >/dev/null
emcmake cmake "${UNICORN_DIR}" \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_SHARED_LIBS=OFF \
-DUNICORN_BUILD_TESTS=OFF \
-DUNICORN_INSTALL=OFF \
-DUNICORN_LEGACY_STATIC_ARCHIVE=ON \
-DUNICORN_INTERPRETER=ON \
-DUNICORN_ARCH="arm;aarch64" \
-DCMAKE_C_COMPILER=emcc \
-DCMAKE_C_FLAGS="-DUSE_STATIC_CODE_GEN_BUFFER"
cmake --build . -- -j"${JOBS}"
popd >/dev/null
echo "unicorn rebuild done: ${UNICORN_BUILD_DIR}"

243
src/adi.rs Normal file
View File

@@ -0,0 +1,243 @@
use crate::debug::debug_print;
use crate::emu::{EmuCore, alloc_c_string, ensure_zero_return};
use crate::errors::VmError;
use crate::util::bytes_to_hex;
pub struct AdiInit {
pub storeservicescore: Vec<u8>,
pub coreadi: Vec<u8>,
pub library_path: String,
pub provisioning_path: Option<String>,
pub identifier: Option<String>,
}
pub struct ProvisioningStartResult {
pub cpim: Vec<u8>,
pub session: u32,
}
pub struct OtpResult {
pub otp: Vec<u8>,
pub machine_id: Vec<u8>,
}
pub struct Adi {
core: EmuCore,
p_load_library_with_path: u64,
p_set_android_id: u64,
p_set_provisioning_path: u64,
p_get_login_code: u64,
p_provisioning_start: u64,
p_provisioning_end: u64,
p_otp_request: u64,
}
impl Adi {
pub fn new(init: AdiInit) -> Result<Self, VmError> {
debug_print(format!("Constructing ADI for '{}'", init.library_path));
let mut core = EmuCore::new_arm64()?;
core.set_library_root(&init.library_path);
core.register_library_blob("libstoreservicescore.so", init.storeservicescore);
core.register_library_blob("libCoreADI.so", init.coreadi);
let storeservices_idx = core.load_library("libstoreservicescore.so")?;
debug_print("Loading Android-specific symbols...");
let p_load_library_with_path =
core.resolve_symbol_by_name(storeservices_idx, "kq56gsgHG6")?;
let p_set_android_id = core.resolve_symbol_by_name(storeservices_idx, "Sph98paBcz")?;
let p_set_provisioning_path =
core.resolve_symbol_by_name(storeservices_idx, "nf92ngaK92")?;
debug_print("Loading ADI symbols...");
let p_get_login_code = core.resolve_symbol_by_name(storeservices_idx, "aslgmuibau")?;
let p_provisioning_start = core.resolve_symbol_by_name(storeservices_idx, "rsegvyrt87")?;
let p_provisioning_end = core.resolve_symbol_by_name(storeservices_idx, "uv5t6nhkui")?;
let p_otp_request = core.resolve_symbol_by_name(storeservices_idx, "qi864985u0")?;
let mut adi = Self {
core,
p_load_library_with_path,
p_set_android_id,
p_set_provisioning_path,
p_get_login_code,
p_provisioning_start,
p_provisioning_end,
p_otp_request,
};
adi.load_library_with_path(&init.library_path)?;
if let Some(provisioning_path) = init.provisioning_path.as_deref() {
adi.set_provisioning_path(provisioning_path)?;
}
if let Some(identifier) = init.identifier.as_deref() {
adi.set_identifier(identifier)?;
}
Ok(adi)
}
pub fn set_identifier(&mut self, identifier: &str) -> Result<(), VmError> {
if identifier.is_empty() {
debug_print("Skipping empty identifier");
return Ok(());
}
debug_print(format!("Setting identifier {identifier}"));
let bytes = identifier.as_bytes();
let p_identifier = self.core.alloc_data(bytes)?;
let ret = self
.core
.invoke_cdecl(self.p_set_android_id, &[p_identifier, bytes.len() as u64])?;
debug_print(format!(
"{}: {:X}={}",
"pADISetAndroidID", ret, ret as u32 as i32
));
ensure_zero_return("ADISetAndroidID", ret)
}
pub fn set_provisioning_path(&mut self, path: &str) -> Result<(), VmError> {
let p_path = alloc_c_string(&mut self.core, path)?;
let ret = self
.core
.invoke_cdecl(self.p_set_provisioning_path, &[p_path])?;
ensure_zero_return("ADISetProvisioningPath", ret)
}
pub fn load_library_with_path(&mut self, path: &str) -> Result<(), VmError> {
let p_path = alloc_c_string(&mut self.core, path)?;
let ret = self
.core
.invoke_cdecl(self.p_load_library_with_path, &[p_path])?;
ensure_zero_return("ADILoadLibraryWithPath", ret)
}
pub fn start_provisioning(
&mut self,
dsid: u64,
server_provisioning_intermediate_metadata: &[u8],
) -> Result<ProvisioningStartResult, VmError> {
debug_print("ADI.start_provisioning");
let p_cpim = self.core.alloc_temporary(8)?;
let p_cpim_len = self.core.alloc_temporary(4)?;
let p_session = self.core.alloc_temporary(4)?;
let p_spim = self
.core
.alloc_data(server_provisioning_intermediate_metadata)?;
debug_print(format!("0x{dsid:X}"));
debug_print(bytes_to_hex(server_provisioning_intermediate_metadata));
let ret = self.core.invoke_cdecl(
self.p_provisioning_start,
&[
dsid,
p_spim,
server_provisioning_intermediate_metadata.len() as u64,
p_cpim,
p_cpim_len,
p_session,
],
)?;
debug_print(format!(
"{}: {:X}={}",
"pADIProvisioningStart", ret, ret as u32 as i32
));
ensure_zero_return("ADIProvisioningStart", ret)?;
let cpim_ptr = self.core.read_u64(p_cpim)?;
let cpim_len = self.core.read_u32(p_cpim_len)? as usize;
let cpim = self.core.read_data(cpim_ptr, cpim_len)?;
let session = self.core.read_u32(p_session)?;
debug_print(format!("Wrote data to 0x{cpim_ptr:X}"));
debug_print(format!("{} {} {}", cpim_len, bytes_to_hex(&cpim), session));
Ok(ProvisioningStartResult { cpim, session })
}
pub fn is_machine_provisioned(&mut self, dsid: u64) -> Result<bool, VmError> {
debug_print("ADI.is_machine_provisioned");
let ret = self.core.invoke_cdecl(self.p_get_login_code, &[dsid])?;
let code = ret as u32 as i32;
if code == 0 {
return Ok(true);
}
if code == -45061 {
return Ok(false);
}
debug_print(format!(
"Unknown errorCode in is_machine_provisioned: {code}=0x{code:X}"
));
Err(VmError::AdiCallFailed {
name: "ADIGetLoginCode",
code,
})
}
pub fn end_provisioning(
&mut self,
session: u32,
persistent_token_metadata: &[u8],
trust_key: &[u8],
) -> Result<(), VmError> {
let p_ptm = self.core.alloc_data(persistent_token_metadata)?;
let p_tk = self.core.alloc_data(trust_key)?;
let ret = self.core.invoke_cdecl(
self.p_provisioning_end,
&[
session as u64,
p_ptm,
persistent_token_metadata.len() as u64,
p_tk,
trust_key.len() as u64,
],
)?;
debug_print(format!("0x{session:X}"));
debug_print(format!(
"{} {}",
bytes_to_hex(persistent_token_metadata),
persistent_token_metadata.len()
));
debug_print(format!("{} {}", bytes_to_hex(trust_key), trust_key.len()));
debug_print(format!(
"{}: {:X}={}",
"pADIProvisioningEnd", ret, ret as u32 as i32
));
ensure_zero_return("ADIProvisioningEnd", ret)
}
pub fn request_otp(&mut self, dsid: u64) -> Result<OtpResult, VmError> {
debug_print("ADI.request_otp");
let p_otp = self.core.alloc_temporary(8)?;
let p_otp_len = self.core.alloc_temporary(4)?;
let p_mid = self.core.alloc_temporary(8)?;
let p_mid_len = self.core.alloc_temporary(4)?;
let ret = self.core.invoke_cdecl(
self.p_otp_request,
&[dsid, p_mid, p_mid_len, p_otp, p_otp_len],
)?;
debug_print(format!(
"{}: {:X}={}",
"pADIOTPRequest", ret, ret as u32 as i32
));
ensure_zero_return("ADIOTPRequest", ret)?;
let otp_ptr = self.core.read_u64(p_otp)?;
let otp_len = self.core.read_u32(p_otp_len)? as usize;
let otp = self.core.read_data(otp_ptr, otp_len)?;
let mid_ptr = self.core.read_u64(p_mid)?;
let mid_len = self.core.read_u32(p_mid_len)? as usize;
let machine_id = self.core.read_data(mid_ptr, mid_len)?;
Ok(OtpResult { otp, machine_id })
}
}

50
src/allocator.rs Normal file
View File

@@ -0,0 +1,50 @@
use crate::constants::PAGE_SIZE;
use crate::errors::VmError;
use crate::util::align_up;
#[derive(Debug, Clone)]
pub struct Allocator {
base: u64,
size: u64,
offset: u64,
}
impl Allocator {
pub fn new(base: u64, size: u64) -> Self {
Self {
base,
size,
offset: 0,
}
}
pub fn alloc(&mut self, request: u64) -> Result<u64, VmError> {
let length = align_up(request.max(1), PAGE_SIZE);
let address = self.base + self.offset;
let next = self.offset.saturating_add(length);
if next > self.size {
return Err(VmError::AllocatorOom {
base: self.base,
size: self.size,
request,
});
}
self.offset = next;
Ok(address)
}
}
#[cfg(test)]
mod tests {
use super::Allocator;
#[test]
fn allocator_aligns_to_pages() {
let mut allocator = Allocator::new(0x1000_0000, 0x20_000);
let a = allocator.alloc(1).expect("alloc 1");
let b = allocator.alloc(0x1500).expect("alloc 2");
assert_eq!(a, 0x1000_0000);
assert_eq!(b, 0x1000_1000);
}
}

67
src/constants.rs Normal file
View File

@@ -0,0 +1,67 @@
use unicorn_engine::RegisterARM64;
pub const PAGE_SIZE: u64 = 0x1000;
pub const RETURN_ADDRESS: u64 = 0xDEAD_0000;
pub const STACK_ADDRESS: u64 = 0xF000_0000;
pub const STACK_SIZE: u64 = 0x10_0000;
pub const MALLOC_ADDRESS: u64 = 0x6000_0000;
pub const MALLOC_SIZE: u64 = 0x10_00000;
pub const IMPORT_ADDRESS: u64 = 0xA000_0000;
pub const IMPORT_SIZE: u64 = 0x1000;
pub const IMPORT_LIBRARY_STRIDE: u64 = 0x0100_0000;
pub const IMPORT_LIBRARY_COUNT: usize = 10;
pub const TEMP_ALLOC_BASE: u64 = 0x0008_0000_0000;
pub const TEMP_ALLOC_SIZE: u64 = 0x1000_0000;
pub const LIB_ALLOC_BASE: u64 = 0x0010_0000;
pub const LIB_ALLOC_SIZE: u64 = 0x9000_0000;
pub const LIB_RESERVATION_SIZE: u64 = 0x1000_0000;
pub const O_WRONLY: u64 = 0o1;
pub const O_RDWR: u64 = 0o2;
pub const O_ACCMODE: u64 = 0o3;
pub const O_CREAT: u64 = 0o100;
pub const O_NOFOLLOW: u64 = 0o100000;
pub const ENOENT: u32 = 2;
pub const RET_AARCH64: [u8; 4] = [0xC0, 0x03, 0x5F, 0xD6];
pub const ARG_REGS: [RegisterARM64; 29] = [
RegisterARM64::X0,
RegisterARM64::X1,
RegisterARM64::X2,
RegisterARM64::X3,
RegisterARM64::X4,
RegisterARM64::X5,
RegisterARM64::X6,
RegisterARM64::X7,
RegisterARM64::X8,
RegisterARM64::X9,
RegisterARM64::X10,
RegisterARM64::X11,
RegisterARM64::X12,
RegisterARM64::X13,
RegisterARM64::X14,
RegisterARM64::X15,
RegisterARM64::X16,
RegisterARM64::X17,
RegisterARM64::X18,
RegisterARM64::X19,
RegisterARM64::X20,
RegisterARM64::X21,
RegisterARM64::X22,
RegisterARM64::X23,
RegisterARM64::X24,
RegisterARM64::X25,
RegisterARM64::X26,
RegisterARM64::X27,
RegisterARM64::X28,
];
pub const DEBUG_PRINT_ENABLED: bool = false;
pub const DEBUG_TRACE_ENABLED: bool = false;

113
src/debug.rs Normal file
View File

@@ -0,0 +1,113 @@
use std::fmt::Write as _;
use unicorn_engine::unicorn_const::MemType;
use unicorn_engine::{RegisterARM64, Unicorn};
use crate::constants::{DEBUG_PRINT_ENABLED, DEBUG_TRACE_ENABLED};
use crate::runtime::RuntimeState;
pub(crate) fn debug_print(message: impl AsRef<str>) {
if DEBUG_PRINT_ENABLED {
println!("{}", message.as_ref());
}
}
pub(crate) fn debug_trace(message: impl AsRef<str>) {
if DEBUG_TRACE_ENABLED {
println!("{}", message.as_ref());
}
}
pub(crate) fn reg_or_zero(uc: &Unicorn<'_, RuntimeState>, reg: RegisterARM64) -> u64 {
uc.reg_read(reg).unwrap_or(0)
}
pub(crate) fn trace_mem_invalid_hook(
uc: &Unicorn<'_, RuntimeState>,
access: MemType,
address: u64,
size: usize,
value: i64,
) {
let pc = reg_or_zero(uc, RegisterARM64::PC);
match access {
MemType::READ_UNMAPPED => {
debug_print(format!(
">>> Missing memory is being READ at 0x{address:x}, data size = {size}, data value = 0x{:x}, PC=0x{pc:x}",
value as u64
));
dump_registers(uc, "read unmapped");
}
MemType::WRITE_UNMAPPED => {
debug_print(format!(
">>> Missing memory is being WRITE at 0x{address:x}, data size = {size}, data value = 0x{:x}, PC=0x{pc:x}",
value as u64
));
dump_registers(uc, "write unmapped");
}
MemType::FETCH_UNMAPPED => {
debug_print(format!(
">>> Missing memory is being FETCH at 0x{address:x}, data size = {size}, data value = 0x{:x}, PC=0x{pc:x}",
value as u64
));
}
_ => {}
}
}
pub(crate) fn dump_registers(uc: &Unicorn<'_, RuntimeState>, label: &str) {
// debug_print(format!());
println!("REGDUMP {label}");
let regs: &[(RegisterARM64, &str)] = &[
(RegisterARM64::X0, "X0"),
(RegisterARM64::X1, "X1"),
(RegisterARM64::X2, "X2"),
(RegisterARM64::X3, "X3"),
(RegisterARM64::X4, "X4"),
(RegisterARM64::X5, "X5"),
(RegisterARM64::X6, "X6"),
(RegisterARM64::X7, "X7"),
(RegisterARM64::X8, "X8"),
(RegisterARM64::X9, "X9"),
(RegisterARM64::X10, "X10"),
(RegisterARM64::X11, "X11"),
(RegisterARM64::X12, "X12"),
(RegisterARM64::X13, "X13"),
(RegisterARM64::X14, "X14"),
(RegisterARM64::X15, "X15"),
(RegisterARM64::X16, "X16"),
(RegisterARM64::X17, "X17"),
(RegisterARM64::X18, "X18"),
(RegisterARM64::X19, "X19"),
(RegisterARM64::X20, "X20"),
(RegisterARM64::X21, "X21"),
(RegisterARM64::X22, "X22"),
(RegisterARM64::X23, "X23"),
(RegisterARM64::X24, "X24"),
(RegisterARM64::X25, "X25"),
(RegisterARM64::X26, "X26"),
(RegisterARM64::X27, "X27"),
(RegisterARM64::X28, "X28"),
(RegisterARM64::FP, "FP"),
(RegisterARM64::LR, "LR"),
(RegisterARM64::SP, "SP"),
];
let mut line = String::new();
for (i, (reg, name)) in regs.iter().enumerate() {
let value = reg_or_zero(uc, *reg);
let _ = write!(line, " {name}=0x{value:016X}");
if (i + 1) % 4 == 0 {
println!("{}", line);
line = String::new();
}
}
if !line.is_empty() {
println!("{}", line);
}
}

92
src/device.rs Normal file
View File

@@ -0,0 +1,92 @@
use std::fmt::Write as _;
use std::fs;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use rand::RngCore;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
const DEFAULT_CLIENT_INFO: &str =
"<MacBookPro13,2> <macOS;13.1;22C65> <com.apple.AuthKit/1 (com.apple.dt.Xcode/3594.4.19)>";
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct DeviceData {
#[serde(rename = "UUID")]
pub unique_device_identifier: String,
#[serde(rename = "clientInfo")]
pub server_friendly_description: String,
#[serde(rename = "identifier")]
pub adi_identifier: String,
#[serde(rename = "localUUID")]
pub local_user_uuid: String,
}
#[derive(Debug, Clone)]
pub struct Device {
path: PathBuf,
pub data: DeviceData,
pub initialized: bool,
}
impl Device {
pub fn load(path: impl AsRef<Path>) -> Result<Self> {
let path = path.as_ref().to_path_buf();
if !path.exists() {
return Ok(Self {
path,
data: DeviceData::default(),
initialized: false,
});
}
let bytes = fs::read(&path)
.with_context(|| format!("failed to read device file {}", path.display()))?;
let data: DeviceData = serde_json::from_slice(&bytes)
.with_context(|| format!("failed to parse device file {}", path.display()))?;
Ok(Self {
path,
data,
initialized: true,
})
}
pub fn initialize_defaults(&mut self) {
self.data.server_friendly_description = DEFAULT_CLIENT_INFO.to_string();
self.data.unique_device_identifier = Uuid::new_v4().to_string().to_uppercase();
self.data.adi_identifier = random_hex(8, false);
self.data.local_user_uuid = random_hex(32, true);
self.initialized = true;
}
pub fn persist(&self) -> Result<()> {
if let Some(parent) = self.path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("failed to create parent dir {}", parent.display()))?;
}
let bytes = serde_json::to_vec_pretty(&self.data)?;
fs::write(&self.path, bytes)
.with_context(|| format!("failed to write device file {}", self.path.display()))?;
Ok(())
}
}
fn random_hex(byte_len: usize, uppercase: bool) -> String {
let mut bytes = vec![0_u8; byte_len];
rand::thread_rng().fill_bytes(&mut bytes);
let mut output = String::with_capacity(byte_len * 2);
for byte in bytes {
let _ = write!(output, "{byte:02x}");
}
if uppercase {
output.make_ascii_uppercase();
}
output
}

428
src/emu.rs Normal file
View File

@@ -0,0 +1,428 @@
use std::collections::HashMap;
use goblin::elf::program_header::PT_LOAD;
use goblin::elf::section_header::SHN_UNDEF;
use goblin::elf::{Elf, Reloc};
use unicorn_engine::unicorn_const::{Arch, HookType, Mode, Permission, uc_error};
use unicorn_engine::{RegisterARM64, Unicorn};
use crate::constants::{
ARG_REGS, IMPORT_ADDRESS, IMPORT_LIBRARY_COUNT, IMPORT_LIBRARY_STRIDE, IMPORT_SIZE,
LIB_RESERVATION_SIZE, MALLOC_ADDRESS, MALLOC_SIZE, PAGE_SIZE, RET_AARCH64, RETURN_ADDRESS,
STACK_ADDRESS, STACK_SIZE,
};
use crate::debug::{debug_print, trace_mem_invalid_hook};
use crate::errors::VmError;
use crate::runtime::{LoadedLibrary, RuntimeState, SymbolEntry};
use crate::stub::dispatch_import_stub;
use crate::util::{add_i64, align_down, align_up, as_usize};
pub struct EmuCore {
uc: Unicorn<'static, RuntimeState>,
}
impl EmuCore {
pub fn new_arm64() -> Result<Self, VmError> {
let mut uc = Unicorn::new_with_data(Arch::ARM64, Mode::ARM, RuntimeState::new())?;
uc.mem_map(RETURN_ADDRESS, as_usize(PAGE_SIZE)?, Permission::ALL)?;
uc.mem_map(MALLOC_ADDRESS, as_usize(MALLOC_SIZE)?, Permission::ALL)?;
uc.mem_map(STACK_ADDRESS, as_usize(STACK_SIZE)?, Permission::ALL)?;
for i in 0..IMPORT_LIBRARY_COUNT {
let base = IMPORT_ADDRESS + (i as u64) * IMPORT_LIBRARY_STRIDE;
uc.mem_map(base, as_usize(IMPORT_SIZE)?, Permission::ALL)?;
let mut stubs = vec![0_u8; IMPORT_SIZE as usize];
for chunk in stubs.chunks_mut(4) {
chunk.copy_from_slice(&RET_AARCH64);
}
uc.mem_write(base, &stubs)?;
uc.add_code_hook(base, base + IMPORT_SIZE - 1, |uc, address, _| {
if let Err(err) = dispatch_import_stub(uc, address) {
debug_print(format!("import hook failed at 0x{address:X}: {err}"));
let _ = uc.emu_stop();
}
})?;
}
uc.add_mem_hook(
HookType::MEM_READ_UNMAPPED
| HookType::MEM_WRITE_UNMAPPED
| HookType::MEM_FETCH_UNMAPPED,
1,
0,
|uc, access, address, size, value| {
trace_mem_invalid_hook(uc, access, address, size, value);
false
},
)?;
Ok(Self { uc })
}
pub fn register_library_blob(&mut self, name: impl Into<String>, data: Vec<u8>) {
self.uc
.get_data_mut()
.library_blobs
.insert(name.into(), data);
}
pub fn set_library_root(&mut self, path: &str) {
let normalized = normalize_library_root(path);
if normalized.is_empty() {
return;
}
self.uc.get_data_mut().library_root = Some(normalized);
}
pub fn load_library(&mut self, library_name: &str) -> Result<usize, VmError> {
load_library_by_name(&mut self.uc, library_name)
}
pub fn resolve_symbol_by_name(
&self,
library_index: usize,
symbol_name: &str,
) -> Result<u64, VmError> {
resolve_symbol_from_loaded_library_by_name(&self.uc, library_index, symbol_name)
}
pub fn invoke_cdecl(&mut self, address: u64, args: &[u64]) -> Result<u64, VmError> {
if args.len() > ARG_REGS.len() {
return Err(VmError::TooManyArguments(args.len()));
}
for (index, value) in args.iter().enumerate() {
self.uc.reg_write(ARG_REGS[index], *value)?;
debug_print(format!("X{index}: 0x{value:08X}"));
}
debug_print(format!("Calling 0x{address:X}"));
self.uc
.reg_write(RegisterARM64::SP, STACK_ADDRESS + STACK_SIZE)?;
self.uc.reg_write(RegisterARM64::LR, RETURN_ADDRESS)?;
self.uc.emu_start(address, RETURN_ADDRESS, 0, 0)?;
Ok(self.uc.reg_read(RegisterARM64::X0)?)
}
pub fn alloc_data(&mut self, data: &[u8]) -> Result<u64, VmError> {
alloc_temp_bytes(&mut self.uc, data, 0xCC)
}
pub fn alloc_temporary(&mut self, length: usize) -> Result<u64, VmError> {
let data = vec![0xAA; length.max(1)];
alloc_temp_bytes(&mut self.uc, &data, 0xAA)
}
pub fn read_data(&self, address: u64, length: usize) -> Result<Vec<u8>, VmError> {
Ok(self.uc.mem_read_as_vec(address, length)?)
}
pub fn write_data(&mut self, address: u64, data: &[u8]) -> Result<(), VmError> {
self.uc.mem_write(address, data)?;
Ok(())
}
pub fn read_u32(&self, address: u64) -> Result<u32, VmError> {
let mut bytes = [0_u8; 4];
self.uc.mem_read(address, &mut bytes)?;
Ok(u32::from_le_bytes(bytes))
}
pub fn read_u64(&self, address: u64) -> Result<u64, VmError> {
let mut bytes = [0_u8; 8];
self.uc.mem_read(address, &mut bytes)?;
Ok(u64::from_le_bytes(bytes))
}
pub fn write_u32(&mut self, address: u64, value: u32) -> Result<(), VmError> {
self.uc.mem_write(address, &value.to_le_bytes())?;
Ok(())
}
pub fn write_u64(&mut self, address: u64, value: u64) -> Result<(), VmError> {
self.uc.mem_write(address, &value.to_le_bytes())?;
Ok(())
}
pub fn read_c_string(&self, address: u64, max_len: usize) -> Result<String, VmError> {
read_c_string(&self.uc, address, max_len)
}
}
pub(crate) fn alloc_c_string(core: &mut EmuCore, value: &str) -> Result<u64, VmError> {
let mut bytes = Vec::with_capacity(value.len() + 1);
bytes.extend_from_slice(value.as_bytes());
bytes.push(0);
core.alloc_data(&bytes)
}
pub(crate) fn ensure_zero_return(name: &'static str, value: u64) -> Result<(), VmError> {
let code = value as u32 as i32;
if code == 0 {
Ok(())
} else {
Err(VmError::AdiCallFailed { name, code })
}
}
fn normalize_library_root(path: &str) -> String {
let trimmed = path.trim();
if trimmed.is_empty() {
return String::new();
}
let mut out = String::with_capacity(trimmed.len());
let mut prev_slash = false;
for ch in trimmed.chars() {
if ch == '/' {
if prev_slash {
continue;
}
prev_slash = true;
} else {
prev_slash = false;
}
out.push(ch);
}
while out.len() > 1 && out.ends_with('/') {
out.pop();
}
out
}
fn alloc_temp_bytes(
uc: &mut Unicorn<'_, RuntimeState>,
data: &[u8],
padding_byte: u8,
) -> Result<u64, VmError> {
let request = data.len().max(1) as u64;
let length = align_up(request, PAGE_SIZE);
let address = {
let state = uc.get_data_mut();
state.temp_allocator.alloc(length)?
};
debug_print(format!(
"Allocating at 0x{address:X}; bytes 0x{:X}/0x{length:X}",
data.len()
));
uc.mem_map(address, as_usize(length)?, Permission::ALL)?;
let mut buffer = vec![padding_byte; length as usize];
if !data.is_empty() {
buffer[..data.len()].copy_from_slice(data);
}
uc.mem_write(address, &buffer)?;
Ok(address)
}
pub(crate) fn set_errno(uc: &mut Unicorn<'_, RuntimeState>, value: u32) -> Result<(), VmError> {
let errno_address = ensure_errno_address(uc)?;
uc.mem_write(errno_address, &value.to_le_bytes())?;
Ok(())
}
pub(crate) fn ensure_errno_address(uc: &mut Unicorn<'_, RuntimeState>) -> Result<u64, VmError> {
if let Some(address) = uc.get_data().errno_address {
return Ok(address);
}
let address = alloc_temp_bytes(uc, &[0, 0, 0, 0], 0)?;
uc.get_data_mut().errno_address = Some(address);
Ok(address)
}
pub(crate) fn load_library_by_name(
uc: &mut Unicorn<'_, RuntimeState>,
library_name: &str,
) -> Result<usize, VmError> {
for (index, library) in uc.get_data().loaded_libraries.iter().enumerate() {
if library.name == library_name {
debug_print("Library already loaded");
return Ok(index);
}
}
let (library_index, elf_data) = {
let state = uc.get_data();
let data = state
.library_blobs
.get(library_name)
.cloned()
.ok_or_else(|| VmError::LibraryNotRegistered(library_name.to_string()))?;
(state.loaded_libraries.len(), data)
};
let elf = Elf::parse(&elf_data)?;
let base = {
let state = uc.get_data_mut();
state.library_allocator.alloc(LIB_RESERVATION_SIZE)?
};
let mut symbols = Vec::with_capacity(elf.dynsyms.len());
let mut symbols_by_name = HashMap::new();
for (index, sym) in elf.dynsyms.iter().enumerate() {
let name = elf.dynstrtab.get_at(sym.st_name).unwrap_or("").to_string();
let resolved = if sym.st_shndx == SHN_UNDEF as usize {
IMPORT_ADDRESS + (library_index as u64) * IMPORT_LIBRARY_STRIDE + (index as u64) * 4
} else {
base.wrapping_add(sym.st_value)
};
if !name.is_empty() {
symbols_by_name.entry(name.clone()).or_insert(resolved);
}
symbols.push(SymbolEntry { name, resolved });
}
for ph in &elf.program_headers {
let seg_addr = base.wrapping_add(ph.p_vaddr);
let map_start = align_down(seg_addr, PAGE_SIZE);
let map_end = align_up(seg_addr.wrapping_add(ph.p_memsz), PAGE_SIZE);
let map_len = map_end.saturating_sub(map_start);
if map_len == 0 {
continue;
}
debug_print(format!(
"Mapping at 0x{map_start:X}-0x{map_end:X} (0x{seg_addr:X}-0x{:X}); bytes 0x{map_len:X}",
seg_addr + map_len.saturating_sub(1)
));
if ph.p_type != PT_LOAD || ph.p_memsz == 0 {
debug_print(format!(
"- Skipping p_type={} offset=0x{:X} vaddr=0x{:X}",
ph.p_type, ph.p_offset, ph.p_vaddr
));
continue;
}
match uc.mem_map(map_start, as_usize(map_len)?, Permission::ALL) {
Ok(()) => {}
Err(uc_error::MAP) => {}
Err(err) => return Err(err.into()),
}
let file_offset = ph.p_offset as usize;
let file_len = ph.p_filesz as usize;
let file_end = file_offset
.checked_add(file_len)
.ok_or(VmError::InvalidElfRange)?;
if file_end > elf_data.len() {
return Err(VmError::InvalidElfRange);
}
let mut bytes = vec![0_u8; map_len as usize];
let start_offset = (seg_addr - map_start) as usize;
if file_len > 0 {
let dest_end = start_offset
.checked_add(file_len)
.ok_or(VmError::InvalidElfRange)?;
if dest_end > bytes.len() {
return Err(VmError::InvalidElfRange);
}
bytes[start_offset..dest_end].copy_from_slice(&elf_data[file_offset..file_end]);
}
uc.mem_write(map_start, &bytes)?;
}
for rela in elf.dynrelas.iter() {
apply_relocation(uc, base, &rela, library_name, &symbols)?;
}
for rela in elf.pltrelocs.iter() {
apply_relocation(uc, base, &rela, library_name, &symbols)?;
}
let loaded = LoadedLibrary {
name: library_name.to_string(),
symbols,
symbols_by_name,
};
uc.get_data_mut().loaded_libraries.push(loaded);
Ok(library_index)
}
fn apply_relocation(
uc: &mut Unicorn<'_, RuntimeState>,
base: u64,
relocation: &Reloc,
library_name: &str,
symbols: &[SymbolEntry],
) -> Result<(), VmError> {
if relocation.r_type == 0 {
return Ok(());
}
let relocation_addr = base.wrapping_add(relocation.r_offset);
let addend = relocation.r_addend.unwrap_or(0);
let symbol_address = if relocation.r_sym < symbols.len() {
symbols[relocation.r_sym].resolved
} else {
return Err(VmError::SymbolIndexOutOfRange {
library: library_name.to_string(),
index: relocation.r_sym,
});
};
let value = match relocation.r_type {
goblin::elf64::reloc::R_AARCH64_ABS64 | goblin::elf64::reloc::R_AARCH64_GLOB_DAT => {
add_i64(symbol_address, addend)
}
goblin::elf64::reloc::R_AARCH64_JUMP_SLOT => symbol_address,
goblin::elf64::reloc::R_AARCH64_RELATIVE => add_i64(base, addend),
other => return Err(VmError::UnsupportedRelocation(other)),
};
uc.mem_write(relocation_addr, &value.to_le_bytes())?;
Ok(())
}
pub(crate) fn resolve_symbol_from_loaded_library_by_name(
uc: &Unicorn<'_, RuntimeState>,
library_index: usize,
symbol_name: &str,
) -> Result<u64, VmError> {
let library = uc
.get_data()
.loaded_libraries
.get(library_index)
.ok_or(VmError::LibraryNotLoaded(library_index))?;
library
.symbols_by_name
.get(symbol_name)
.copied()
.ok_or_else(|| VmError::SymbolNotFound {
library: library.name.clone(),
symbol: symbol_name.to_string(),
})
}
pub(crate) fn read_c_string(
uc: &Unicorn<'_, RuntimeState>,
address: u64,
max_len: usize,
) -> Result<String, VmError> {
let bytes = uc.mem_read_as_vec(address, max_len)?;
let len = bytes
.iter()
.position(|byte| *byte == 0)
.ok_or(VmError::UnterminatedCString(address))?;
Ok(String::from_utf8_lossy(&bytes[..len]).into_owned())
}

50
src/errors.rs Normal file
View File

@@ -0,0 +1,50 @@
use thiserror::Error;
use unicorn_engine::unicorn_const::uc_error;
#[derive(Debug, Error)]
pub enum VmError {
#[error("unicorn error: {0:?}")]
Unicorn(uc_error),
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("elf parse error: {0}")]
Elf(#[from] goblin::error::Error),
#[error("allocator out of memory: base=0x{base:X} size=0x{size:X} request=0x{request:X}")]
AllocatorOom { base: u64, size: u64, request: u64 },
#[error("library not registered: {0}")]
LibraryNotRegistered(String),
#[error("library not loaded: {0}")]
LibraryNotLoaded(usize),
#[error("symbol not found: {symbol} in {library}")]
SymbolNotFound { library: String, symbol: String },
#[error("symbol index out of range: lib={library} index={index}")]
SymbolIndexOutOfRange { library: String, index: usize },
#[error("unsupported relocation type: {0}")]
UnsupportedRelocation(u32),
#[error("invalid ELF file range")]
InvalidElfRange,
#[error("unhandled import: {0}")]
UnhandledImport(String),
#[error("invalid import address: 0x{0:X}")]
InvalidImportAddress(u64),
#[error("invalid dlopen handle: {0}")]
InvalidDlopenHandle(u64),
#[error("invalid file descriptor: {0}")]
InvalidFileDescriptor(u64),
#[error("too many cdecl args: {0} (max 29)")]
TooManyArguments(usize),
#[error("adi call failed: {name} returned {code}")]
AdiCallFailed { name: &'static str, code: i32 },
#[error("unterminated C string at 0x{0:X}")]
UnterminatedCString(u64),
#[error("empty path")]
EmptyPath,
#[error("integer conversion failed for value: {0}")]
IntegerOverflow(u64),
}
impl From<uc_error> for VmError {
fn from(value: uc_error) -> Self {
Self::Unicorn(value)
}
}

452
src/exports.rs Normal file
View File

@@ -0,0 +1,452 @@
use std::cell::RefCell;
use std::ffi::{CStr, c_char};
use std::fs;
use std::path::Path;
use crate::{Adi, AdiInit, init_idbfs_for_path, sync_idbfs};
#[derive(Default)]
struct ExportState {
adi: Option<Adi>,
last_error: String,
cpim: Vec<u8>,
session: u32,
otp: Vec<u8>,
mid: Vec<u8>,
}
thread_local! {
static STATE: RefCell<ExportState> = RefCell::new(ExportState::default());
}
fn set_last_error(message: impl Into<String>) {
STATE.with(|state| {
state.borrow_mut().last_error = message.into();
});
}
fn clear_last_error() {
STATE.with(|state| {
state.borrow_mut().last_error.clear();
});
}
unsafe fn c_string(ptr: *const c_char) -> Result<String, String> {
if ptr.is_null() {
return Err("null C string pointer".to_string());
}
unsafe { CStr::from_ptr(ptr) }
.to_str()
.map(|s| s.to_string())
.map_err(|e| format!("invalid utf-8 string: {e}"))
}
unsafe fn optional_c_string(ptr: *const c_char) -> Result<Option<String>, String> {
if ptr.is_null() {
return Ok(None);
}
unsafe { c_string(ptr).map(Some) }
}
unsafe fn input_bytes(ptr: *const u8, len: usize) -> Result<Vec<u8>, String> {
if len == 0 {
return Ok(Vec::new());
}
if ptr.is_null() {
return Err("null bytes pointer with non-zero length".to_string());
}
Ok(unsafe { std::slice::from_raw_parts(ptr, len) }.to_vec())
}
fn with_adi_mut<T, F>(f: F) -> Result<T, String>
where
F: FnOnce(&mut Adi) -> Result<T, String>,
{
STATE.with(|state| {
let mut state = state.borrow_mut();
let adi = state
.adi
.as_mut()
.ok_or_else(|| "ADI is not initialized".to_string())?;
f(adi)
})
}
fn install_adi(adi: Adi) {
STATE.with(|state| {
let mut state = state.borrow_mut();
state.adi = Some(adi);
state.cpim.clear();
state.otp.clear();
state.mid.clear();
state.session = 0;
});
}
fn init_adi_from_parts(
storeservicescore: Vec<u8>,
coreadi: Vec<u8>,
library_path: String,
provisioning_path: Option<String>,
identifier: Option<String>,
) -> Result<(), String> {
let adi = Adi::new(AdiInit {
storeservicescore,
coreadi,
library_path,
provisioning_path,
identifier,
})
.map_err(|e| format!("ADI init failed: {e}"))?;
install_adi(adi);
Ok(())
}
#[unsafe(no_mangle)]
pub extern "C" fn anisette_init_from_files(
storeservices_path: *const c_char,
coreadi_path: *const c_char,
library_path: *const c_char,
provisioning_path: *const c_char,
identifier: *const c_char,
) -> i32 {
let result = (|| -> Result<(), String> {
let storeservices_path = unsafe { c_string(storeservices_path)? };
let coreadi_path = unsafe { c_string(coreadi_path)? };
let library_path = unsafe { c_string(library_path)? };
let provisioning_path = unsafe { optional_c_string(provisioning_path)? };
let identifier = unsafe { optional_c_string(identifier)? };
let storeservicescore = fs::read(&storeservices_path).map_err(|e| {
format!(
"failed to read storeservices core '{}': {e}",
storeservices_path
)
})?;
let coreadi = fs::read(&coreadi_path)
.map_err(|e| format!("failed to read coreadi '{}': {e}", coreadi_path))?;
init_adi_from_parts(
storeservicescore,
coreadi,
library_path,
provisioning_path,
identifier,
)
})();
match result {
Ok(()) => {
clear_last_error();
0
}
Err(err) => {
set_last_error(err);
-1
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn anisette_init_from_blobs(
storeservices_ptr: *const u8,
storeservices_len: usize,
coreadi_ptr: *const u8,
coreadi_len: usize,
library_path: *const c_char,
provisioning_path: *const c_char,
identifier: *const c_char,
) -> i32 {
let result = (|| -> Result<(), String> {
let storeservicescore = unsafe { input_bytes(storeservices_ptr, storeservices_len)? };
let coreadi = unsafe { input_bytes(coreadi_ptr, coreadi_len)? };
let library_path = unsafe { c_string(library_path)? };
let provisioning_path = unsafe { optional_c_string(provisioning_path)? };
let identifier = unsafe { optional_c_string(identifier)? };
init_adi_from_parts(
storeservicescore,
coreadi,
library_path,
provisioning_path,
identifier,
)
})();
match result {
Ok(()) => {
clear_last_error();
0
}
Err(err) => {
set_last_error(err);
-1
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn anisette_set_identifier(identifier: *const c_char) -> i32 {
let result = (|| -> Result<(), String> {
let identifier = unsafe { c_string(identifier)? };
with_adi_mut(|adi| adi.set_identifier(&identifier).map_err(|e| e.to_string()))
})();
match result {
Ok(()) => {
clear_last_error();
0
}
Err(err) => {
set_last_error(err);
-1
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn anisette_set_provisioning_path(path: *const c_char) -> i32 {
let result = (|| -> Result<(), String> {
let path = unsafe { c_string(path)? };
with_adi_mut(|adi| adi.set_provisioning_path(&path).map_err(|e| e.to_string()))
})();
match result {
Ok(()) => {
clear_last_error();
0
}
Err(err) => {
set_last_error(err);
-1
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn anisette_is_machine_provisioned(dsid: u64) -> i32 {
let result = (|| -> Result<i32, String> {
let mut out = -1;
with_adi_mut(|adi| {
let provisioned = adi
.is_machine_provisioned(dsid)
.map_err(|e| e.to_string())?;
out = if provisioned { 1 } else { 0 };
Ok(())
})?;
Ok(out)
})();
match result {
Ok(value) => {
clear_last_error();
value
}
Err(err) => {
set_last_error(err);
-1
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn anisette_start_provisioning(
dsid: u64,
spim_ptr: *const u8,
spim_len: usize,
) -> i32 {
let result = (|| -> Result<(), String> {
let spim = unsafe { input_bytes(spim_ptr, spim_len)? };
let out = with_adi_mut(|adi| {
adi.start_provisioning(dsid, &spim)
.map_err(|e| format!("start_provisioning failed: {e}"))
})?;
STATE.with(|state| {
let mut state = state.borrow_mut();
state.cpim = out.cpim;
state.session = out.session;
});
Ok(())
})();
match result {
Ok(()) => {
clear_last_error();
0
}
Err(err) => {
set_last_error(err);
-1
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn anisette_get_cpim_ptr() -> *const u8 {
STATE.with(|state| state.borrow().cpim.as_ptr())
}
#[unsafe(no_mangle)]
pub extern "C" fn anisette_get_cpim_len() -> usize {
STATE.with(|state| state.borrow().cpim.len())
}
#[unsafe(no_mangle)]
pub extern "C" fn anisette_get_session() -> u32 {
STATE.with(|state| state.borrow().session)
}
#[unsafe(no_mangle)]
pub extern "C" fn anisette_end_provisioning(
session: u32,
ptm_ptr: *const u8,
ptm_len: usize,
tk_ptr: *const u8,
tk_len: usize,
) -> i32 {
let result = (|| -> Result<(), String> {
let ptm = unsafe { input_bytes(ptm_ptr, ptm_len)? };
let tk = unsafe { input_bytes(tk_ptr, tk_len)? };
with_adi_mut(|adi| {
adi.end_provisioning(session, &ptm, &tk)
.map_err(|e| format!("end_provisioning failed: {e}"))
})
})();
match result {
Ok(()) => {
clear_last_error();
0
}
Err(err) => {
set_last_error(err);
-1
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn anisette_request_otp(dsid: u64) -> i32 {
let result = (|| -> Result<(), String> {
let out = with_adi_mut(|adi| {
adi.request_otp(dsid)
.map_err(|e| format!("request_otp failed: {e:#}"))
})?;
STATE.with(|state| {
let mut state = state.borrow_mut();
state.otp = out.otp;
state.mid = out.machine_id;
});
Ok(())
})();
match result {
Ok(()) => {
clear_last_error();
0
}
Err(err) => {
set_last_error(err);
-1
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn anisette_get_otp_ptr() -> *const u8 {
STATE.with(|state| state.borrow().otp.as_ptr())
}
#[unsafe(no_mangle)]
pub extern "C" fn anisette_get_otp_len() -> usize {
STATE.with(|state| state.borrow().otp.len())
}
#[unsafe(no_mangle)]
pub extern "C" fn anisette_get_mid_ptr() -> *const u8 {
STATE.with(|state| state.borrow().mid.as_ptr())
}
#[unsafe(no_mangle)]
pub extern "C" fn anisette_get_mid_len() -> usize {
STATE.with(|state| state.borrow().mid.len())
}
#[unsafe(no_mangle)]
pub extern "C" fn anisette_fs_write_file(
path: *const c_char,
data_ptr: *const u8,
data_len: usize,
) -> i32 {
let result = (|| -> Result<(), String> {
let path = unsafe { c_string(path)? };
let data = unsafe { input_bytes(data_ptr, data_len)? };
let path_ref = Path::new(&path);
if let Some(parent) = path_ref.parent()
&& !parent.as_os_str().is_empty() {
fs::create_dir_all(parent)
.map_err(|e| format!("failed to create dir '{}': {e}", parent.display()))?;
}
fs::write(&path, data).map_err(|e| format!("failed to write '{path}': {e}"))?;
Ok(())
})();
match result {
Ok(()) => {
clear_last_error();
0
}
Err(err) => {
set_last_error(err);
-1
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn anisette_idbfs_init(path: *const c_char) -> i32 {
let result = (|| -> Result<(), String> {
let path = unsafe { c_string(path)? };
init_idbfs_for_path(&path)?;
Ok(())
})();
match result {
Ok(()) => {
clear_last_error();
0
}
Err(err) => {
set_last_error(err);
-1
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn anisette_idbfs_sync(populate_from_storage: i32) -> i32 {
let result = sync_idbfs(populate_from_storage != 0);
match result {
Ok(()) => {
clear_last_error();
0
}
Err(err) => {
set_last_error(err);
-1
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn anisette_last_error_ptr() -> *const u8 {
STATE.with(|state| state.borrow().last_error.as_ptr())
}
#[unsafe(no_mangle)]
pub extern "C" fn anisette_last_error_len() -> usize {
STATE.with(|state| state.borrow().last_error.len())
}

82
src/idbfs.rs Normal file
View File

@@ -0,0 +1,82 @@
#[cfg(target_os = "emscripten")]
use std::ffi::CString;
fn normalize_mount_path(path: &str) -> String {
let trimmed = path.trim();
let no_slash = trimmed.trim_end_matches('/');
let no_dot = no_slash.strip_prefix("./").unwrap_or(no_slash);
if no_dot.is_empty() {
"/".to_string()
} else if no_dot.starts_with('/') {
no_dot.to_string()
} else {
format!("/{no_dot}")
}
}
#[cfg(target_os = "emscripten")]
unsafe extern "C" {
fn emscripten_run_script(script: *const core::ffi::c_char);
}
#[cfg(target_os = "emscripten")]
fn run_script(script: &str) -> Result<(), String> {
let script = CString::new(script).map_err(|e| format!("invalid JS script: {e}"))?;
unsafe {
emscripten_run_script(script.as_ptr());
}
Ok(())
}
#[cfg(not(target_os = "emscripten"))]
fn run_script(_script: &str) -> Result<(), String> {
Ok(())
}
pub fn init_idbfs_for_path(path: &str) -> Result<String, String> {
let mount_path = normalize_mount_path(path);
let script = format!(
r#"(function() {{
if (typeof FS === 'undefined' || typeof IDBFS === 'undefined') {{
console.warn('[anisette-rs] FS/IDBFS unavailable');
return;
}}
var mp = "{mount_path}";
try {{ FS.mkdirTree(mp); }} catch (_e) {{}}
try {{ FS.mount(IDBFS, {{}}, mp); }} catch (_e) {{}}
FS.syncfs(true, function(err) {{
if (err) {{
console.error('[anisette-rs] IDBFS initial sync failed', err);
}} else {{
console.log('[anisette-rs] IDBFS ready at ' + mp);
}}
}});
}})();"#,
);
run_script(&script)?;
Ok(mount_path)
}
pub fn sync_idbfs(populate_from_storage: bool) -> Result<(), String> {
let populate = if populate_from_storage {
"true"
} else {
"false"
};
let script = format!(
r#"(function() {{
if (typeof FS === 'undefined') {{
return;
}}
FS.syncfs({populate}, function(err) {{
if (err) {{
console.error('[anisette-rs] IDBFS sync failed', err);
}} else {{
console.log('[anisette-rs] IDBFS sync done');
}}
}});
}})();"#,
);
run_script(&script)
}

28
src/lib.rs Normal file
View File

@@ -0,0 +1,28 @@
pub mod device;
mod exports;
pub mod idbfs;
#[cfg(not(target_arch = "wasm32"))]
pub mod provisioning;
#[cfg(target_arch = "wasm32")]
mod provisioning_wasm;
mod adi;
mod allocator;
mod constants;
mod debug;
mod emu;
mod errors;
mod runtime;
mod stub;
mod util;
pub use adi::{Adi, AdiInit, OtpResult, ProvisioningStartResult};
pub use allocator::Allocator;
pub use device::{Device, DeviceData};
pub use emu::EmuCore;
pub use errors::VmError;
pub use idbfs::{init_idbfs_for_path, sync_idbfs};
#[cfg(not(target_arch = "wasm32"))]
pub use provisioning::ProvisioningSession;
#[cfg(target_arch = "wasm32")]
pub use provisioning_wasm::ProvisioningSession;

235
src/provisioning.rs Normal file
View File

@@ -0,0 +1,235 @@
use std::collections::HashMap;
use std::fmt::Write as _;
use std::fs;
use std::io::Cursor;
use std::path::{Path, PathBuf};
use std::time::Duration;
use anyhow::{Context, Result, anyhow, bail};
use base64::{Engine as _, engine::general_purpose::STANDARD};
use chrono::Local;
use plist::Value;
use reqwest::Certificate;
use reqwest::blocking::{Client, RequestBuilder};
use crate::Adi;
use crate::device::DeviceData;
pub struct ProvisioningSession<'a> {
adi: &'a mut Adi,
device: &'a DeviceData,
client: Client,
url_bag: HashMap<String, String>,
}
impl<'a> ProvisioningSession<'a> {
pub fn new(
adi: &'a mut Adi,
device: &'a DeviceData,
apple_root_pem: Option<PathBuf>,
) -> Result<Self> {
let client = build_http_client(apple_root_pem.as_deref())?;
Ok(Self {
adi,
device,
client,
url_bag: HashMap::new(),
})
}
pub fn provision(&mut self, dsid: u64) -> Result<()> {
println!("ProvisioningSession.provision");
if self.url_bag.is_empty() {
self.load_url_bag()?;
}
let start_url = self
.url_bag
.get("midStartProvisioning")
.cloned()
.ok_or_else(|| anyhow!("url bag missing midStartProvisioning"))?;
let finish_url = self
.url_bag
.get("midFinishProvisioning")
.cloned()
.ok_or_else(|| anyhow!("url bag missing midFinishProvisioning"))?;
let start_body = r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Header</key>
<dict/>
<key>Request</key>
<dict/>
</dict>
</plist>"#;
let start_bytes = self.post_with_time(&start_url, start_body)?;
let start_plist = parse_plist(&start_bytes)?;
let spim_b64 = plist_get_string_in_response(&start_plist, "spim")?;
println!("{spim_b64}");
let spim = STANDARD.decode(spim_b64.as_bytes())?;
let start = self.adi.start_provisioning(dsid, &spim)?;
println!("{}", bytes_to_hex(&start.cpim));
let cpim_b64 = STANDARD.encode(&start.cpim);
let finish_body = format!(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n <key>Header</key>\n <dict/>\n <key>Request</key>\n <dict>\n <key>cpim</key>\n <string>{}</string>\n </dict>\n</dict>\n</plist>",
cpim_b64
);
let finish_bytes = self.post_with_time(&finish_url, &finish_body)?;
let finish_plist = parse_plist(&finish_bytes)?;
let ptm_b64 = plist_get_string_in_response(&finish_plist, "ptm")?;
let tk_b64 = plist_get_string_in_response(&finish_plist, "tk")?;
let ptm = STANDARD.decode(ptm_b64.as_bytes())?;
let tk = STANDARD.decode(tk_b64.as_bytes())?;
self.adi.end_provisioning(start.session, &ptm, &tk)?;
Ok(())
}
fn load_url_bag(&mut self) -> Result<()> {
let bytes = self.get("https://gsa.apple.com/grandslam/GsService2/lookup")?;
let plist = parse_plist(&bytes)?;
let root = plist
.as_dictionary()
.ok_or_else(|| anyhow!("lookup plist root is not a dictionary"))?;
let urls = root
.get("urls")
.and_then(Value::as_dictionary)
.ok_or_else(|| anyhow!("lookup plist missing urls dictionary"))?;
self.url_bag.clear();
for (name, value) in urls {
if let Some(url) = value.as_string() {
self.url_bag.insert(name.to_string(), url.to_string());
}
}
Ok(())
}
fn get(&self, url: &str) -> Result<Vec<u8>> {
let request = self.with_common_headers(self.client.get(url), None);
let response = request.send()?.error_for_status()?;
Ok(response.bytes()?.to_vec())
}
fn post_with_time(&self, url: &str, body: &str) -> Result<Vec<u8>> {
let client_time = current_client_time();
let request = self.with_common_headers(
self.client.post(url).body(body.to_string()),
Some(&client_time),
);
let response = request.send()?.error_for_status()?;
Ok(response.bytes()?.to_vec())
}
fn with_common_headers(
&self,
request: RequestBuilder,
client_time: Option<&str>,
) -> RequestBuilder {
let mut request = request
.header("User-Agent", "akd/1.0 CFNetwork/1404.0.5 Darwin/22.3.0")
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Connection", "keep-alive")
.header("X-Mme-Device-Id", &self.device.unique_device_identifier)
.header(
"X-MMe-Client-Info",
&self.device.server_friendly_description,
)
.header("X-Apple-I-MD-LU", &self.device.local_user_uuid)
.header("X-Apple-Client-App-Name", "Setup");
if let Some(time) = client_time {
request = request.header("X-Apple-I-Client-Time", time);
}
request
}
}
fn bytes_to_hex(bytes: &[u8]) -> String {
let mut output = String::with_capacity(bytes.len() * 2);
for byte in bytes {
let _ = write!(output, "{byte:02x}");
}
output
}
fn build_http_client(apple_root_pem: Option<&Path>) -> Result<Client> {
let mut builder = Client::builder().timeout(Duration::from_secs(5));
if let Some(cert) = load_apple_root_cert(apple_root_pem)? {
builder = builder.add_root_certificate(cert);
} else {
eprintln!("warning: apple-root.pem not found, falling back to insecure TLS mode");
builder = builder.danger_accept_invalid_certs(true);
}
Ok(builder.build()?)
}
fn load_apple_root_cert(explicit_path: Option<&Path>) -> Result<Option<Certificate>> {
let mut candidates: Vec<PathBuf> = Vec::new();
if let Some(path) = explicit_path {
candidates.push(path.to_path_buf());
}
candidates.push(PathBuf::from("apple-root.pem"));
candidates.push(PathBuf::from(
"/Users/libr/Desktop/Life/Anisette.py/src/anisette/apple-root.pem",
));
for candidate in candidates {
if candidate.exists() {
let pem = fs::read(&candidate)
.with_context(|| format!("failed to read certificate {}", candidate.display()))?;
let cert = Certificate::from_pem(&pem)
.with_context(|| format!("invalid certificate pem {}", candidate.display()))?;
return Ok(Some(cert));
}
}
Ok(None)
}
fn parse_plist(bytes: &[u8]) -> Result<Value> {
Ok(Value::from_reader_xml(Cursor::new(bytes))?)
}
fn plist_get_string_in_response<'a>(plist: &'a Value, key: &str) -> Result<&'a str> {
let root = plist
.as_dictionary()
.ok_or_else(|| anyhow!("plist root is not a dictionary"))?;
let response = root
.get("Response")
.and_then(Value::as_dictionary)
.ok_or_else(|| anyhow!("plist missing Response dictionary"))?;
let value = response
.get(key)
.ok_or_else(|| anyhow!("plist Response missing {key}"))?;
if let Some(text) = value.as_string() {
return Ok(text);
}
bail!("plist Response field {key} is not a string")
}
fn current_client_time() -> String {
Local::now().format("%Y-%m-%dT%H:%M:%S%:z").to_string()
}

241
src/provisioning_wasm.rs Normal file
View File

@@ -0,0 +1,241 @@
use std::collections::HashMap;
use std::ffi::{CStr, CString};
use std::io::Cursor;
use std::path::PathBuf;
use anyhow::{Context, Result, anyhow, bail};
use base64::{Engine as _, engine::general_purpose::STANDARD};
use chrono::Utc;
use plist::Value;
use serde::Deserialize;
use serde_json::json;
use crate::Adi;
use crate::device::DeviceData;
#[derive(Debug, Deserialize)]
struct JsHttpResponse {
status: u16,
body: String,
#[serde(default)]
error: String,
}
pub struct ProvisioningSession<'a> {
adi: &'a mut Adi,
device: &'a DeviceData,
url_bag: HashMap<String, String>,
}
impl<'a> ProvisioningSession<'a> {
pub fn new(
adi: &'a mut Adi,
device: &'a DeviceData,
_apple_root_pem: Option<PathBuf>,
) -> Result<Self> {
Ok(Self {
adi,
device,
url_bag: HashMap::new(),
})
}
pub fn provision(&mut self, dsid: u64) -> Result<()> {
println!("ProvisioningSession.provision");
if self.url_bag.is_empty() {
self.load_url_bag()?;
}
let start_url = self
.url_bag
.get("midStartProvisioning")
.cloned()
.ok_or_else(|| anyhow!("url bag missing midStartProvisioning"))?;
let finish_url = self
.url_bag
.get("midFinishProvisioning")
.cloned()
.ok_or_else(|| anyhow!("url bag missing midFinishProvisioning"))?;
let start_body = r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Header</key>
<dict/>
<key>Request</key>
<dict/>
</dict>
</plist>"#;
let start_bytes = self.post_with_time(&start_url, start_body)?;
let start_plist = parse_plist(&start_bytes)?;
let spim_b64 = plist_get_string_in_response(&start_plist, "spim")?;
let spim = STANDARD.decode(spim_b64.as_bytes())?;
let start = self.adi.start_provisioning(dsid, &spim)?;
let cpim_b64 = STANDARD.encode(&start.cpim);
let finish_body = format!(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n <key>Header</key>\n <dict/>\n <key>Request</key>\n <dict>\n <key>cpim</key>\n <string>{}</string>\n </dict>\n</dict>\n</plist>",
cpim_b64
);
let finish_bytes = self.post_with_time(&finish_url, &finish_body)?;
let finish_plist = parse_plist(&finish_bytes)?;
let ptm_b64 = plist_get_string_in_response(&finish_plist, "ptm")?;
let tk_b64 = plist_get_string_in_response(&finish_plist, "tk")?;
let ptm = STANDARD.decode(ptm_b64.as_bytes())?;
let tk = STANDARD.decode(tk_b64.as_bytes())?;
self.adi.end_provisioning(start.session, &ptm, &tk)?;
Ok(())
}
fn load_url_bag(&mut self) -> Result<()> {
let bytes = self.get("https://gsa.apple.com/grandslam/GsService2/lookup")?;
let plist = parse_plist(&bytes)?;
let root = plist
.as_dictionary()
.ok_or_else(|| anyhow!("lookup plist root is not a dictionary"))?;
let urls = root
.get("urls")
.and_then(Value::as_dictionary)
.ok_or_else(|| anyhow!("lookup plist missing urls dictionary"))?;
self.url_bag.clear();
for (name, value) in urls {
if let Some(url) = value.as_string() {
self.url_bag.insert(name.to_string(), url.to_string());
}
}
Ok(())
}
fn get(&self, url: &str) -> Result<Vec<u8>> {
let request = json!({
"url": url,
"headers": self.common_headers(None),
});
self.call_http("anisette_http_get", request)
}
fn post_with_time(&self, url: &str, body: &str) -> Result<Vec<u8>> {
let client_time = current_client_time();
let request = json!({
"url": url,
"headers": self.common_headers(Some(&client_time)),
"body": body,
});
self.call_http("anisette_http_post", request)
}
fn common_headers(&self, client_time: Option<&str>) -> HashMap<&'static str, String> {
let mut headers = HashMap::from([
(
"User-Agent",
"akd/1.0 CFNetwork/1404.0.5 Darwin/22.3.0".to_string(),
),
(
"Content-Type",
"application/x-www-form-urlencoded".to_string(),
),
("Connection", "keep-alive".to_string()),
(
"X-Mme-Device-Id",
self.device.unique_device_identifier.clone(),
),
(
"X-MMe-Client-Info",
self.device.server_friendly_description.clone(),
),
("X-Apple-I-MD-LU", self.device.local_user_uuid.clone()),
("X-Apple-Client-App-Name", "Setup".to_string()),
]);
if let Some(time) = client_time {
headers.insert("X-Apple-I-Client-Time", time.to_string());
}
headers
}
fn call_http(&self, name: &str, payload: serde_json::Value) -> Result<Vec<u8>> {
// JS callback must return JSON: { status: number, body: base64, error?: string }.
let payload_json = serde_json::to_string(&payload)?;
let script = format!(
"(function(){{var fn = (typeof {name} === 'function') ? {name} : (typeof Module !== 'undefined' ? Module.{name} : null); return fn ? fn({payload_json}) : '';}})();"
);
let response_json = run_script_string(&script)?;
if response_json.trim().is_empty() {
bail!("missing JS http callback {name}");
}
let response: JsHttpResponse = serde_json::from_str(&response_json)
.with_context(|| format!("invalid JS http response for {name}"))?;
if !response.error.trim().is_empty() {
bail!("js http error: {}", response.error);
}
if response.status >= 400 {
bail!("js http status {} for {}", response.status, name);
}
let bytes = STANDARD
.decode(response.body.as_bytes())
.map_err(|e| anyhow!("base64 decode failed: {e}"))?;
Ok(bytes)
}
}
#[cfg(target_os = "emscripten")]
unsafe extern "C" {
fn emscripten_run_script_string(script: *const core::ffi::c_char) -> *mut core::ffi::c_char;
}
#[cfg(target_os = "emscripten")]
fn run_script_string(script: &str) -> Result<String> {
let script = CString::new(script).map_err(|e| anyhow!("invalid JS script: {e}"))?;
let ptr = unsafe { emscripten_run_script_string(script.as_ptr()) };
if ptr.is_null() {
return Err(anyhow!("emscripten_run_script_string returned null"));
}
let text = unsafe { CStr::from_ptr(ptr) }
.to_string_lossy()
.into_owned();
Ok(text)
}
fn parse_plist(bytes: &[u8]) -> Result<Value> {
Ok(Value::from_reader_xml(Cursor::new(bytes))?)
}
fn plist_get_string_in_response<'a>(plist: &'a Value, key: &str) -> Result<&'a str> {
let root = plist
.as_dictionary()
.ok_or_else(|| anyhow!("plist root is not a dictionary"))?;
let response = root
.get("Response")
.and_then(Value::as_dictionary)
.ok_or_else(|| anyhow!("plist missing Response dictionary"))?;
let value = response
.get(key)
.ok_or_else(|| anyhow!("plist Response missing {key}"))?;
if let Some(text) = value.as_string() {
return Ok(text);
}
bail!("plist Response field {key} is not a string")
}
fn current_client_time() -> String {
Utc::now().format("%Y-%m-%dT%H:%M:%S%:z").to_string()
}

47
src/runtime.rs Normal file
View File

@@ -0,0 +1,47 @@
use std::collections::HashMap;
use std::fs::File;
use crate::allocator::Allocator;
use crate::constants::{
LIB_ALLOC_BASE, LIB_ALLOC_SIZE, MALLOC_ADDRESS, MALLOC_SIZE, TEMP_ALLOC_BASE, TEMP_ALLOC_SIZE,
};
#[derive(Debug, Clone)]
pub(crate) struct SymbolEntry {
pub(crate) name: String,
pub(crate) resolved: u64,
}
#[derive(Debug, Clone)]
pub(crate) struct LoadedLibrary {
pub(crate) name: String,
pub(crate) symbols: Vec<SymbolEntry>,
pub(crate) symbols_by_name: HashMap<String, u64>,
}
#[derive(Debug)]
pub(crate) struct RuntimeState {
pub(crate) temp_allocator: Allocator,
pub(crate) library_allocator: Allocator,
pub(crate) malloc_allocator: Allocator,
pub(crate) errno_address: Option<u64>,
pub(crate) library_blobs: HashMap<String, Vec<u8>>,
pub(crate) loaded_libraries: Vec<LoadedLibrary>,
pub(crate) file_handles: Vec<Option<File>>,
pub(crate) library_root: Option<String>,
}
impl RuntimeState {
pub(crate) fn new() -> Self {
Self {
temp_allocator: Allocator::new(TEMP_ALLOC_BASE, TEMP_ALLOC_SIZE),
library_allocator: Allocator::new(LIB_ALLOC_BASE, LIB_ALLOC_SIZE),
malloc_allocator: Allocator::new(MALLOC_ADDRESS, MALLOC_SIZE),
errno_address: None,
library_blobs: HashMap::new(),
loaded_libraries: Vec::new(),
file_handles: Vec::new(),
library_root: None,
}
}
}

624
src/stub.rs Normal file
View File

@@ -0,0 +1,624 @@
use std::fs::{self, OpenOptions};
use std::io::{Read, Write};
use std::time::{SystemTime, UNIX_EPOCH};
use unicorn_engine::{RegisterARM64, Unicorn};
use crate::constants::{
ENOENT, IMPORT_ADDRESS, IMPORT_LIBRARY_STRIDE, O_ACCMODE, O_CREAT, O_NOFOLLOW, O_RDWR, O_WRONLY,
};
use crate::debug::{debug_print, debug_trace};
use crate::emu::{
ensure_errno_address, load_library_by_name, read_c_string,
resolve_symbol_from_loaded_library_by_name, set_errno,
};
use crate::errors::VmError;
use crate::runtime::RuntimeState;
use crate::util::bytes_to_hex;
pub fn dispatch_import_stub(
uc: &mut Unicorn<'_, RuntimeState>,
address: u64,
) -> Result<(), VmError> {
if address < IMPORT_ADDRESS {
return Err(VmError::InvalidImportAddress(address));
}
let offset = address - IMPORT_ADDRESS;
let library_index = (offset / IMPORT_LIBRARY_STRIDE) as usize;
let symbol_index = ((offset % IMPORT_LIBRARY_STRIDE) / 4) as usize;
let symbol_name =
{
let state = uc.get_data();
let library = state
.loaded_libraries
.get(library_index)
.ok_or(VmError::LibraryNotLoaded(library_index))?;
let symbol = library.symbols.get(symbol_index).ok_or_else(|| {
VmError::SymbolIndexOutOfRange {
library: library.name.clone(),
index: symbol_index,
}
})?;
symbol.name.clone()
};
handle_stub_by_name(uc, &symbol_name)
}
fn handle_stub_by_name(
uc: &mut Unicorn<'_, RuntimeState>,
symbol_name: &str,
) -> Result<(), VmError> {
match symbol_name {
"malloc" => stub_malloc(uc),
"free" => stub_free(uc),
"strncpy" => stub_strncpy(uc),
"mkdir" => stub_mkdir(uc),
"umask" => stub_umask(uc),
"chmod" => stub_chmod(uc),
"lstat" => stub_lstat(uc),
"fstat" => stub_fstat(uc),
"open" => stub_open(uc),
"ftruncate" => stub_ftruncate(uc),
"read" => stub_read(uc),
"write" => stub_write(uc),
"close" => stub_close(uc),
"dlopen" => stub_dlopen(uc),
"dlsym" => stub_dlsym(uc),
"dlclose" => stub_dlclose(uc),
"pthread_once" => stub_return_zero(uc),
"pthread_create" => stub_return_zero(uc),
"pthread_mutex_lock" => stub_return_zero(uc),
"pthread_rwlock_unlock" => stub_return_zero(uc),
"pthread_rwlock_destroy" => stub_return_zero(uc),
"pthread_rwlock_wrlock" => stub_return_zero(uc),
"pthread_rwlock_init" => stub_return_zero(uc),
"pthread_mutex_unlock" => stub_return_zero(uc),
"pthread_rwlock_rdlock" => stub_return_zero(uc),
"gettimeofday" => stub_gettimeofday(uc),
"__errno" => stub_errno_location(uc),
"__system_property_get" => stub_system_property_get(uc),
"arc4random" => stub_arc4random(uc),
other => {
debug_print(other);
Err(VmError::UnhandledImport(other.to_string()))
}
}
}
fn stub_return_zero(uc: &mut Unicorn<'_, RuntimeState>) -> Result<(), VmError> {
uc.reg_write(RegisterARM64::X0, 0)?;
Ok(())
}
fn stub_malloc(uc: &mut Unicorn<'_, RuntimeState>) -> Result<(), VmError> {
let request = uc.reg_read(RegisterARM64::X0)?;
let address = {
let state = uc.get_data_mut();
state.malloc_allocator.alloc(request)?
};
debug_trace(format!("malloc(0x{request:X})=0x{address:X}"));
uc.reg_write(RegisterARM64::X0, address)?;
Ok(())
}
fn stub_free(uc: &mut Unicorn<'_, RuntimeState>) -> Result<(), VmError> {
uc.reg_write(RegisterARM64::X0, 0)?;
Ok(())
}
fn stub_strncpy(uc: &mut Unicorn<'_, RuntimeState>) -> Result<(), VmError> {
let dst = uc.reg_read(RegisterARM64::X0)?;
let src = uc.reg_read(RegisterARM64::X1)?;
let length = uc.reg_read(RegisterARM64::X2)? as usize;
let input = uc.mem_read_as_vec(src, length)?;
let copy_len = input
.iter()
.position(|byte| *byte == 0)
.unwrap_or(length)
.min(length);
let mut output = vec![0_u8; length];
output[..copy_len].copy_from_slice(&input[..copy_len]);
uc.mem_write(dst, &output)?;
uc.reg_write(RegisterARM64::X0, dst)?;
Ok(())
}
fn stub_mkdir(uc: &mut Unicorn<'_, RuntimeState>) -> Result<(), VmError> {
let path_ptr = uc.reg_read(RegisterARM64::X0)?;
let mode = uc.reg_read(RegisterARM64::X1)?;
let path = read_c_string(uc, path_ptr, 0x1000)?;
debug_trace(format!("mkdir('{path}', {mode:#o})"));
// Only allow creating ./anisette directory (matches Python reference impl)
if path != "./anisette" {
debug_print(format!("mkdir: rejecting invalid path '{path}'"));
set_errno(uc, ENOENT)?;
uc.reg_write(RegisterARM64::X0, u64::MAX)?;
return Ok(());
}
match fs::create_dir_all(&path) {
Ok(()) => {
uc.reg_write(RegisterARM64::X0, 0)?;
}
Err(_) => {
set_errno(uc, ENOENT)?;
uc.reg_write(RegisterARM64::X0, u64::MAX)?;
}
}
Ok(())
}
fn stub_umask(uc: &mut Unicorn<'_, RuntimeState>) -> Result<(), VmError> {
uc.reg_write(RegisterARM64::X0, 0o777)?;
Ok(())
}
fn stub_chmod(uc: &mut Unicorn<'_, RuntimeState>) -> Result<(), VmError> {
let path_ptr = uc.reg_read(RegisterARM64::X0)?;
let mode = uc.reg_read(RegisterARM64::X1)?;
let path = read_c_string(uc, path_ptr, 0x1000)?;
debug_trace(format!("chmod('{path}', {mode:#o})"));
uc.reg_write(RegisterARM64::X0, 0)?;
Ok(())
}
fn build_python_stat_bytes(mode: u32, size: u64) -> Vec<u8> {
let mut stat = Vec::with_capacity(128);
stat.extend_from_slice(&[0_u8; 8]); // st_dev
stat.extend_from_slice(&[0_u8; 8]); // st_ino
stat.extend_from_slice(&mode.to_le_bytes()); // st_mode
stat.extend_from_slice(&[0_u8; 4]); // st_nlink
stat.extend_from_slice(&[0xA4, 0x81, 0x00, 0x00]); // st_uid
stat.extend_from_slice(&[0_u8; 4]); // st_gid
stat.extend_from_slice(&[0_u8; 8]); // st_rdev
stat.extend_from_slice(&[0_u8; 8]); // __pad1
stat.extend_from_slice(&size.to_le_bytes()); // st_size
stat.extend_from_slice(&[0_u8; 4]); // st_blksize
stat.extend_from_slice(&[0_u8; 4]); // __pad2
stat.extend_from_slice(&[0_u8; 8]); // st_blocks
stat.extend_from_slice(&[0_u8; 8]); // st_atime
stat.extend_from_slice(&[0_u8; 8]); // st_atime_nsec
stat.extend_from_slice(&[0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00]); // st_mtime
stat.extend_from_slice(&[0_u8; 8]); // st_mtime_nsec
stat.extend_from_slice(&[0_u8; 8]); // st_ctime
stat.extend_from_slice(&[0_u8; 8]); // st_ctime_nsec
stat.extend_from_slice(&[0_u8; 4]); // __unused4
stat.extend_from_slice(&[0_u8; 4]); // __unused5
stat
}
fn write_python_stat(
uc: &mut Unicorn<'_, RuntimeState>,
out_ptr: u64,
mode: u32,
size: u64,
stat_blksize: u64,
stat_blocks: u64,
) -> Result<(), VmError> {
debug_print(format!("{size} {stat_blksize} {stat_blocks}"));
let fake_blksize = 512_u64;
let fake_blocks = size.div_ceil(512);
debug_print(format!("{size} {fake_blksize} {fake_blocks}"));
debug_print(format!("0x{mode:X} = {mode}"));
let stat_bytes = build_python_stat_bytes(mode, size);
debug_print(format!("{}", stat_bytes.len()));
debug_print(format!("Write to ptr: 0x{out_ptr:X}"));
uc.mem_write(out_ptr, &stat_bytes)?;
debug_print("Stat struct written to guest memory");
Ok(())
}
fn stat_path_into_guest(
uc: &mut Unicorn<'_, RuntimeState>,
path: &str,
out_ptr: u64,
) -> Result<(), VmError> {
let metadata = match fs::symlink_metadata(path) {
Ok(metadata) => metadata,
Err(_) => {
debug_print(format!("Unable to stat '{path}'"));
set_errno(uc, ENOENT)?;
uc.reg_write(RegisterARM64::X0, u64::MAX)?;
return Ok(());
}
};
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
write_python_stat(
uc,
out_ptr,
metadata.mode(),
metadata.size(),
metadata.blksize(),
metadata.blocks(),
)?;
}
#[cfg(not(unix))]
{
write_python_stat(uc, out_ptr, 0, metadata.len(), 0, 0)?;
}
uc.reg_write(RegisterARM64::X0, 0)?;
Ok(())
}
fn stat_fd_into_guest(
uc: &mut Unicorn<'_, RuntimeState>,
fd: u64,
out_ptr: u64,
) -> Result<(), VmError> {
let fd_index = usize::try_from(fd).map_err(|_| VmError::InvalidFileDescriptor(fd))?;
let metadata = {
let state = uc.get_data_mut();
let slot = state
.file_handles
.get_mut(fd_index)
.ok_or(VmError::InvalidFileDescriptor(fd))?;
let file = slot.as_mut().ok_or(VmError::InvalidFileDescriptor(fd))?;
file.metadata()
};
let metadata = match metadata {
Ok(metadata) => metadata,
Err(_) => {
debug_print(format!("Unable to stat '{fd}'"));
set_errno(uc, ENOENT)?;
uc.reg_write(RegisterARM64::X0, u64::MAX)?;
return Ok(());
}
};
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
write_python_stat(
uc,
out_ptr,
metadata.mode(),
metadata.size(),
metadata.blksize(),
metadata.blocks(),
)?;
}
#[cfg(not(unix))]
{
write_python_stat(uc, out_ptr, 0, metadata.len(), 0, 0)?;
}
uc.reg_write(RegisterARM64::X0, 0)?;
Ok(())
}
fn stub_lstat(uc: &mut Unicorn<'_, RuntimeState>) -> Result<(), VmError> {
let path_ptr = uc.reg_read(RegisterARM64::X0)?;
let out_ptr = uc.reg_read(RegisterARM64::X1)?;
let path = read_c_string(uc, path_ptr, 0x1000)?;
debug_trace(format!(
"lstat(0x{path_ptr:X}:'{path}', [x1:0x{out_ptr:X}])"
));
stat_path_into_guest(uc, &path, out_ptr)
}
fn stub_fstat(uc: &mut Unicorn<'_, RuntimeState>) -> Result<(), VmError> {
let fd = uc.reg_read(RegisterARM64::X0)?;
let out_ptr = uc.reg_read(RegisterARM64::X1)?;
debug_trace(format!("fstat({fd}, [...])"));
stat_fd_into_guest(uc, fd, out_ptr)
}
fn stub_open(uc: &mut Unicorn<'_, RuntimeState>) -> Result<(), VmError> {
let path_ptr = uc.reg_read(RegisterARM64::X0)?;
let flags = uc.reg_read(RegisterARM64::X1)?;
let mode = uc.reg_read(RegisterARM64::X2)?;
let path = read_c_string(uc, path_ptr, 0x1000)?;
if path.is_empty() {
return Err(VmError::EmptyPath);
}
debug_trace(format!("open('{path}', {flags:#o}, {mode:#o})"));
// Only allow access to ./anisette/adi.pb (matches Python reference impl)
if path != "./anisette/adi.pb" {
debug_print(format!("open: rejecting invalid path '{path}'"));
set_errno(uc, ENOENT)?;
uc.reg_write(RegisterARM64::X0, u64::MAX)?;
return Ok(());
}
if flags != O_NOFOLLOW && flags != (O_NOFOLLOW | O_CREAT | O_WRONLY) {
debug_print(format!("open: rejecting unsupported flags {flags:#o}"));
set_errno(uc, ENOENT)?;
uc.reg_write(RegisterARM64::X0, u64::MAX)?;
return Ok(());
}
let mut options = OpenOptions::new();
let access_mode = flags & O_ACCMODE;
let _write_only = access_mode == O_WRONLY;
let create = (flags & O_CREAT) != 0;
match access_mode {
0 => {
options.read(true);
}
O_WRONLY => {
options.write(true).truncate(true);
}
O_RDWR => {
options.read(true).write(true);
}
_ => {
set_errno(uc, ENOENT)?;
uc.reg_write(RegisterARM64::X0, u64::MAX)?;
return Ok(());
}
}
if create {
options.create(true).read(true).write(true);
if let Some(parent) = std::path::Path::new(&path).parent() {
let _ = fs::create_dir_all(parent);
}
}
if (flags & O_NOFOLLOW) == 0 {
debug_trace("open without O_NOFOLLOW");
}
match options.open(&path) {
Ok(file) => {
let fd = {
let state = uc.get_data_mut();
state.file_handles.push(Some(file));
(state.file_handles.len() - 1) as u64
};
uc.reg_write(RegisterARM64::X0, fd)?;
}
Err(_) => {
set_errno(uc, ENOENT)?;
uc.reg_write(RegisterARM64::X0, u64::MAX)?;
}
}
Ok(())
}
fn stub_ftruncate(uc: &mut Unicorn<'_, RuntimeState>) -> Result<(), VmError> {
let fd = uc.reg_read(RegisterARM64::X0)?;
let length = uc.reg_read(RegisterARM64::X1)?;
debug_trace(format!("ftruncate({fd}, {length})"));
let fd_index = usize::try_from(fd).map_err(|_| VmError::InvalidFileDescriptor(fd))?;
let result = {
let state = uc.get_data_mut();
let slot = state
.file_handles
.get_mut(fd_index)
.ok_or(VmError::InvalidFileDescriptor(fd))?;
let file = slot.as_mut().ok_or(VmError::InvalidFileDescriptor(fd))?;
file.set_len(length)
};
match result {
Ok(()) => uc.reg_write(RegisterARM64::X0, 0)?,
Err(_) => {
set_errno(uc, ENOENT)?;
uc.reg_write(RegisterARM64::X0, u64::MAX)?;
}
}
Ok(())
}
fn stub_read(uc: &mut Unicorn<'_, RuntimeState>) -> Result<(), VmError> {
let fd = uc.reg_read(RegisterARM64::X0)?;
let buf_ptr = uc.reg_read(RegisterARM64::X1)?;
let count = uc.reg_read(RegisterARM64::X2)? as usize;
let fd_index = usize::try_from(fd).map_err(|_| VmError::InvalidFileDescriptor(fd))?;
let mut buffer = vec![0_u8; count];
let read_size = {
let state = uc.get_data_mut();
let slot = state
.file_handles
.get_mut(fd_index)
.ok_or(VmError::InvalidFileDescriptor(fd))?;
let file = slot.as_mut().ok_or(VmError::InvalidFileDescriptor(fd))?;
file.read(&mut buffer)
};
debug_trace(format!("read({fd}, 0x{buf_ptr:X}, {count})={read_size:?}"));
match read_size {
Ok(read_size) => {
uc.mem_write(buf_ptr, &buffer[..read_size])?;
uc.reg_write(RegisterARM64::X0, read_size as u64)?;
}
Err(_) => {
set_errno(uc, ENOENT)?;
uc.reg_write(RegisterARM64::X0, u64::MAX)?;
}
}
Ok(())
}
fn stub_write(uc: &mut Unicorn<'_, RuntimeState>) -> Result<(), VmError> {
let fd = uc.reg_read(RegisterARM64::X0)?;
let buf_ptr = uc.reg_read(RegisterARM64::X1)?;
let count = uc.reg_read(RegisterARM64::X2)? as usize;
debug_trace(format!("write({fd}, 0x{buf_ptr:X}, {count})"));
let fd_index = usize::try_from(fd).map_err(|_| VmError::InvalidFileDescriptor(fd))?;
let bytes = uc.mem_read_as_vec(buf_ptr, count)?;
let write_size = {
let state = uc.get_data_mut();
let slot = state
.file_handles
.get_mut(fd_index)
.ok_or(VmError::InvalidFileDescriptor(fd))?;
let file = slot.as_mut().ok_or(VmError::InvalidFileDescriptor(fd))?;
file.write_all(&bytes)
};
match write_size {
Ok(()) => uc.reg_write(RegisterARM64::X0, count as u64)?,
Err(_) => {
set_errno(uc, ENOENT)?;
uc.reg_write(RegisterARM64::X0, u64::MAX)?;
}
}
Ok(())
}
fn stub_close(uc: &mut Unicorn<'_, RuntimeState>) -> Result<(), VmError> {
let fd = uc.reg_read(RegisterARM64::X0)?;
let fd_index = usize::try_from(fd).map_err(|_| VmError::InvalidFileDescriptor(fd))?;
let state = uc.get_data_mut();
let slot = state
.file_handles
.get_mut(fd_index)
.ok_or(VmError::InvalidFileDescriptor(fd))?;
*slot = None;
uc.reg_write(RegisterARM64::X0, 0)?;
Ok(())
}
fn stub_dlopen(uc: &mut Unicorn<'_, RuntimeState>) -> Result<(), VmError> {
let path_ptr = uc.reg_read(RegisterARM64::X0)?;
let path = read_c_string(uc, path_ptr, 0x1000)?;
let library_name = path.rsplit('/').next().ok_or(VmError::EmptyPath)?;
debug_trace(format!("dlopen('{path}' ({library_name}))"));
let library_index = load_library_by_name(uc, library_name)?;
uc.reg_write(RegisterARM64::X0, (library_index + 1) as u64)?;
Ok(())
}
fn stub_dlsym(uc: &mut Unicorn<'_, RuntimeState>) -> Result<(), VmError> {
let handle = uc.reg_read(RegisterARM64::X0)?;
if handle == 0 {
return Err(VmError::InvalidDlopenHandle(handle));
}
let symbol_ptr = uc.reg_read(RegisterARM64::X1)?;
let symbol_name = read_c_string(uc, symbol_ptr, 0x1000)?;
let library_index = (handle - 1) as usize;
{
let state = uc.get_data();
if let Some(library) = state.loaded_libraries.get(library_index) {
debug_trace(format!(
"dlsym({handle:X} ({}), '{}')",
library.name, symbol_name
));
}
}
let symbol_address =
resolve_symbol_from_loaded_library_by_name(uc, library_index, &symbol_name)?;
debug_print(format!("Found at 0x{symbol_address:X}"));
uc.reg_write(RegisterARM64::X0, symbol_address)?;
Ok(())
}
fn stub_dlclose(uc: &mut Unicorn<'_, RuntimeState>) -> Result<(), VmError> {
uc.reg_write(RegisterARM64::X0, 0)?;
Ok(())
}
fn stub_gettimeofday(uc: &mut Unicorn<'_, RuntimeState>) -> Result<(), VmError> {
let time_ptr = uc.reg_read(RegisterARM64::X0)?;
let tz_ptr = uc.reg_read(RegisterARM64::X1)?;
debug_trace(format!("gettimeofday(0x{time_ptr:X}, 0x{tz_ptr:X})"));
if tz_ptr != 0 {
return Err(VmError::UnhandledImport(format!(
"gettimeofday tz pointer must be null, got 0x{tz_ptr:X}"
)));
}
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
let sec = now.as_secs();
let usec = now.subsec_micros() as i64;
let mut timeval = [0_u8; 16];
timeval[0..8].copy_from_slice(&sec.to_le_bytes());
timeval[8..16].copy_from_slice(&usec.to_le_bytes());
debug_print(format!(
"{{'tv_sec': {sec}, 'tv_usec': {usec}}} {} {}",
bytes_to_hex(&timeval),
timeval.len()
));
uc.mem_write(time_ptr, &timeval)?;
uc.reg_write(RegisterARM64::X0, 0)?;
Ok(())
}
fn stub_errno_location(uc: &mut Unicorn<'_, RuntimeState>) -> Result<(), VmError> {
if uc.get_data().errno_address.is_none() {
debug_print("Checking errno before first error (!)");
}
let errno_address = ensure_errno_address(uc)?;
uc.reg_write(RegisterARM64::X0, errno_address)?;
Ok(())
}
fn stub_system_property_get(uc: &mut Unicorn<'_, RuntimeState>) -> Result<(), VmError> {
let name_ptr = uc.reg_read(RegisterARM64::X0)?;
let name = read_c_string(uc, name_ptr, 0x1000)?;
debug_trace(format!("__system_property_get({name}, [...])"));
let value_ptr = uc.reg_read(RegisterARM64::X1)?;
let value = b"no s/n number";
uc.mem_write(value_ptr, value)?;
uc.reg_write(RegisterARM64::X0, value.len() as u64)?;
Ok(())
}
fn stub_arc4random(uc: &mut Unicorn<'_, RuntimeState>) -> Result<(), VmError> {
uc.reg_write(RegisterARM64::X0, 0xDEAD_BEEF)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::Allocator;
#[test]
fn allocator_aligns_to_pages() {
let mut allocator = Allocator::new(0x1000_0000, 0x20_000);
let a = allocator.alloc(1).expect("alloc 1");
let b = allocator.alloc(0x1500).expect("alloc 2");
assert_eq!(a, 0x1000_0000);
assert_eq!(b, 0x1000_1000);
}
}

37
src/util.rs Normal file
View File

@@ -0,0 +1,37 @@
use std::fmt::Write as _;
use crate::errors::VmError;
pub(crate) fn bytes_to_hex(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len() * 2);
for byte in bytes {
let _ = write!(out, "{byte:02x}");
}
out
}
pub(crate) fn align_up(value: u64, align: u64) -> u64 {
if align == 0 {
return value;
}
(value + align - 1) & !(align - 1)
}
pub(crate) fn align_down(value: u64, align: u64) -> u64 {
if align == 0 {
return value;
}
value & !(align - 1)
}
pub(crate) fn add_i64(base: u64, addend: i64) -> u64 {
if addend >= 0 {
base.wrapping_add(addend as u64)
} else {
base.wrapping_sub((-addend) as u64)
}
}
pub(crate) fn as_usize(value: u64) -> Result<usize, VmError> {
usize::try_from(value).map_err(|_| VmError::IntegerOverflow(value))
}