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

243
src/adi.rs Normal file
View File

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

50
src/allocator.rs Normal file
View File

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

67
src/constants.rs Normal file
View File

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

113
src/debug.rs Normal file
View File

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

92
src/device.rs Normal file
View File

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

428
src/emu.rs Normal file
View File

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

50
src/errors.rs Normal file
View File

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

452
src/exports.rs Normal file
View File

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

82
src/idbfs.rs Normal file
View File

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

28
src/lib.rs Normal file
View File

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

235
src/provisioning.rs Normal file
View File

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

241
src/provisioning_wasm.rs Normal file
View File

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

47
src/runtime.rs Normal file
View File

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

624
src/stub.rs Normal file
View File

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

37
src/util.rs Normal file
View File

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