init
This commit is contained in:
15
.cargo/config.toml
Normal file
15
.cargo/config.toml
Normal 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",
|
||||
]
|
||||
10
.cargo/pkgconfig/unicorn.pc
Normal file
10
.cargo/pkgconfig/unicorn.pc
Normal 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
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
target/
|
||||
test/
|
||||
27
Cargo.toml
Normal file
27
Cargo.toml
Normal 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
13
README.md
Normal 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
62
build.rs
Normal 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
143
bun.lock
Normal 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
89
example/anisette.rs
Normal 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
790
example/browser-run.js
Normal 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
260
example/run-node.mjs
Normal 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
9
package.json
Normal 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
99
script/build-glue.sh
Executable 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"
|
||||
66
script/patches/ffi.inc.c.diff
Normal file
66
script/patches/ffi.inc.c.diff
Normal 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
191
script/patches/ffi.rs.diff
Normal 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);
|
||||
16
script/patches/translate-all.c.diff
Normal file
16
script/patches/translate-all.c.diff
Normal 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
54
script/rebuild-unicorn.sh
Executable 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
243
src/adi.rs
Normal 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
50
src/allocator.rs
Normal 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
67
src/constants.rs
Normal 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
113
src/debug.rs
Normal 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
92
src/device.rs
Normal 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
428
src/emu.rs
Normal 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
50
src/errors.rs
Normal 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
452
src/exports.rs
Normal 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
82
src/idbfs.rs
Normal 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
28
src/lib.rs
Normal 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
235
src/provisioning.rs
Normal 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
241
src/provisioning_wasm.rs
Normal 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
47
src/runtime.rs
Normal 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
624
src/stub.rs
Normal 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
37
src/util.rs
Normal 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))
|
||||
}
|
||||
Reference in New Issue
Block a user