commit 3339111ff2ac4eeaf7e570f724611772da893d1d Author: libr Date: Thu Feb 26 16:59:30 2026 +0800 init diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..a3fbba0 --- /dev/null +++ b/.cargo/config.toml @@ -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", +] diff --git a/.cargo/pkgconfig/unicorn.pc b/.cargo/pkgconfig/unicorn.pc new file mode 100644 index 0000000..48f2de2 --- /dev/null +++ b/.cargo/pkgconfig/unicorn.pc @@ -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} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..383386b --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target/ +test/ \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..fb7ee01 --- /dev/null +++ b/Cargo.toml @@ -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"] } diff --git a/README.md b/README.md new file mode 100644 index 0000000..2bcbc45 --- /dev/null +++ b/README.md @@ -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. + + + + diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..2455547 --- /dev/null +++ b/build.rs @@ -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}"); + } +} diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..55bfa6b --- /dev/null +++ b/bun.lock @@ -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=="], + } +} diff --git a/example/anisette.rs b/example/anisette.rs new file mode 100644 index 0000000..0a19fa1 --- /dev/null +++ b/example/anisette.rs @@ -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 -- [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(()) +} diff --git a/example/browser-run.js b/example/browser-run.js new file mode 100644 index 0000000..7bdc7b4 --- /dev/null +++ b/example/browser-run.js @@ -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 = ' '; + + // 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 = ` + + + + Header + + Request + + +`; + + 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 = ` + + + + Header + + Request + + cpim + ${bytesToBase64(cpim)} + + +`; + + 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(); diff --git a/example/run-node.mjs b/example/run-node.mjs new file mode 100644 index 0000000..9d7efe4 --- /dev/null +++ b/example/run-node.mjs @@ -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 [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)); +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..576c450 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/script/build-glue.sh b/script/build-glue.sh new file mode 100755 index 0000000..73f1d86 --- /dev/null +++ b/script/build-glue.sh @@ -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" diff --git a/script/patches/ffi.inc.c.diff b/script/patches/ffi.inc.c.diff new file mode 100644 index 0000000..e22c20e --- /dev/null +++ b/script/patches/ffi.inc.c.diff @@ -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++) { diff --git a/script/patches/ffi.rs.diff b/script/patches/ffi.rs.diff new file mode 100644 index 0000000..a21180f --- /dev/null +++ b/script/patches/ffi.rs.diff @@ -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( + where + F: FnMut(&mut crate::Unicorn, 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( + ) where + F: FnMut(&mut crate::Unicorn, 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( + ) where + F: FnMut(&mut crate::Unicorn, 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( + ) where + F: FnMut(&mut crate::Unicorn, 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( + where + F: FnMut(&mut crate::Unicorn, 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( + ) where + F: FnMut(&mut crate::Unicorn, 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( + where + F: FnMut(&mut crate::Unicorn, 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( + where + F: FnMut(&mut crate::Unicorn) -> 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( + ) where + F: FnMut(&mut crate::Unicorn, 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(uc: uc_handle, user_data: *mu + where + F: FnMut(&mut crate::Unicorn), + { ++ 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( + where + F: FnMut(&mut crate::Unicorn, u64, MemType) -> Option, + { ++ 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); diff --git a/script/patches/translate-all.c.diff b/script/patches/translate-all.c.diff new file mode 100644 index 0000000..c6b6ca7 --- /dev/null +++ b/script/patches/translate-all.c.diff @@ -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) diff --git a/script/rebuild-unicorn.sh b/script/rebuild-unicorn.sh new file mode 100755 index 0000000..7484675 --- /dev/null +++ b/script/rebuild-unicorn.sh @@ -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}" diff --git a/src/adi.rs b/src/adi.rs new file mode 100644 index 0000000..a250936 --- /dev/null +++ b/src/adi.rs @@ -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, + pub coreadi: Vec, + pub library_path: String, + pub provisioning_path: Option, + pub identifier: Option, +} + +pub struct ProvisioningStartResult { + pub cpim: Vec, + pub session: u32, +} + +pub struct OtpResult { + pub otp: Vec, + pub machine_id: Vec, +} + +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 { + 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 { + 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 { + 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 { + 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 }) + } +} diff --git a/src/allocator.rs b/src/allocator.rs new file mode 100644 index 0000000..076dd49 --- /dev/null +++ b/src/allocator.rs @@ -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 { + 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); + } +} diff --git a/src/constants.rs b/src/constants.rs new file mode 100644 index 0000000..d44f9cc --- /dev/null +++ b/src/constants.rs @@ -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; diff --git a/src/debug.rs b/src/debug.rs new file mode 100644 index 0000000..5ed529d --- /dev/null +++ b/src/debug.rs @@ -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) { + if DEBUG_PRINT_ENABLED { + println!("{}", message.as_ref()); + } +} + +pub(crate) fn debug_trace(message: impl AsRef) { + 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); + } +} + diff --git a/src/device.rs b/src/device.rs new file mode 100644 index 0000000..c729292 --- /dev/null +++ b/src/device.rs @@ -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 = + " "; + +#[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) -> Result { + 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 +} diff --git a/src/emu.rs b/src/emu.rs new file mode 100644 index 0000000..04cae70 --- /dev/null +++ b/src/emu.rs @@ -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 { + 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, data: Vec) { + 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 { + load_library_by_name(&mut self.uc, library_name) + } + + pub fn resolve_symbol_by_name( + &self, + library_index: usize, + symbol_name: &str, + ) -> Result { + resolve_symbol_from_loaded_library_by_name(&self.uc, library_index, symbol_name) + } + + pub fn invoke_cdecl(&mut self, address: u64, args: &[u64]) -> Result { + 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 { + alloc_temp_bytes(&mut self.uc, data, 0xCC) + } + + pub fn alloc_temporary(&mut self, length: usize) -> Result { + 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, 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 { + 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 { + 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 { + read_c_string(&self.uc, address, max_len) + } +} + +pub(crate) fn alloc_c_string(core: &mut EmuCore, value: &str) -> Result { + 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 { + 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 { + 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 { + 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 { + 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 { + 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()) +} diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..f6556c5 --- /dev/null +++ b/src/errors.rs @@ -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 for VmError { + fn from(value: uc_error) -> Self { + Self::Unicorn(value) + } +} diff --git a/src/exports.rs b/src/exports.rs new file mode 100644 index 0000000..5e3fabe --- /dev/null +++ b/src/exports.rs @@ -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, + last_error: String, + cpim: Vec, + session: u32, + otp: Vec, + mid: Vec, +} + +thread_local! { + static STATE: RefCell = RefCell::new(ExportState::default()); +} + +fn set_last_error(message: impl Into) { + 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 { + 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, String> { + if ptr.is_null() { + return Ok(None); + } + unsafe { c_string(ptr).map(Some) } +} + +unsafe fn input_bytes(ptr: *const u8, len: usize) -> Result, 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(f: F) -> Result +where + F: FnOnce(&mut Adi) -> Result, +{ + 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, + coreadi: Vec, + library_path: String, + provisioning_path: Option, + identifier: Option, +) -> 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 { + 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()) +} diff --git a/src/idbfs.rs b/src/idbfs.rs new file mode 100644 index 0000000..74e1b04 --- /dev/null +++ b/src/idbfs.rs @@ -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 { + 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) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..eaced2a --- /dev/null +++ b/src/lib.rs @@ -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; diff --git a/src/provisioning.rs b/src/provisioning.rs new file mode 100644 index 0000000..3347452 --- /dev/null +++ b/src/provisioning.rs @@ -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, +} + +impl<'a> ProvisioningSession<'a> { + pub fn new( + adi: &'a mut Adi, + device: &'a DeviceData, + apple_root_pem: Option, + ) -> Result { + 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#" + + + + Header + + Request + + +"#; + + 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!( + "\n\n\n\n Header\n \n Request\n \n cpim\n {}\n \n\n", + 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> { + 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> { + 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 { + 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> { + let mut candidates: Vec = 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 { + 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() +} diff --git a/src/provisioning_wasm.rs b/src/provisioning_wasm.rs new file mode 100644 index 0000000..9129709 --- /dev/null +++ b/src/provisioning_wasm.rs @@ -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, +} + +impl<'a> ProvisioningSession<'a> { + pub fn new( + adi: &'a mut Adi, + device: &'a DeviceData, + _apple_root_pem: Option, + ) -> Result { + 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#" + + + + Header + + Request + + +"#; + + 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!( + "\n\n\n\n Header\n \n Request\n \n cpim\n {}\n \n\n", + 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> { + 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> { + 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> { + // 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 { + 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 { + 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() +} diff --git a/src/runtime.rs b/src/runtime.rs new file mode 100644 index 0000000..30bb726 --- /dev/null +++ b/src/runtime.rs @@ -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, + pub(crate) symbols_by_name: HashMap, +} + +#[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, + pub(crate) library_blobs: HashMap>, + pub(crate) loaded_libraries: Vec, + pub(crate) file_handles: Vec>, + pub(crate) library_root: Option, +} + +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, + } + } +} diff --git a/src/stub.rs b/src/stub.rs new file mode 100644 index 0000000..18374a9 --- /dev/null +++ b/src/stub.rs @@ -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 { + 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); + } +} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..90d13b8 --- /dev/null +++ b/src/util.rs @@ -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::try_from(value).map_err(|_| VmError::IntegerOverflow(value)) +}