init
This commit is contained in:
243
src/adi.rs
Normal file
243
src/adi.rs
Normal file
@@ -0,0 +1,243 @@
|
||||
use crate::debug::debug_print;
|
||||
use crate::emu::{EmuCore, alloc_c_string, ensure_zero_return};
|
||||
use crate::errors::VmError;
|
||||
use crate::util::bytes_to_hex;
|
||||
|
||||
pub struct AdiInit {
|
||||
pub storeservicescore: Vec<u8>,
|
||||
pub coreadi: Vec<u8>,
|
||||
pub library_path: String,
|
||||
pub provisioning_path: Option<String>,
|
||||
pub identifier: Option<String>,
|
||||
}
|
||||
|
||||
pub struct ProvisioningStartResult {
|
||||
pub cpim: Vec<u8>,
|
||||
pub session: u32,
|
||||
}
|
||||
|
||||
pub struct OtpResult {
|
||||
pub otp: Vec<u8>,
|
||||
pub machine_id: Vec<u8>,
|
||||
}
|
||||
|
||||
pub struct Adi {
|
||||
core: EmuCore,
|
||||
p_load_library_with_path: u64,
|
||||
p_set_android_id: u64,
|
||||
p_set_provisioning_path: u64,
|
||||
p_get_login_code: u64,
|
||||
p_provisioning_start: u64,
|
||||
p_provisioning_end: u64,
|
||||
p_otp_request: u64,
|
||||
}
|
||||
|
||||
impl Adi {
|
||||
pub fn new(init: AdiInit) -> Result<Self, VmError> {
|
||||
debug_print(format!("Constructing ADI for '{}'", init.library_path));
|
||||
let mut core = EmuCore::new_arm64()?;
|
||||
core.set_library_root(&init.library_path);
|
||||
core.register_library_blob("libstoreservicescore.so", init.storeservicescore);
|
||||
core.register_library_blob("libCoreADI.so", init.coreadi);
|
||||
|
||||
let storeservices_idx = core.load_library("libstoreservicescore.so")?;
|
||||
|
||||
debug_print("Loading Android-specific symbols...");
|
||||
let p_load_library_with_path =
|
||||
core.resolve_symbol_by_name(storeservices_idx, "kq56gsgHG6")?;
|
||||
let p_set_android_id = core.resolve_symbol_by_name(storeservices_idx, "Sph98paBcz")?;
|
||||
let p_set_provisioning_path =
|
||||
core.resolve_symbol_by_name(storeservices_idx, "nf92ngaK92")?;
|
||||
|
||||
debug_print("Loading ADI symbols...");
|
||||
let p_get_login_code = core.resolve_symbol_by_name(storeservices_idx, "aslgmuibau")?;
|
||||
let p_provisioning_start = core.resolve_symbol_by_name(storeservices_idx, "rsegvyrt87")?;
|
||||
let p_provisioning_end = core.resolve_symbol_by_name(storeservices_idx, "uv5t6nhkui")?;
|
||||
let p_otp_request = core.resolve_symbol_by_name(storeservices_idx, "qi864985u0")?;
|
||||
|
||||
let mut adi = Self {
|
||||
core,
|
||||
p_load_library_with_path,
|
||||
p_set_android_id,
|
||||
p_set_provisioning_path,
|
||||
p_get_login_code,
|
||||
p_provisioning_start,
|
||||
p_provisioning_end,
|
||||
p_otp_request,
|
||||
};
|
||||
|
||||
adi.load_library_with_path(&init.library_path)?;
|
||||
|
||||
if let Some(provisioning_path) = init.provisioning_path.as_deref() {
|
||||
adi.set_provisioning_path(provisioning_path)?;
|
||||
}
|
||||
|
||||
if let Some(identifier) = init.identifier.as_deref() {
|
||||
adi.set_identifier(identifier)?;
|
||||
}
|
||||
|
||||
Ok(adi)
|
||||
}
|
||||
|
||||
pub fn set_identifier(&mut self, identifier: &str) -> Result<(), VmError> {
|
||||
if identifier.is_empty() {
|
||||
debug_print("Skipping empty identifier");
|
||||
return Ok(());
|
||||
}
|
||||
debug_print(format!("Setting identifier {identifier}"));
|
||||
let bytes = identifier.as_bytes();
|
||||
let p_identifier = self.core.alloc_data(bytes)?;
|
||||
let ret = self
|
||||
.core
|
||||
.invoke_cdecl(self.p_set_android_id, &[p_identifier, bytes.len() as u64])?;
|
||||
debug_print(format!(
|
||||
"{}: {:X}={}",
|
||||
"pADISetAndroidID", ret, ret as u32 as i32
|
||||
));
|
||||
ensure_zero_return("ADISetAndroidID", ret)
|
||||
}
|
||||
|
||||
pub fn set_provisioning_path(&mut self, path: &str) -> Result<(), VmError> {
|
||||
let p_path = alloc_c_string(&mut self.core, path)?;
|
||||
let ret = self
|
||||
.core
|
||||
.invoke_cdecl(self.p_set_provisioning_path, &[p_path])?;
|
||||
ensure_zero_return("ADISetProvisioningPath", ret)
|
||||
}
|
||||
|
||||
pub fn load_library_with_path(&mut self, path: &str) -> Result<(), VmError> {
|
||||
let p_path = alloc_c_string(&mut self.core, path)?;
|
||||
let ret = self
|
||||
.core
|
||||
.invoke_cdecl(self.p_load_library_with_path, &[p_path])?;
|
||||
ensure_zero_return("ADILoadLibraryWithPath", ret)
|
||||
}
|
||||
pub fn start_provisioning(
|
||||
&mut self,
|
||||
dsid: u64,
|
||||
server_provisioning_intermediate_metadata: &[u8],
|
||||
) -> Result<ProvisioningStartResult, VmError> {
|
||||
debug_print("ADI.start_provisioning");
|
||||
let p_cpim = self.core.alloc_temporary(8)?;
|
||||
let p_cpim_len = self.core.alloc_temporary(4)?;
|
||||
let p_session = self.core.alloc_temporary(4)?;
|
||||
let p_spim = self
|
||||
.core
|
||||
.alloc_data(server_provisioning_intermediate_metadata)?;
|
||||
|
||||
debug_print(format!("0x{dsid:X}"));
|
||||
debug_print(bytes_to_hex(server_provisioning_intermediate_metadata));
|
||||
|
||||
let ret = self.core.invoke_cdecl(
|
||||
self.p_provisioning_start,
|
||||
&[
|
||||
dsid,
|
||||
p_spim,
|
||||
server_provisioning_intermediate_metadata.len() as u64,
|
||||
p_cpim,
|
||||
p_cpim_len,
|
||||
p_session,
|
||||
],
|
||||
)?;
|
||||
debug_print(format!(
|
||||
"{}: {:X}={}",
|
||||
"pADIProvisioningStart", ret, ret as u32 as i32
|
||||
));
|
||||
ensure_zero_return("ADIProvisioningStart", ret)?;
|
||||
|
||||
let cpim_ptr = self.core.read_u64(p_cpim)?;
|
||||
let cpim_len = self.core.read_u32(p_cpim_len)? as usize;
|
||||
let cpim = self.core.read_data(cpim_ptr, cpim_len)?;
|
||||
let session = self.core.read_u32(p_session)?;
|
||||
|
||||
debug_print(format!("Wrote data to 0x{cpim_ptr:X}"));
|
||||
debug_print(format!("{} {} {}", cpim_len, bytes_to_hex(&cpim), session));
|
||||
|
||||
Ok(ProvisioningStartResult { cpim, session })
|
||||
}
|
||||
|
||||
pub fn is_machine_provisioned(&mut self, dsid: u64) -> Result<bool, VmError> {
|
||||
debug_print("ADI.is_machine_provisioned");
|
||||
let ret = self.core.invoke_cdecl(self.p_get_login_code, &[dsid])?;
|
||||
let code = ret as u32 as i32;
|
||||
|
||||
if code == 0 {
|
||||
return Ok(true);
|
||||
}
|
||||
if code == -45061 {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
debug_print(format!(
|
||||
"Unknown errorCode in is_machine_provisioned: {code}=0x{code:X}"
|
||||
));
|
||||
|
||||
Err(VmError::AdiCallFailed {
|
||||
name: "ADIGetLoginCode",
|
||||
code,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn end_provisioning(
|
||||
&mut self,
|
||||
session: u32,
|
||||
persistent_token_metadata: &[u8],
|
||||
trust_key: &[u8],
|
||||
) -> Result<(), VmError> {
|
||||
let p_ptm = self.core.alloc_data(persistent_token_metadata)?;
|
||||
let p_tk = self.core.alloc_data(trust_key)?;
|
||||
|
||||
let ret = self.core.invoke_cdecl(
|
||||
self.p_provisioning_end,
|
||||
&[
|
||||
session as u64,
|
||||
p_ptm,
|
||||
persistent_token_metadata.len() as u64,
|
||||
p_tk,
|
||||
trust_key.len() as u64,
|
||||
],
|
||||
)?;
|
||||
|
||||
debug_print(format!("0x{session:X}"));
|
||||
debug_print(format!(
|
||||
"{} {}",
|
||||
bytes_to_hex(persistent_token_metadata),
|
||||
persistent_token_metadata.len()
|
||||
));
|
||||
debug_print(format!("{} {}", bytes_to_hex(trust_key), trust_key.len()));
|
||||
debug_print(format!(
|
||||
"{}: {:X}={}",
|
||||
"pADIProvisioningEnd", ret, ret as u32 as i32
|
||||
));
|
||||
|
||||
ensure_zero_return("ADIProvisioningEnd", ret)
|
||||
}
|
||||
|
||||
pub fn request_otp(&mut self, dsid: u64) -> Result<OtpResult, VmError> {
|
||||
debug_print("ADI.request_otp");
|
||||
let p_otp = self.core.alloc_temporary(8)?;
|
||||
let p_otp_len = self.core.alloc_temporary(4)?;
|
||||
let p_mid = self.core.alloc_temporary(8)?;
|
||||
let p_mid_len = self.core.alloc_temporary(4)?;
|
||||
|
||||
let ret = self.core.invoke_cdecl(
|
||||
self.p_otp_request,
|
||||
&[dsid, p_mid, p_mid_len, p_otp, p_otp_len],
|
||||
)?;
|
||||
debug_print(format!(
|
||||
"{}: {:X}={}",
|
||||
"pADIOTPRequest", ret, ret as u32 as i32
|
||||
));
|
||||
ensure_zero_return("ADIOTPRequest", ret)?;
|
||||
|
||||
let otp_ptr = self.core.read_u64(p_otp)?;
|
||||
let otp_len = self.core.read_u32(p_otp_len)? as usize;
|
||||
let otp = self.core.read_data(otp_ptr, otp_len)?;
|
||||
|
||||
let mid_ptr = self.core.read_u64(p_mid)?;
|
||||
let mid_len = self.core.read_u32(p_mid_len)? as usize;
|
||||
let machine_id = self.core.read_data(mid_ptr, mid_len)?;
|
||||
|
||||
Ok(OtpResult { otp, machine_id })
|
||||
}
|
||||
}
|
||||
50
src/allocator.rs
Normal file
50
src/allocator.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use crate::constants::PAGE_SIZE;
|
||||
use crate::errors::VmError;
|
||||
use crate::util::align_up;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Allocator {
|
||||
base: u64,
|
||||
size: u64,
|
||||
offset: u64,
|
||||
}
|
||||
|
||||
impl Allocator {
|
||||
pub fn new(base: u64, size: u64) -> Self {
|
||||
Self {
|
||||
base,
|
||||
size,
|
||||
offset: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn alloc(&mut self, request: u64) -> Result<u64, VmError> {
|
||||
let length = align_up(request.max(1), PAGE_SIZE);
|
||||
let address = self.base + self.offset;
|
||||
let next = self.offset.saturating_add(length);
|
||||
if next > self.size {
|
||||
return Err(VmError::AllocatorOom {
|
||||
base: self.base,
|
||||
size: self.size,
|
||||
request,
|
||||
});
|
||||
}
|
||||
self.offset = next;
|
||||
Ok(address)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Allocator;
|
||||
|
||||
#[test]
|
||||
fn allocator_aligns_to_pages() {
|
||||
let mut allocator = Allocator::new(0x1000_0000, 0x20_000);
|
||||
let a = allocator.alloc(1).expect("alloc 1");
|
||||
let b = allocator.alloc(0x1500).expect("alloc 2");
|
||||
|
||||
assert_eq!(a, 0x1000_0000);
|
||||
assert_eq!(b, 0x1000_1000);
|
||||
}
|
||||
}
|
||||
67
src/constants.rs
Normal file
67
src/constants.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use unicorn_engine::RegisterARM64;
|
||||
|
||||
pub const PAGE_SIZE: u64 = 0x1000;
|
||||
|
||||
pub const RETURN_ADDRESS: u64 = 0xDEAD_0000;
|
||||
pub const STACK_ADDRESS: u64 = 0xF000_0000;
|
||||
pub const STACK_SIZE: u64 = 0x10_0000;
|
||||
|
||||
pub const MALLOC_ADDRESS: u64 = 0x6000_0000;
|
||||
pub const MALLOC_SIZE: u64 = 0x10_00000;
|
||||
|
||||
pub const IMPORT_ADDRESS: u64 = 0xA000_0000;
|
||||
pub const IMPORT_SIZE: u64 = 0x1000;
|
||||
pub const IMPORT_LIBRARY_STRIDE: u64 = 0x0100_0000;
|
||||
pub const IMPORT_LIBRARY_COUNT: usize = 10;
|
||||
|
||||
pub const TEMP_ALLOC_BASE: u64 = 0x0008_0000_0000;
|
||||
pub const TEMP_ALLOC_SIZE: u64 = 0x1000_0000;
|
||||
pub const LIB_ALLOC_BASE: u64 = 0x0010_0000;
|
||||
pub const LIB_ALLOC_SIZE: u64 = 0x9000_0000;
|
||||
pub const LIB_RESERVATION_SIZE: u64 = 0x1000_0000;
|
||||
|
||||
pub const O_WRONLY: u64 = 0o1;
|
||||
pub const O_RDWR: u64 = 0o2;
|
||||
pub const O_ACCMODE: u64 = 0o3;
|
||||
pub const O_CREAT: u64 = 0o100;
|
||||
pub const O_NOFOLLOW: u64 = 0o100000;
|
||||
|
||||
pub const ENOENT: u32 = 2;
|
||||
|
||||
pub const RET_AARCH64: [u8; 4] = [0xC0, 0x03, 0x5F, 0xD6];
|
||||
|
||||
|
||||
pub const ARG_REGS: [RegisterARM64; 29] = [
|
||||
RegisterARM64::X0,
|
||||
RegisterARM64::X1,
|
||||
RegisterARM64::X2,
|
||||
RegisterARM64::X3,
|
||||
RegisterARM64::X4,
|
||||
RegisterARM64::X5,
|
||||
RegisterARM64::X6,
|
||||
RegisterARM64::X7,
|
||||
RegisterARM64::X8,
|
||||
RegisterARM64::X9,
|
||||
RegisterARM64::X10,
|
||||
RegisterARM64::X11,
|
||||
RegisterARM64::X12,
|
||||
RegisterARM64::X13,
|
||||
RegisterARM64::X14,
|
||||
RegisterARM64::X15,
|
||||
RegisterARM64::X16,
|
||||
RegisterARM64::X17,
|
||||
RegisterARM64::X18,
|
||||
RegisterARM64::X19,
|
||||
RegisterARM64::X20,
|
||||
RegisterARM64::X21,
|
||||
RegisterARM64::X22,
|
||||
RegisterARM64::X23,
|
||||
RegisterARM64::X24,
|
||||
RegisterARM64::X25,
|
||||
RegisterARM64::X26,
|
||||
RegisterARM64::X27,
|
||||
RegisterARM64::X28,
|
||||
];
|
||||
|
||||
pub const DEBUG_PRINT_ENABLED: bool = false;
|
||||
pub const DEBUG_TRACE_ENABLED: bool = false;
|
||||
113
src/debug.rs
Normal file
113
src/debug.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
use std::fmt::Write as _;
|
||||
|
||||
use unicorn_engine::unicorn_const::MemType;
|
||||
use unicorn_engine::{RegisterARM64, Unicorn};
|
||||
|
||||
use crate::constants::{DEBUG_PRINT_ENABLED, DEBUG_TRACE_ENABLED};
|
||||
use crate::runtime::RuntimeState;
|
||||
|
||||
|
||||
pub(crate) fn debug_print(message: impl AsRef<str>) {
|
||||
if DEBUG_PRINT_ENABLED {
|
||||
println!("{}", message.as_ref());
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn debug_trace(message: impl AsRef<str>) {
|
||||
if DEBUG_TRACE_ENABLED {
|
||||
println!("{}", message.as_ref());
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn reg_or_zero(uc: &Unicorn<'_, RuntimeState>, reg: RegisterARM64) -> u64 {
|
||||
uc.reg_read(reg).unwrap_or(0)
|
||||
}
|
||||
|
||||
pub(crate) fn trace_mem_invalid_hook(
|
||||
uc: &Unicorn<'_, RuntimeState>,
|
||||
access: MemType,
|
||||
address: u64,
|
||||
size: usize,
|
||||
value: i64,
|
||||
) {
|
||||
let pc = reg_or_zero(uc, RegisterARM64::PC);
|
||||
match access {
|
||||
MemType::READ_UNMAPPED => {
|
||||
debug_print(format!(
|
||||
">>> Missing memory is being READ at 0x{address:x}, data size = {size}, data value = 0x{:x}, PC=0x{pc:x}",
|
||||
value as u64
|
||||
));
|
||||
dump_registers(uc, "read unmapped");
|
||||
}
|
||||
MemType::WRITE_UNMAPPED => {
|
||||
debug_print(format!(
|
||||
">>> Missing memory is being WRITE at 0x{address:x}, data size = {size}, data value = 0x{:x}, PC=0x{pc:x}",
|
||||
value as u64
|
||||
));
|
||||
|
||||
dump_registers(uc, "write unmapped");
|
||||
}
|
||||
MemType::FETCH_UNMAPPED => {
|
||||
debug_print(format!(
|
||||
">>> Missing memory is being FETCH at 0x{address:x}, data size = {size}, data value = 0x{:x}, PC=0x{pc:x}",
|
||||
value as u64
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn dump_registers(uc: &Unicorn<'_, RuntimeState>, label: &str) {
|
||||
// debug_print(format!());
|
||||
println!("REGDUMP {label}");
|
||||
|
||||
let regs: &[(RegisterARM64, &str)] = &[
|
||||
(RegisterARM64::X0, "X0"),
|
||||
(RegisterARM64::X1, "X1"),
|
||||
(RegisterARM64::X2, "X2"),
|
||||
(RegisterARM64::X3, "X3"),
|
||||
(RegisterARM64::X4, "X4"),
|
||||
(RegisterARM64::X5, "X5"),
|
||||
(RegisterARM64::X6, "X6"),
|
||||
(RegisterARM64::X7, "X7"),
|
||||
(RegisterARM64::X8, "X8"),
|
||||
(RegisterARM64::X9, "X9"),
|
||||
(RegisterARM64::X10, "X10"),
|
||||
(RegisterARM64::X11, "X11"),
|
||||
(RegisterARM64::X12, "X12"),
|
||||
(RegisterARM64::X13, "X13"),
|
||||
(RegisterARM64::X14, "X14"),
|
||||
(RegisterARM64::X15, "X15"),
|
||||
(RegisterARM64::X16, "X16"),
|
||||
(RegisterARM64::X17, "X17"),
|
||||
(RegisterARM64::X18, "X18"),
|
||||
(RegisterARM64::X19, "X19"),
|
||||
(RegisterARM64::X20, "X20"),
|
||||
(RegisterARM64::X21, "X21"),
|
||||
(RegisterARM64::X22, "X22"),
|
||||
(RegisterARM64::X23, "X23"),
|
||||
(RegisterARM64::X24, "X24"),
|
||||
(RegisterARM64::X25, "X25"),
|
||||
(RegisterARM64::X26, "X26"),
|
||||
(RegisterARM64::X27, "X27"),
|
||||
(RegisterARM64::X28, "X28"),
|
||||
(RegisterARM64::FP, "FP"),
|
||||
(RegisterARM64::LR, "LR"),
|
||||
(RegisterARM64::SP, "SP"),
|
||||
];
|
||||
|
||||
let mut line = String::new();
|
||||
for (i, (reg, name)) in regs.iter().enumerate() {
|
||||
let value = reg_or_zero(uc, *reg);
|
||||
let _ = write!(line, " {name}=0x{value:016X}");
|
||||
if (i + 1) % 4 == 0 {
|
||||
println!("{}", line);
|
||||
line = String::new();
|
||||
}
|
||||
}
|
||||
|
||||
if !line.is_empty() {
|
||||
println!("{}", line);
|
||||
}
|
||||
}
|
||||
|
||||
92
src/device.rs
Normal file
92
src/device.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
use std::fmt::Write as _;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use rand::RngCore;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
const DEFAULT_CLIENT_INFO: &str =
|
||||
"<MacBookPro13,2> <macOS;13.1;22C65> <com.apple.AuthKit/1 (com.apple.dt.Xcode/3594.4.19)>";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct DeviceData {
|
||||
#[serde(rename = "UUID")]
|
||||
pub unique_device_identifier: String,
|
||||
#[serde(rename = "clientInfo")]
|
||||
pub server_friendly_description: String,
|
||||
#[serde(rename = "identifier")]
|
||||
pub adi_identifier: String,
|
||||
#[serde(rename = "localUUID")]
|
||||
pub local_user_uuid: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Device {
|
||||
path: PathBuf,
|
||||
pub data: DeviceData,
|
||||
pub initialized: bool,
|
||||
}
|
||||
|
||||
impl Device {
|
||||
pub fn load(path: impl AsRef<Path>) -> Result<Self> {
|
||||
let path = path.as_ref().to_path_buf();
|
||||
|
||||
if !path.exists() {
|
||||
return Ok(Self {
|
||||
path,
|
||||
data: DeviceData::default(),
|
||||
initialized: false,
|
||||
});
|
||||
}
|
||||
|
||||
let bytes = fs::read(&path)
|
||||
.with_context(|| format!("failed to read device file {}", path.display()))?;
|
||||
let data: DeviceData = serde_json::from_slice(&bytes)
|
||||
.with_context(|| format!("failed to parse device file {}", path.display()))?;
|
||||
|
||||
Ok(Self {
|
||||
path,
|
||||
data,
|
||||
initialized: true,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn initialize_defaults(&mut self) {
|
||||
self.data.server_friendly_description = DEFAULT_CLIENT_INFO.to_string();
|
||||
self.data.unique_device_identifier = Uuid::new_v4().to_string().to_uppercase();
|
||||
self.data.adi_identifier = random_hex(8, false);
|
||||
self.data.local_user_uuid = random_hex(32, true);
|
||||
self.initialized = true;
|
||||
}
|
||||
|
||||
pub fn persist(&self) -> Result<()> {
|
||||
if let Some(parent) = self.path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.with_context(|| format!("failed to create parent dir {}", parent.display()))?;
|
||||
}
|
||||
|
||||
let bytes = serde_json::to_vec_pretty(&self.data)?;
|
||||
fs::write(&self.path, bytes)
|
||||
.with_context(|| format!("failed to write device file {}", self.path.display()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn random_hex(byte_len: usize, uppercase: bool) -> String {
|
||||
let mut bytes = vec![0_u8; byte_len];
|
||||
rand::thread_rng().fill_bytes(&mut bytes);
|
||||
|
||||
let mut output = String::with_capacity(byte_len * 2);
|
||||
for byte in bytes {
|
||||
let _ = write!(output, "{byte:02x}");
|
||||
}
|
||||
|
||||
if uppercase {
|
||||
output.make_ascii_uppercase();
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
428
src/emu.rs
Normal file
428
src/emu.rs
Normal file
@@ -0,0 +1,428 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use goblin::elf::program_header::PT_LOAD;
|
||||
use goblin::elf::section_header::SHN_UNDEF;
|
||||
use goblin::elf::{Elf, Reloc};
|
||||
use unicorn_engine::unicorn_const::{Arch, HookType, Mode, Permission, uc_error};
|
||||
use unicorn_engine::{RegisterARM64, Unicorn};
|
||||
|
||||
use crate::constants::{
|
||||
ARG_REGS, IMPORT_ADDRESS, IMPORT_LIBRARY_COUNT, IMPORT_LIBRARY_STRIDE, IMPORT_SIZE,
|
||||
LIB_RESERVATION_SIZE, MALLOC_ADDRESS, MALLOC_SIZE, PAGE_SIZE, RET_AARCH64, RETURN_ADDRESS,
|
||||
STACK_ADDRESS, STACK_SIZE,
|
||||
};
|
||||
use crate::debug::{debug_print, trace_mem_invalid_hook};
|
||||
use crate::errors::VmError;
|
||||
use crate::runtime::{LoadedLibrary, RuntimeState, SymbolEntry};
|
||||
use crate::stub::dispatch_import_stub;
|
||||
use crate::util::{add_i64, align_down, align_up, as_usize};
|
||||
|
||||
pub struct EmuCore {
|
||||
uc: Unicorn<'static, RuntimeState>,
|
||||
}
|
||||
|
||||
impl EmuCore {
|
||||
pub fn new_arm64() -> Result<Self, VmError> {
|
||||
let mut uc = Unicorn::new_with_data(Arch::ARM64, Mode::ARM, RuntimeState::new())?;
|
||||
|
||||
uc.mem_map(RETURN_ADDRESS, as_usize(PAGE_SIZE)?, Permission::ALL)?;
|
||||
uc.mem_map(MALLOC_ADDRESS, as_usize(MALLOC_SIZE)?, Permission::ALL)?;
|
||||
uc.mem_map(STACK_ADDRESS, as_usize(STACK_SIZE)?, Permission::ALL)?;
|
||||
|
||||
for i in 0..IMPORT_LIBRARY_COUNT {
|
||||
let base = IMPORT_ADDRESS + (i as u64) * IMPORT_LIBRARY_STRIDE;
|
||||
uc.mem_map(base, as_usize(IMPORT_SIZE)?, Permission::ALL)?;
|
||||
|
||||
let mut stubs = vec![0_u8; IMPORT_SIZE as usize];
|
||||
for chunk in stubs.chunks_mut(4) {
|
||||
chunk.copy_from_slice(&RET_AARCH64);
|
||||
}
|
||||
uc.mem_write(base, &stubs)?;
|
||||
|
||||
uc.add_code_hook(base, base + IMPORT_SIZE - 1, |uc, address, _| {
|
||||
if let Err(err) = dispatch_import_stub(uc, address) {
|
||||
debug_print(format!("import hook failed at 0x{address:X}: {err}"));
|
||||
let _ = uc.emu_stop();
|
||||
}
|
||||
})?;
|
||||
}
|
||||
|
||||
uc.add_mem_hook(
|
||||
HookType::MEM_READ_UNMAPPED
|
||||
| HookType::MEM_WRITE_UNMAPPED
|
||||
| HookType::MEM_FETCH_UNMAPPED,
|
||||
1,
|
||||
0,
|
||||
|uc, access, address, size, value| {
|
||||
trace_mem_invalid_hook(uc, access, address, size, value);
|
||||
false
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(Self { uc })
|
||||
}
|
||||
|
||||
pub fn register_library_blob(&mut self, name: impl Into<String>, data: Vec<u8>) {
|
||||
self.uc
|
||||
.get_data_mut()
|
||||
.library_blobs
|
||||
.insert(name.into(), data);
|
||||
}
|
||||
|
||||
pub fn set_library_root(&mut self, path: &str) {
|
||||
let normalized = normalize_library_root(path);
|
||||
if normalized.is_empty() {
|
||||
return;
|
||||
}
|
||||
self.uc.get_data_mut().library_root = Some(normalized);
|
||||
}
|
||||
|
||||
pub fn load_library(&mut self, library_name: &str) -> Result<usize, VmError> {
|
||||
load_library_by_name(&mut self.uc, library_name)
|
||||
}
|
||||
|
||||
pub fn resolve_symbol_by_name(
|
||||
&self,
|
||||
library_index: usize,
|
||||
symbol_name: &str,
|
||||
) -> Result<u64, VmError> {
|
||||
resolve_symbol_from_loaded_library_by_name(&self.uc, library_index, symbol_name)
|
||||
}
|
||||
|
||||
pub fn invoke_cdecl(&mut self, address: u64, args: &[u64]) -> Result<u64, VmError> {
|
||||
if args.len() > ARG_REGS.len() {
|
||||
return Err(VmError::TooManyArguments(args.len()));
|
||||
}
|
||||
|
||||
for (index, value) in args.iter().enumerate() {
|
||||
self.uc.reg_write(ARG_REGS[index], *value)?;
|
||||
debug_print(format!("X{index}: 0x{value:08X}"));
|
||||
}
|
||||
|
||||
debug_print(format!("Calling 0x{address:X}"));
|
||||
self.uc
|
||||
.reg_write(RegisterARM64::SP, STACK_ADDRESS + STACK_SIZE)?;
|
||||
self.uc.reg_write(RegisterARM64::LR, RETURN_ADDRESS)?;
|
||||
self.uc.emu_start(address, RETURN_ADDRESS, 0, 0)?;
|
||||
Ok(self.uc.reg_read(RegisterARM64::X0)?)
|
||||
}
|
||||
|
||||
pub fn alloc_data(&mut self, data: &[u8]) -> Result<u64, VmError> {
|
||||
alloc_temp_bytes(&mut self.uc, data, 0xCC)
|
||||
}
|
||||
|
||||
pub fn alloc_temporary(&mut self, length: usize) -> Result<u64, VmError> {
|
||||
let data = vec![0xAA; length.max(1)];
|
||||
alloc_temp_bytes(&mut self.uc, &data, 0xAA)
|
||||
}
|
||||
|
||||
pub fn read_data(&self, address: u64, length: usize) -> Result<Vec<u8>, VmError> {
|
||||
Ok(self.uc.mem_read_as_vec(address, length)?)
|
||||
}
|
||||
|
||||
pub fn write_data(&mut self, address: u64, data: &[u8]) -> Result<(), VmError> {
|
||||
self.uc.mem_write(address, data)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn read_u32(&self, address: u64) -> Result<u32, VmError> {
|
||||
let mut bytes = [0_u8; 4];
|
||||
self.uc.mem_read(address, &mut bytes)?;
|
||||
Ok(u32::from_le_bytes(bytes))
|
||||
}
|
||||
|
||||
pub fn read_u64(&self, address: u64) -> Result<u64, VmError> {
|
||||
let mut bytes = [0_u8; 8];
|
||||
self.uc.mem_read(address, &mut bytes)?;
|
||||
Ok(u64::from_le_bytes(bytes))
|
||||
}
|
||||
|
||||
pub fn write_u32(&mut self, address: u64, value: u32) -> Result<(), VmError> {
|
||||
self.uc.mem_write(address, &value.to_le_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn write_u64(&mut self, address: u64, value: u64) -> Result<(), VmError> {
|
||||
self.uc.mem_write(address, &value.to_le_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
pub fn read_c_string(&self, address: u64, max_len: usize) -> Result<String, VmError> {
|
||||
read_c_string(&self.uc, address, max_len)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn alloc_c_string(core: &mut EmuCore, value: &str) -> Result<u64, VmError> {
|
||||
let mut bytes = Vec::with_capacity(value.len() + 1);
|
||||
bytes.extend_from_slice(value.as_bytes());
|
||||
bytes.push(0);
|
||||
core.alloc_data(&bytes)
|
||||
}
|
||||
|
||||
pub(crate) fn ensure_zero_return(name: &'static str, value: u64) -> Result<(), VmError> {
|
||||
let code = value as u32 as i32;
|
||||
if code == 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(VmError::AdiCallFailed { name, code })
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_library_root(path: &str) -> String {
|
||||
let trimmed = path.trim();
|
||||
if trimmed.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let mut out = String::with_capacity(trimmed.len());
|
||||
let mut prev_slash = false;
|
||||
for ch in trimmed.chars() {
|
||||
if ch == '/' {
|
||||
if prev_slash {
|
||||
continue;
|
||||
}
|
||||
prev_slash = true;
|
||||
} else {
|
||||
prev_slash = false;
|
||||
}
|
||||
out.push(ch);
|
||||
}
|
||||
|
||||
while out.len() > 1 && out.ends_with('/') {
|
||||
out.pop();
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
fn alloc_temp_bytes(
|
||||
uc: &mut Unicorn<'_, RuntimeState>,
|
||||
data: &[u8],
|
||||
padding_byte: u8,
|
||||
) -> Result<u64, VmError> {
|
||||
let request = data.len().max(1) as u64;
|
||||
let length = align_up(request, PAGE_SIZE);
|
||||
let address = {
|
||||
let state = uc.get_data_mut();
|
||||
state.temp_allocator.alloc(length)?
|
||||
};
|
||||
|
||||
debug_print(format!(
|
||||
"Allocating at 0x{address:X}; bytes 0x{:X}/0x{length:X}",
|
||||
data.len()
|
||||
));
|
||||
uc.mem_map(address, as_usize(length)?, Permission::ALL)?;
|
||||
|
||||
let mut buffer = vec![padding_byte; length as usize];
|
||||
if !data.is_empty() {
|
||||
buffer[..data.len()].copy_from_slice(data);
|
||||
}
|
||||
uc.mem_write(address, &buffer)?;
|
||||
|
||||
Ok(address)
|
||||
}
|
||||
|
||||
pub(crate) fn set_errno(uc: &mut Unicorn<'_, RuntimeState>, value: u32) -> Result<(), VmError> {
|
||||
let errno_address = ensure_errno_address(uc)?;
|
||||
uc.mem_write(errno_address, &value.to_le_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn ensure_errno_address(uc: &mut Unicorn<'_, RuntimeState>) -> Result<u64, VmError> {
|
||||
if let Some(address) = uc.get_data().errno_address {
|
||||
return Ok(address);
|
||||
}
|
||||
|
||||
let address = alloc_temp_bytes(uc, &[0, 0, 0, 0], 0)?;
|
||||
uc.get_data_mut().errno_address = Some(address);
|
||||
Ok(address)
|
||||
}
|
||||
|
||||
pub(crate) fn load_library_by_name(
|
||||
uc: &mut Unicorn<'_, RuntimeState>,
|
||||
library_name: &str,
|
||||
) -> Result<usize, VmError> {
|
||||
for (index, library) in uc.get_data().loaded_libraries.iter().enumerate() {
|
||||
if library.name == library_name {
|
||||
debug_print("Library already loaded");
|
||||
return Ok(index);
|
||||
}
|
||||
}
|
||||
|
||||
let (library_index, elf_data) = {
|
||||
let state = uc.get_data();
|
||||
let data = state
|
||||
.library_blobs
|
||||
.get(library_name)
|
||||
.cloned()
|
||||
.ok_or_else(|| VmError::LibraryNotRegistered(library_name.to_string()))?;
|
||||
(state.loaded_libraries.len(), data)
|
||||
};
|
||||
|
||||
let elf = Elf::parse(&elf_data)?;
|
||||
let base = {
|
||||
let state = uc.get_data_mut();
|
||||
state.library_allocator.alloc(LIB_RESERVATION_SIZE)?
|
||||
};
|
||||
|
||||
let mut symbols = Vec::with_capacity(elf.dynsyms.len());
|
||||
let mut symbols_by_name = HashMap::new();
|
||||
|
||||
for (index, sym) in elf.dynsyms.iter().enumerate() {
|
||||
let name = elf.dynstrtab.get_at(sym.st_name).unwrap_or("").to_string();
|
||||
let resolved = if sym.st_shndx == SHN_UNDEF as usize {
|
||||
IMPORT_ADDRESS + (library_index as u64) * IMPORT_LIBRARY_STRIDE + (index as u64) * 4
|
||||
} else {
|
||||
base.wrapping_add(sym.st_value)
|
||||
};
|
||||
|
||||
if !name.is_empty() {
|
||||
symbols_by_name.entry(name.clone()).or_insert(resolved);
|
||||
}
|
||||
|
||||
symbols.push(SymbolEntry { name, resolved });
|
||||
}
|
||||
|
||||
for ph in &elf.program_headers {
|
||||
let seg_addr = base.wrapping_add(ph.p_vaddr);
|
||||
let map_start = align_down(seg_addr, PAGE_SIZE);
|
||||
let map_end = align_up(seg_addr.wrapping_add(ph.p_memsz), PAGE_SIZE);
|
||||
let map_len = map_end.saturating_sub(map_start);
|
||||
|
||||
if map_len == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
debug_print(format!(
|
||||
"Mapping at 0x{map_start:X}-0x{map_end:X} (0x{seg_addr:X}-0x{:X}); bytes 0x{map_len:X}",
|
||||
seg_addr + map_len.saturating_sub(1)
|
||||
));
|
||||
|
||||
if ph.p_type != PT_LOAD || ph.p_memsz == 0 {
|
||||
debug_print(format!(
|
||||
"- Skipping p_type={} offset=0x{:X} vaddr=0x{:X}",
|
||||
ph.p_type, ph.p_offset, ph.p_vaddr
|
||||
));
|
||||
continue;
|
||||
}
|
||||
match uc.mem_map(map_start, as_usize(map_len)?, Permission::ALL) {
|
||||
Ok(()) => {}
|
||||
Err(uc_error::MAP) => {}
|
||||
Err(err) => return Err(err.into()),
|
||||
}
|
||||
|
||||
let file_offset = ph.p_offset as usize;
|
||||
let file_len = ph.p_filesz as usize;
|
||||
let file_end = file_offset
|
||||
.checked_add(file_len)
|
||||
.ok_or(VmError::InvalidElfRange)?;
|
||||
|
||||
if file_end > elf_data.len() {
|
||||
return Err(VmError::InvalidElfRange);
|
||||
}
|
||||
|
||||
let mut bytes = vec![0_u8; map_len as usize];
|
||||
let start_offset = (seg_addr - map_start) as usize;
|
||||
|
||||
if file_len > 0 {
|
||||
let dest_end = start_offset
|
||||
.checked_add(file_len)
|
||||
.ok_or(VmError::InvalidElfRange)?;
|
||||
if dest_end > bytes.len() {
|
||||
return Err(VmError::InvalidElfRange);
|
||||
}
|
||||
bytes[start_offset..dest_end].copy_from_slice(&elf_data[file_offset..file_end]);
|
||||
}
|
||||
|
||||
uc.mem_write(map_start, &bytes)?;
|
||||
}
|
||||
|
||||
for rela in elf.dynrelas.iter() {
|
||||
apply_relocation(uc, base, &rela, library_name, &symbols)?;
|
||||
}
|
||||
|
||||
for rela in elf.pltrelocs.iter() {
|
||||
apply_relocation(uc, base, &rela, library_name, &symbols)?;
|
||||
}
|
||||
|
||||
let loaded = LoadedLibrary {
|
||||
name: library_name.to_string(),
|
||||
symbols,
|
||||
symbols_by_name,
|
||||
};
|
||||
|
||||
uc.get_data_mut().loaded_libraries.push(loaded);
|
||||
|
||||
Ok(library_index)
|
||||
}
|
||||
|
||||
fn apply_relocation(
|
||||
uc: &mut Unicorn<'_, RuntimeState>,
|
||||
base: u64,
|
||||
relocation: &Reloc,
|
||||
library_name: &str,
|
||||
symbols: &[SymbolEntry],
|
||||
) -> Result<(), VmError> {
|
||||
if relocation.r_type == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let relocation_addr = base.wrapping_add(relocation.r_offset);
|
||||
let addend = relocation.r_addend.unwrap_or(0);
|
||||
|
||||
let symbol_address = if relocation.r_sym < symbols.len() {
|
||||
symbols[relocation.r_sym].resolved
|
||||
} else {
|
||||
return Err(VmError::SymbolIndexOutOfRange {
|
||||
library: library_name.to_string(),
|
||||
index: relocation.r_sym,
|
||||
});
|
||||
};
|
||||
|
||||
let value = match relocation.r_type {
|
||||
goblin::elf64::reloc::R_AARCH64_ABS64 | goblin::elf64::reloc::R_AARCH64_GLOB_DAT => {
|
||||
add_i64(symbol_address, addend)
|
||||
}
|
||||
goblin::elf64::reloc::R_AARCH64_JUMP_SLOT => symbol_address,
|
||||
goblin::elf64::reloc::R_AARCH64_RELATIVE => add_i64(base, addend),
|
||||
other => return Err(VmError::UnsupportedRelocation(other)),
|
||||
};
|
||||
|
||||
uc.mem_write(relocation_addr, &value.to_le_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_symbol_from_loaded_library_by_name(
|
||||
uc: &Unicorn<'_, RuntimeState>,
|
||||
library_index: usize,
|
||||
symbol_name: &str,
|
||||
) -> Result<u64, VmError> {
|
||||
let library = uc
|
||||
.get_data()
|
||||
.loaded_libraries
|
||||
.get(library_index)
|
||||
.ok_or(VmError::LibraryNotLoaded(library_index))?;
|
||||
|
||||
library
|
||||
.symbols_by_name
|
||||
.get(symbol_name)
|
||||
.copied()
|
||||
.ok_or_else(|| VmError::SymbolNotFound {
|
||||
library: library.name.clone(),
|
||||
symbol: symbol_name.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn read_c_string(
|
||||
uc: &Unicorn<'_, RuntimeState>,
|
||||
address: u64,
|
||||
max_len: usize,
|
||||
) -> Result<String, VmError> {
|
||||
let bytes = uc.mem_read_as_vec(address, max_len)?;
|
||||
let len = bytes
|
||||
.iter()
|
||||
.position(|byte| *byte == 0)
|
||||
.ok_or(VmError::UnterminatedCString(address))?;
|
||||
Ok(String::from_utf8_lossy(&bytes[..len]).into_owned())
|
||||
}
|
||||
50
src/errors.rs
Normal file
50
src/errors.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use thiserror::Error;
|
||||
use unicorn_engine::unicorn_const::uc_error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum VmError {
|
||||
#[error("unicorn error: {0:?}")]
|
||||
Unicorn(uc_error),
|
||||
#[error("io error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("elf parse error: {0}")]
|
||||
Elf(#[from] goblin::error::Error),
|
||||
#[error("allocator out of memory: base=0x{base:X} size=0x{size:X} request=0x{request:X}")]
|
||||
AllocatorOom { base: u64, size: u64, request: u64 },
|
||||
#[error("library not registered: {0}")]
|
||||
LibraryNotRegistered(String),
|
||||
#[error("library not loaded: {0}")]
|
||||
LibraryNotLoaded(usize),
|
||||
#[error("symbol not found: {symbol} in {library}")]
|
||||
SymbolNotFound { library: String, symbol: String },
|
||||
#[error("symbol index out of range: lib={library} index={index}")]
|
||||
SymbolIndexOutOfRange { library: String, index: usize },
|
||||
#[error("unsupported relocation type: {0}")]
|
||||
UnsupportedRelocation(u32),
|
||||
#[error("invalid ELF file range")]
|
||||
InvalidElfRange,
|
||||
#[error("unhandled import: {0}")]
|
||||
UnhandledImport(String),
|
||||
#[error("invalid import address: 0x{0:X}")]
|
||||
InvalidImportAddress(u64),
|
||||
#[error("invalid dlopen handle: {0}")]
|
||||
InvalidDlopenHandle(u64),
|
||||
#[error("invalid file descriptor: {0}")]
|
||||
InvalidFileDescriptor(u64),
|
||||
#[error("too many cdecl args: {0} (max 29)")]
|
||||
TooManyArguments(usize),
|
||||
#[error("adi call failed: {name} returned {code}")]
|
||||
AdiCallFailed { name: &'static str, code: i32 },
|
||||
#[error("unterminated C string at 0x{0:X}")]
|
||||
UnterminatedCString(u64),
|
||||
#[error("empty path")]
|
||||
EmptyPath,
|
||||
#[error("integer conversion failed for value: {0}")]
|
||||
IntegerOverflow(u64),
|
||||
}
|
||||
|
||||
impl From<uc_error> for VmError {
|
||||
fn from(value: uc_error) -> Self {
|
||||
Self::Unicorn(value)
|
||||
}
|
||||
}
|
||||
452
src/exports.rs
Normal file
452
src/exports.rs
Normal file
@@ -0,0 +1,452 @@
|
||||
use std::cell::RefCell;
|
||||
use std::ffi::{CStr, c_char};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::{Adi, AdiInit, init_idbfs_for_path, sync_idbfs};
|
||||
|
||||
#[derive(Default)]
|
||||
struct ExportState {
|
||||
adi: Option<Adi>,
|
||||
last_error: String,
|
||||
cpim: Vec<u8>,
|
||||
session: u32,
|
||||
otp: Vec<u8>,
|
||||
mid: Vec<u8>,
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static STATE: RefCell<ExportState> = RefCell::new(ExportState::default());
|
||||
}
|
||||
|
||||
fn set_last_error(message: impl Into<String>) {
|
||||
STATE.with(|state| {
|
||||
state.borrow_mut().last_error = message.into();
|
||||
});
|
||||
}
|
||||
|
||||
fn clear_last_error() {
|
||||
STATE.with(|state| {
|
||||
state.borrow_mut().last_error.clear();
|
||||
});
|
||||
}
|
||||
|
||||
unsafe fn c_string(ptr: *const c_char) -> Result<String, String> {
|
||||
if ptr.is_null() {
|
||||
return Err("null C string pointer".to_string());
|
||||
}
|
||||
unsafe { CStr::from_ptr(ptr) }
|
||||
.to_str()
|
||||
.map(|s| s.to_string())
|
||||
.map_err(|e| format!("invalid utf-8 string: {e}"))
|
||||
}
|
||||
|
||||
unsafe fn optional_c_string(ptr: *const c_char) -> Result<Option<String>, String> {
|
||||
if ptr.is_null() {
|
||||
return Ok(None);
|
||||
}
|
||||
unsafe { c_string(ptr).map(Some) }
|
||||
}
|
||||
|
||||
unsafe fn input_bytes(ptr: *const u8, len: usize) -> Result<Vec<u8>, String> {
|
||||
if len == 0 {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
if ptr.is_null() {
|
||||
return Err("null bytes pointer with non-zero length".to_string());
|
||||
}
|
||||
Ok(unsafe { std::slice::from_raw_parts(ptr, len) }.to_vec())
|
||||
}
|
||||
|
||||
fn with_adi_mut<T, F>(f: F) -> Result<T, String>
|
||||
where
|
||||
F: FnOnce(&mut Adi) -> Result<T, String>,
|
||||
{
|
||||
STATE.with(|state| {
|
||||
let mut state = state.borrow_mut();
|
||||
let adi = state
|
||||
.adi
|
||||
.as_mut()
|
||||
.ok_or_else(|| "ADI is not initialized".to_string())?;
|
||||
f(adi)
|
||||
})
|
||||
}
|
||||
|
||||
fn install_adi(adi: Adi) {
|
||||
STATE.with(|state| {
|
||||
let mut state = state.borrow_mut();
|
||||
state.adi = Some(adi);
|
||||
state.cpim.clear();
|
||||
state.otp.clear();
|
||||
state.mid.clear();
|
||||
state.session = 0;
|
||||
});
|
||||
}
|
||||
|
||||
fn init_adi_from_parts(
|
||||
storeservicescore: Vec<u8>,
|
||||
coreadi: Vec<u8>,
|
||||
library_path: String,
|
||||
provisioning_path: Option<String>,
|
||||
identifier: Option<String>,
|
||||
) -> Result<(), String> {
|
||||
let adi = Adi::new(AdiInit {
|
||||
storeservicescore,
|
||||
coreadi,
|
||||
library_path,
|
||||
provisioning_path,
|
||||
identifier,
|
||||
})
|
||||
.map_err(|e| format!("ADI init failed: {e}"))?;
|
||||
|
||||
install_adi(adi);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn anisette_init_from_files(
|
||||
storeservices_path: *const c_char,
|
||||
coreadi_path: *const c_char,
|
||||
library_path: *const c_char,
|
||||
provisioning_path: *const c_char,
|
||||
identifier: *const c_char,
|
||||
) -> i32 {
|
||||
let result = (|| -> Result<(), String> {
|
||||
let storeservices_path = unsafe { c_string(storeservices_path)? };
|
||||
let coreadi_path = unsafe { c_string(coreadi_path)? };
|
||||
let library_path = unsafe { c_string(library_path)? };
|
||||
let provisioning_path = unsafe { optional_c_string(provisioning_path)? };
|
||||
let identifier = unsafe { optional_c_string(identifier)? };
|
||||
|
||||
let storeservicescore = fs::read(&storeservices_path).map_err(|e| {
|
||||
format!(
|
||||
"failed to read storeservices core '{}': {e}",
|
||||
storeservices_path
|
||||
)
|
||||
})?;
|
||||
let coreadi = fs::read(&coreadi_path)
|
||||
.map_err(|e| format!("failed to read coreadi '{}': {e}", coreadi_path))?;
|
||||
|
||||
init_adi_from_parts(
|
||||
storeservicescore,
|
||||
coreadi,
|
||||
library_path,
|
||||
provisioning_path,
|
||||
identifier,
|
||||
)
|
||||
})();
|
||||
|
||||
match result {
|
||||
Ok(()) => {
|
||||
clear_last_error();
|
||||
0
|
||||
}
|
||||
Err(err) => {
|
||||
set_last_error(err);
|
||||
-1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn anisette_init_from_blobs(
|
||||
storeservices_ptr: *const u8,
|
||||
storeservices_len: usize,
|
||||
coreadi_ptr: *const u8,
|
||||
coreadi_len: usize,
|
||||
library_path: *const c_char,
|
||||
provisioning_path: *const c_char,
|
||||
identifier: *const c_char,
|
||||
) -> i32 {
|
||||
let result = (|| -> Result<(), String> {
|
||||
let storeservicescore = unsafe { input_bytes(storeservices_ptr, storeservices_len)? };
|
||||
let coreadi = unsafe { input_bytes(coreadi_ptr, coreadi_len)? };
|
||||
let library_path = unsafe { c_string(library_path)? };
|
||||
let provisioning_path = unsafe { optional_c_string(provisioning_path)? };
|
||||
let identifier = unsafe { optional_c_string(identifier)? };
|
||||
|
||||
init_adi_from_parts(
|
||||
storeservicescore,
|
||||
coreadi,
|
||||
library_path,
|
||||
provisioning_path,
|
||||
identifier,
|
||||
)
|
||||
})();
|
||||
|
||||
match result {
|
||||
Ok(()) => {
|
||||
clear_last_error();
|
||||
0
|
||||
}
|
||||
Err(err) => {
|
||||
set_last_error(err);
|
||||
-1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn anisette_set_identifier(identifier: *const c_char) -> i32 {
|
||||
let result = (|| -> Result<(), String> {
|
||||
let identifier = unsafe { c_string(identifier)? };
|
||||
with_adi_mut(|adi| adi.set_identifier(&identifier).map_err(|e| e.to_string()))
|
||||
})();
|
||||
|
||||
match result {
|
||||
Ok(()) => {
|
||||
clear_last_error();
|
||||
0
|
||||
}
|
||||
Err(err) => {
|
||||
set_last_error(err);
|
||||
-1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn anisette_set_provisioning_path(path: *const c_char) -> i32 {
|
||||
let result = (|| -> Result<(), String> {
|
||||
let path = unsafe { c_string(path)? };
|
||||
with_adi_mut(|adi| adi.set_provisioning_path(&path).map_err(|e| e.to_string()))
|
||||
})();
|
||||
|
||||
match result {
|
||||
Ok(()) => {
|
||||
clear_last_error();
|
||||
0
|
||||
}
|
||||
Err(err) => {
|
||||
set_last_error(err);
|
||||
-1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn anisette_is_machine_provisioned(dsid: u64) -> i32 {
|
||||
let result = (|| -> Result<i32, String> {
|
||||
let mut out = -1;
|
||||
with_adi_mut(|adi| {
|
||||
let provisioned = adi
|
||||
.is_machine_provisioned(dsid)
|
||||
.map_err(|e| e.to_string())?;
|
||||
out = if provisioned { 1 } else { 0 };
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(out)
|
||||
})();
|
||||
|
||||
match result {
|
||||
Ok(value) => {
|
||||
clear_last_error();
|
||||
value
|
||||
}
|
||||
Err(err) => {
|
||||
set_last_error(err);
|
||||
-1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn anisette_start_provisioning(
|
||||
dsid: u64,
|
||||
spim_ptr: *const u8,
|
||||
spim_len: usize,
|
||||
) -> i32 {
|
||||
let result = (|| -> Result<(), String> {
|
||||
let spim = unsafe { input_bytes(spim_ptr, spim_len)? };
|
||||
let out = with_adi_mut(|adi| {
|
||||
adi.start_provisioning(dsid, &spim)
|
||||
.map_err(|e| format!("start_provisioning failed: {e}"))
|
||||
})?;
|
||||
STATE.with(|state| {
|
||||
let mut state = state.borrow_mut();
|
||||
state.cpim = out.cpim;
|
||||
state.session = out.session;
|
||||
});
|
||||
Ok(())
|
||||
})();
|
||||
|
||||
match result {
|
||||
Ok(()) => {
|
||||
clear_last_error();
|
||||
0
|
||||
}
|
||||
Err(err) => {
|
||||
set_last_error(err);
|
||||
-1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn anisette_get_cpim_ptr() -> *const u8 {
|
||||
STATE.with(|state| state.borrow().cpim.as_ptr())
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn anisette_get_cpim_len() -> usize {
|
||||
STATE.with(|state| state.borrow().cpim.len())
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn anisette_get_session() -> u32 {
|
||||
STATE.with(|state| state.borrow().session)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn anisette_end_provisioning(
|
||||
session: u32,
|
||||
ptm_ptr: *const u8,
|
||||
ptm_len: usize,
|
||||
tk_ptr: *const u8,
|
||||
tk_len: usize,
|
||||
) -> i32 {
|
||||
let result = (|| -> Result<(), String> {
|
||||
let ptm = unsafe { input_bytes(ptm_ptr, ptm_len)? };
|
||||
let tk = unsafe { input_bytes(tk_ptr, tk_len)? };
|
||||
with_adi_mut(|adi| {
|
||||
adi.end_provisioning(session, &ptm, &tk)
|
||||
.map_err(|e| format!("end_provisioning failed: {e}"))
|
||||
})
|
||||
})();
|
||||
|
||||
match result {
|
||||
Ok(()) => {
|
||||
clear_last_error();
|
||||
0
|
||||
}
|
||||
Err(err) => {
|
||||
set_last_error(err);
|
||||
-1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn anisette_request_otp(dsid: u64) -> i32 {
|
||||
let result = (|| -> Result<(), String> {
|
||||
let out = with_adi_mut(|adi| {
|
||||
adi.request_otp(dsid)
|
||||
.map_err(|e| format!("request_otp failed: {e:#}"))
|
||||
})?;
|
||||
STATE.with(|state| {
|
||||
let mut state = state.borrow_mut();
|
||||
state.otp = out.otp;
|
||||
state.mid = out.machine_id;
|
||||
});
|
||||
Ok(())
|
||||
})();
|
||||
|
||||
match result {
|
||||
Ok(()) => {
|
||||
clear_last_error();
|
||||
0
|
||||
}
|
||||
Err(err) => {
|
||||
set_last_error(err);
|
||||
-1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn anisette_get_otp_ptr() -> *const u8 {
|
||||
STATE.with(|state| state.borrow().otp.as_ptr())
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn anisette_get_otp_len() -> usize {
|
||||
STATE.with(|state| state.borrow().otp.len())
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn anisette_get_mid_ptr() -> *const u8 {
|
||||
STATE.with(|state| state.borrow().mid.as_ptr())
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn anisette_get_mid_len() -> usize {
|
||||
STATE.with(|state| state.borrow().mid.len())
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn anisette_fs_write_file(
|
||||
path: *const c_char,
|
||||
data_ptr: *const u8,
|
||||
data_len: usize,
|
||||
) -> i32 {
|
||||
let result = (|| -> Result<(), String> {
|
||||
let path = unsafe { c_string(path)? };
|
||||
let data = unsafe { input_bytes(data_ptr, data_len)? };
|
||||
let path_ref = Path::new(&path);
|
||||
if let Some(parent) = path_ref.parent()
|
||||
&& !parent.as_os_str().is_empty() {
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|e| format!("failed to create dir '{}': {e}", parent.display()))?;
|
||||
}
|
||||
fs::write(&path, data).map_err(|e| format!("failed to write '{path}': {e}"))?;
|
||||
Ok(())
|
||||
})();
|
||||
|
||||
match result {
|
||||
Ok(()) => {
|
||||
clear_last_error();
|
||||
0
|
||||
}
|
||||
Err(err) => {
|
||||
set_last_error(err);
|
||||
-1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn anisette_idbfs_init(path: *const c_char) -> i32 {
|
||||
let result = (|| -> Result<(), String> {
|
||||
let path = unsafe { c_string(path)? };
|
||||
init_idbfs_for_path(&path)?;
|
||||
Ok(())
|
||||
})();
|
||||
|
||||
match result {
|
||||
Ok(()) => {
|
||||
clear_last_error();
|
||||
0
|
||||
}
|
||||
Err(err) => {
|
||||
set_last_error(err);
|
||||
-1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn anisette_idbfs_sync(populate_from_storage: i32) -> i32 {
|
||||
let result = sync_idbfs(populate_from_storage != 0);
|
||||
match result {
|
||||
Ok(()) => {
|
||||
clear_last_error();
|
||||
0
|
||||
}
|
||||
Err(err) => {
|
||||
set_last_error(err);
|
||||
-1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn anisette_last_error_ptr() -> *const u8 {
|
||||
STATE.with(|state| state.borrow().last_error.as_ptr())
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn anisette_last_error_len() -> usize {
|
||||
STATE.with(|state| state.borrow().last_error.len())
|
||||
}
|
||||
82
src/idbfs.rs
Normal file
82
src/idbfs.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
#[cfg(target_os = "emscripten")]
|
||||
use std::ffi::CString;
|
||||
|
||||
fn normalize_mount_path(path: &str) -> String {
|
||||
let trimmed = path.trim();
|
||||
let no_slash = trimmed.trim_end_matches('/');
|
||||
let no_dot = no_slash.strip_prefix("./").unwrap_or(no_slash);
|
||||
|
||||
if no_dot.is_empty() {
|
||||
"/".to_string()
|
||||
} else if no_dot.starts_with('/') {
|
||||
no_dot.to_string()
|
||||
} else {
|
||||
format!("/{no_dot}")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "emscripten")]
|
||||
unsafe extern "C" {
|
||||
fn emscripten_run_script(script: *const core::ffi::c_char);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "emscripten")]
|
||||
fn run_script(script: &str) -> Result<(), String> {
|
||||
let script = CString::new(script).map_err(|e| format!("invalid JS script: {e}"))?;
|
||||
unsafe {
|
||||
emscripten_run_script(script.as_ptr());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "emscripten"))]
|
||||
fn run_script(_script: &str) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn init_idbfs_for_path(path: &str) -> Result<String, String> {
|
||||
let mount_path = normalize_mount_path(path);
|
||||
let script = format!(
|
||||
r#"(function() {{
|
||||
if (typeof FS === 'undefined' || typeof IDBFS === 'undefined') {{
|
||||
console.warn('[anisette-rs] FS/IDBFS unavailable');
|
||||
return;
|
||||
}}
|
||||
var mp = "{mount_path}";
|
||||
try {{ FS.mkdirTree(mp); }} catch (_e) {{}}
|
||||
try {{ FS.mount(IDBFS, {{}}, mp); }} catch (_e) {{}}
|
||||
FS.syncfs(true, function(err) {{
|
||||
if (err) {{
|
||||
console.error('[anisette-rs] IDBFS initial sync failed', err);
|
||||
}} else {{
|
||||
console.log('[anisette-rs] IDBFS ready at ' + mp);
|
||||
}}
|
||||
}});
|
||||
}})();"#,
|
||||
);
|
||||
run_script(&script)?;
|
||||
Ok(mount_path)
|
||||
}
|
||||
|
||||
pub fn sync_idbfs(populate_from_storage: bool) -> Result<(), String> {
|
||||
let populate = if populate_from_storage {
|
||||
"true"
|
||||
} else {
|
||||
"false"
|
||||
};
|
||||
let script = format!(
|
||||
r#"(function() {{
|
||||
if (typeof FS === 'undefined') {{
|
||||
return;
|
||||
}}
|
||||
FS.syncfs({populate}, function(err) {{
|
||||
if (err) {{
|
||||
console.error('[anisette-rs] IDBFS sync failed', err);
|
||||
}} else {{
|
||||
console.log('[anisette-rs] IDBFS sync done');
|
||||
}}
|
||||
}});
|
||||
}})();"#,
|
||||
);
|
||||
run_script(&script)
|
||||
}
|
||||
28
src/lib.rs
Normal file
28
src/lib.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
pub mod device;
|
||||
mod exports;
|
||||
pub mod idbfs;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod provisioning;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod provisioning_wasm;
|
||||
|
||||
mod adi;
|
||||
mod allocator;
|
||||
mod constants;
|
||||
mod debug;
|
||||
mod emu;
|
||||
mod errors;
|
||||
mod runtime;
|
||||
mod stub;
|
||||
mod util;
|
||||
|
||||
pub use adi::{Adi, AdiInit, OtpResult, ProvisioningStartResult};
|
||||
pub use allocator::Allocator;
|
||||
pub use device::{Device, DeviceData};
|
||||
pub use emu::EmuCore;
|
||||
pub use errors::VmError;
|
||||
pub use idbfs::{init_idbfs_for_path, sync_idbfs};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use provisioning::ProvisioningSession;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use provisioning_wasm::ProvisioningSession;
|
||||
235
src/provisioning.rs
Normal file
235
src/provisioning.rs
Normal file
@@ -0,0 +1,235 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Write as _;
|
||||
use std::fs;
|
||||
use std::io::Cursor;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context, Result, anyhow, bail};
|
||||
use base64::{Engine as _, engine::general_purpose::STANDARD};
|
||||
use chrono::Local;
|
||||
use plist::Value;
|
||||
use reqwest::Certificate;
|
||||
use reqwest::blocking::{Client, RequestBuilder};
|
||||
|
||||
use crate::Adi;
|
||||
use crate::device::DeviceData;
|
||||
|
||||
pub struct ProvisioningSession<'a> {
|
||||
adi: &'a mut Adi,
|
||||
device: &'a DeviceData,
|
||||
client: Client,
|
||||
url_bag: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl<'a> ProvisioningSession<'a> {
|
||||
pub fn new(
|
||||
adi: &'a mut Adi,
|
||||
device: &'a DeviceData,
|
||||
apple_root_pem: Option<PathBuf>,
|
||||
) -> Result<Self> {
|
||||
let client = build_http_client(apple_root_pem.as_deref())?;
|
||||
|
||||
Ok(Self {
|
||||
adi,
|
||||
device,
|
||||
client,
|
||||
url_bag: HashMap::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn provision(&mut self, dsid: u64) -> Result<()> {
|
||||
println!("ProvisioningSession.provision");
|
||||
if self.url_bag.is_empty() {
|
||||
self.load_url_bag()?;
|
||||
}
|
||||
|
||||
let start_url = self
|
||||
.url_bag
|
||||
.get("midStartProvisioning")
|
||||
.cloned()
|
||||
.ok_or_else(|| anyhow!("url bag missing midStartProvisioning"))?;
|
||||
|
||||
let finish_url = self
|
||||
.url_bag
|
||||
.get("midFinishProvisioning")
|
||||
.cloned()
|
||||
.ok_or_else(|| anyhow!("url bag missing midFinishProvisioning"))?;
|
||||
|
||||
let start_body = r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Header</key>
|
||||
<dict/>
|
||||
<key>Request</key>
|
||||
<dict/>
|
||||
</dict>
|
||||
</plist>"#;
|
||||
|
||||
let start_bytes = self.post_with_time(&start_url, start_body)?;
|
||||
let start_plist = parse_plist(&start_bytes)?;
|
||||
|
||||
let spim_b64 = plist_get_string_in_response(&start_plist, "spim")?;
|
||||
println!("{spim_b64}");
|
||||
let spim = STANDARD.decode(spim_b64.as_bytes())?;
|
||||
|
||||
let start = self.adi.start_provisioning(dsid, &spim)?;
|
||||
println!("{}", bytes_to_hex(&start.cpim));
|
||||
let cpim_b64 = STANDARD.encode(&start.cpim);
|
||||
|
||||
let finish_body = format!(
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n <key>Header</key>\n <dict/>\n <key>Request</key>\n <dict>\n <key>cpim</key>\n <string>{}</string>\n </dict>\n</dict>\n</plist>",
|
||||
cpim_b64
|
||||
);
|
||||
|
||||
let finish_bytes = self.post_with_time(&finish_url, &finish_body)?;
|
||||
let finish_plist = parse_plist(&finish_bytes)?;
|
||||
|
||||
let ptm_b64 = plist_get_string_in_response(&finish_plist, "ptm")?;
|
||||
let tk_b64 = plist_get_string_in_response(&finish_plist, "tk")?;
|
||||
|
||||
let ptm = STANDARD.decode(ptm_b64.as_bytes())?;
|
||||
let tk = STANDARD.decode(tk_b64.as_bytes())?;
|
||||
|
||||
self.adi.end_provisioning(start.session, &ptm, &tk)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_url_bag(&mut self) -> Result<()> {
|
||||
let bytes = self.get("https://gsa.apple.com/grandslam/GsService2/lookup")?;
|
||||
let plist = parse_plist(&bytes)?;
|
||||
|
||||
let root = plist
|
||||
.as_dictionary()
|
||||
.ok_or_else(|| anyhow!("lookup plist root is not a dictionary"))?;
|
||||
let urls = root
|
||||
.get("urls")
|
||||
.and_then(Value::as_dictionary)
|
||||
.ok_or_else(|| anyhow!("lookup plist missing urls dictionary"))?;
|
||||
|
||||
self.url_bag.clear();
|
||||
for (name, value) in urls {
|
||||
if let Some(url) = value.as_string() {
|
||||
self.url_bag.insert(name.to_string(), url.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get(&self, url: &str) -> Result<Vec<u8>> {
|
||||
let request = self.with_common_headers(self.client.get(url), None);
|
||||
let response = request.send()?.error_for_status()?;
|
||||
Ok(response.bytes()?.to_vec())
|
||||
}
|
||||
|
||||
fn post_with_time(&self, url: &str, body: &str) -> Result<Vec<u8>> {
|
||||
let client_time = current_client_time();
|
||||
let request = self.with_common_headers(
|
||||
self.client.post(url).body(body.to_string()),
|
||||
Some(&client_time),
|
||||
);
|
||||
let response = request.send()?.error_for_status()?;
|
||||
Ok(response.bytes()?.to_vec())
|
||||
}
|
||||
|
||||
fn with_common_headers(
|
||||
&self,
|
||||
request: RequestBuilder,
|
||||
client_time: Option<&str>,
|
||||
) -> RequestBuilder {
|
||||
let mut request = request
|
||||
.header("User-Agent", "akd/1.0 CFNetwork/1404.0.5 Darwin/22.3.0")
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
.header("Connection", "keep-alive")
|
||||
.header("X-Mme-Device-Id", &self.device.unique_device_identifier)
|
||||
.header(
|
||||
"X-MMe-Client-Info",
|
||||
&self.device.server_friendly_description,
|
||||
)
|
||||
.header("X-Apple-I-MD-LU", &self.device.local_user_uuid)
|
||||
.header("X-Apple-Client-App-Name", "Setup");
|
||||
|
||||
if let Some(time) = client_time {
|
||||
request = request.header("X-Apple-I-Client-Time", time);
|
||||
}
|
||||
|
||||
request
|
||||
}
|
||||
}
|
||||
|
||||
fn bytes_to_hex(bytes: &[u8]) -> String {
|
||||
let mut output = String::with_capacity(bytes.len() * 2);
|
||||
for byte in bytes {
|
||||
let _ = write!(output, "{byte:02x}");
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
fn build_http_client(apple_root_pem: Option<&Path>) -> Result<Client> {
|
||||
let mut builder = Client::builder().timeout(Duration::from_secs(5));
|
||||
|
||||
if let Some(cert) = load_apple_root_cert(apple_root_pem)? {
|
||||
builder = builder.add_root_certificate(cert);
|
||||
} else {
|
||||
eprintln!("warning: apple-root.pem not found, falling back to insecure TLS mode");
|
||||
builder = builder.danger_accept_invalid_certs(true);
|
||||
}
|
||||
|
||||
Ok(builder.build()?)
|
||||
}
|
||||
|
||||
fn load_apple_root_cert(explicit_path: Option<&Path>) -> Result<Option<Certificate>> {
|
||||
let mut candidates: Vec<PathBuf> = Vec::new();
|
||||
|
||||
if let Some(path) = explicit_path {
|
||||
candidates.push(path.to_path_buf());
|
||||
}
|
||||
|
||||
candidates.push(PathBuf::from("apple-root.pem"));
|
||||
candidates.push(PathBuf::from(
|
||||
"/Users/libr/Desktop/Life/Anisette.py/src/anisette/apple-root.pem",
|
||||
));
|
||||
|
||||
for candidate in candidates {
|
||||
if candidate.exists() {
|
||||
let pem = fs::read(&candidate)
|
||||
.with_context(|| format!("failed to read certificate {}", candidate.display()))?;
|
||||
let cert = Certificate::from_pem(&pem)
|
||||
.with_context(|| format!("invalid certificate pem {}", candidate.display()))?;
|
||||
return Ok(Some(cert));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn parse_plist(bytes: &[u8]) -> Result<Value> {
|
||||
Ok(Value::from_reader_xml(Cursor::new(bytes))?)
|
||||
}
|
||||
|
||||
fn plist_get_string_in_response<'a>(plist: &'a Value, key: &str) -> Result<&'a str> {
|
||||
let root = plist
|
||||
.as_dictionary()
|
||||
.ok_or_else(|| anyhow!("plist root is not a dictionary"))?;
|
||||
|
||||
let response = root
|
||||
.get("Response")
|
||||
.and_then(Value::as_dictionary)
|
||||
.ok_or_else(|| anyhow!("plist missing Response dictionary"))?;
|
||||
|
||||
let value = response
|
||||
.get(key)
|
||||
.ok_or_else(|| anyhow!("plist Response missing {key}"))?;
|
||||
|
||||
if let Some(text) = value.as_string() {
|
||||
return Ok(text);
|
||||
}
|
||||
|
||||
bail!("plist Response field {key} is not a string")
|
||||
}
|
||||
|
||||
fn current_client_time() -> String {
|
||||
Local::now().format("%Y-%m-%dT%H:%M:%S%:z").to_string()
|
||||
}
|
||||
241
src/provisioning_wasm.rs
Normal file
241
src/provisioning_wasm.rs
Normal file
@@ -0,0 +1,241 @@
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::io::Cursor;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::{Context, Result, anyhow, bail};
|
||||
use base64::{Engine as _, engine::general_purpose::STANDARD};
|
||||
use chrono::Utc;
|
||||
use plist::Value;
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::Adi;
|
||||
use crate::device::DeviceData;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct JsHttpResponse {
|
||||
status: u16,
|
||||
body: String,
|
||||
#[serde(default)]
|
||||
error: String,
|
||||
}
|
||||
|
||||
pub struct ProvisioningSession<'a> {
|
||||
adi: &'a mut Adi,
|
||||
device: &'a DeviceData,
|
||||
url_bag: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl<'a> ProvisioningSession<'a> {
|
||||
pub fn new(
|
||||
adi: &'a mut Adi,
|
||||
device: &'a DeviceData,
|
||||
_apple_root_pem: Option<PathBuf>,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
adi,
|
||||
device,
|
||||
url_bag: HashMap::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn provision(&mut self, dsid: u64) -> Result<()> {
|
||||
println!("ProvisioningSession.provision");
|
||||
if self.url_bag.is_empty() {
|
||||
self.load_url_bag()?;
|
||||
}
|
||||
|
||||
let start_url = self
|
||||
.url_bag
|
||||
.get("midStartProvisioning")
|
||||
.cloned()
|
||||
.ok_or_else(|| anyhow!("url bag missing midStartProvisioning"))?;
|
||||
|
||||
let finish_url = self
|
||||
.url_bag
|
||||
.get("midFinishProvisioning")
|
||||
.cloned()
|
||||
.ok_or_else(|| anyhow!("url bag missing midFinishProvisioning"))?;
|
||||
|
||||
let start_body = r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Header</key>
|
||||
<dict/>
|
||||
<key>Request</key>
|
||||
<dict/>
|
||||
</dict>
|
||||
</plist>"#;
|
||||
|
||||
let start_bytes = self.post_with_time(&start_url, start_body)?;
|
||||
let start_plist = parse_plist(&start_bytes)?;
|
||||
|
||||
let spim_b64 = plist_get_string_in_response(&start_plist, "spim")?;
|
||||
let spim = STANDARD.decode(spim_b64.as_bytes())?;
|
||||
|
||||
let start = self.adi.start_provisioning(dsid, &spim)?;
|
||||
let cpim_b64 = STANDARD.encode(&start.cpim);
|
||||
|
||||
let finish_body = format!(
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n <key>Header</key>\n <dict/>\n <key>Request</key>\n <dict>\n <key>cpim</key>\n <string>{}</string>\n </dict>\n</dict>\n</plist>",
|
||||
cpim_b64
|
||||
);
|
||||
|
||||
let finish_bytes = self.post_with_time(&finish_url, &finish_body)?;
|
||||
let finish_plist = parse_plist(&finish_bytes)?;
|
||||
|
||||
let ptm_b64 = plist_get_string_in_response(&finish_plist, "ptm")?;
|
||||
let tk_b64 = plist_get_string_in_response(&finish_plist, "tk")?;
|
||||
|
||||
let ptm = STANDARD.decode(ptm_b64.as_bytes())?;
|
||||
let tk = STANDARD.decode(tk_b64.as_bytes())?;
|
||||
|
||||
self.adi.end_provisioning(start.session, &ptm, &tk)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_url_bag(&mut self) -> Result<()> {
|
||||
let bytes = self.get("https://gsa.apple.com/grandslam/GsService2/lookup")?;
|
||||
let plist = parse_plist(&bytes)?;
|
||||
|
||||
let root = plist
|
||||
.as_dictionary()
|
||||
.ok_or_else(|| anyhow!("lookup plist root is not a dictionary"))?;
|
||||
let urls = root
|
||||
.get("urls")
|
||||
.and_then(Value::as_dictionary)
|
||||
.ok_or_else(|| anyhow!("lookup plist missing urls dictionary"))?;
|
||||
|
||||
self.url_bag.clear();
|
||||
for (name, value) in urls {
|
||||
if let Some(url) = value.as_string() {
|
||||
self.url_bag.insert(name.to_string(), url.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get(&self, url: &str) -> Result<Vec<u8>> {
|
||||
let request = json!({
|
||||
"url": url,
|
||||
"headers": self.common_headers(None),
|
||||
});
|
||||
self.call_http("anisette_http_get", request)
|
||||
}
|
||||
|
||||
fn post_with_time(&self, url: &str, body: &str) -> Result<Vec<u8>> {
|
||||
let client_time = current_client_time();
|
||||
let request = json!({
|
||||
"url": url,
|
||||
"headers": self.common_headers(Some(&client_time)),
|
||||
"body": body,
|
||||
});
|
||||
self.call_http("anisette_http_post", request)
|
||||
}
|
||||
|
||||
fn common_headers(&self, client_time: Option<&str>) -> HashMap<&'static str, String> {
|
||||
let mut headers = HashMap::from([
|
||||
(
|
||||
"User-Agent",
|
||||
"akd/1.0 CFNetwork/1404.0.5 Darwin/22.3.0".to_string(),
|
||||
),
|
||||
(
|
||||
"Content-Type",
|
||||
"application/x-www-form-urlencoded".to_string(),
|
||||
),
|
||||
("Connection", "keep-alive".to_string()),
|
||||
(
|
||||
"X-Mme-Device-Id",
|
||||
self.device.unique_device_identifier.clone(),
|
||||
),
|
||||
(
|
||||
"X-MMe-Client-Info",
|
||||
self.device.server_friendly_description.clone(),
|
||||
),
|
||||
("X-Apple-I-MD-LU", self.device.local_user_uuid.clone()),
|
||||
("X-Apple-Client-App-Name", "Setup".to_string()),
|
||||
]);
|
||||
|
||||
if let Some(time) = client_time {
|
||||
headers.insert("X-Apple-I-Client-Time", time.to_string());
|
||||
}
|
||||
|
||||
headers
|
||||
}
|
||||
|
||||
fn call_http(&self, name: &str, payload: serde_json::Value) -> Result<Vec<u8>> {
|
||||
// JS callback must return JSON: { status: number, body: base64, error?: string }.
|
||||
let payload_json = serde_json::to_string(&payload)?;
|
||||
let script = format!(
|
||||
"(function(){{var fn = (typeof {name} === 'function') ? {name} : (typeof Module !== 'undefined' ? Module.{name} : null); return fn ? fn({payload_json}) : '';}})();"
|
||||
);
|
||||
let response_json = run_script_string(&script)?;
|
||||
if response_json.trim().is_empty() {
|
||||
bail!("missing JS http callback {name}");
|
||||
}
|
||||
|
||||
let response: JsHttpResponse = serde_json::from_str(&response_json)
|
||||
.with_context(|| format!("invalid JS http response for {name}"))?;
|
||||
if !response.error.trim().is_empty() {
|
||||
bail!("js http error: {}", response.error);
|
||||
}
|
||||
if response.status >= 400 {
|
||||
bail!("js http status {} for {}", response.status, name);
|
||||
}
|
||||
|
||||
let bytes = STANDARD
|
||||
.decode(response.body.as_bytes())
|
||||
.map_err(|e| anyhow!("base64 decode failed: {e}"))?;
|
||||
Ok(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "emscripten")]
|
||||
unsafe extern "C" {
|
||||
fn emscripten_run_script_string(script: *const core::ffi::c_char) -> *mut core::ffi::c_char;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "emscripten")]
|
||||
fn run_script_string(script: &str) -> Result<String> {
|
||||
let script = CString::new(script).map_err(|e| anyhow!("invalid JS script: {e}"))?;
|
||||
let ptr = unsafe { emscripten_run_script_string(script.as_ptr()) };
|
||||
if ptr.is_null() {
|
||||
return Err(anyhow!("emscripten_run_script_string returned null"));
|
||||
}
|
||||
let text = unsafe { CStr::from_ptr(ptr) }
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
Ok(text)
|
||||
}
|
||||
|
||||
fn parse_plist(bytes: &[u8]) -> Result<Value> {
|
||||
Ok(Value::from_reader_xml(Cursor::new(bytes))?)
|
||||
}
|
||||
|
||||
fn plist_get_string_in_response<'a>(plist: &'a Value, key: &str) -> Result<&'a str> {
|
||||
let root = plist
|
||||
.as_dictionary()
|
||||
.ok_or_else(|| anyhow!("plist root is not a dictionary"))?;
|
||||
|
||||
let response = root
|
||||
.get("Response")
|
||||
.and_then(Value::as_dictionary)
|
||||
.ok_or_else(|| anyhow!("plist missing Response dictionary"))?;
|
||||
|
||||
let value = response
|
||||
.get(key)
|
||||
.ok_or_else(|| anyhow!("plist Response missing {key}"))?;
|
||||
|
||||
if let Some(text) = value.as_string() {
|
||||
return Ok(text);
|
||||
}
|
||||
|
||||
bail!("plist Response field {key} is not a string")
|
||||
}
|
||||
|
||||
fn current_client_time() -> String {
|
||||
Utc::now().format("%Y-%m-%dT%H:%M:%S%:z").to_string()
|
||||
}
|
||||
47
src/runtime.rs
Normal file
47
src/runtime.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
|
||||
use crate::allocator::Allocator;
|
||||
use crate::constants::{
|
||||
LIB_ALLOC_BASE, LIB_ALLOC_SIZE, MALLOC_ADDRESS, MALLOC_SIZE, TEMP_ALLOC_BASE, TEMP_ALLOC_SIZE,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct SymbolEntry {
|
||||
pub(crate) name: String,
|
||||
pub(crate) resolved: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct LoadedLibrary {
|
||||
pub(crate) name: String,
|
||||
pub(crate) symbols: Vec<SymbolEntry>,
|
||||
pub(crate) symbols_by_name: HashMap<String, u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct RuntimeState {
|
||||
pub(crate) temp_allocator: Allocator,
|
||||
pub(crate) library_allocator: Allocator,
|
||||
pub(crate) malloc_allocator: Allocator,
|
||||
pub(crate) errno_address: Option<u64>,
|
||||
pub(crate) library_blobs: HashMap<String, Vec<u8>>,
|
||||
pub(crate) loaded_libraries: Vec<LoadedLibrary>,
|
||||
pub(crate) file_handles: Vec<Option<File>>,
|
||||
pub(crate) library_root: Option<String>,
|
||||
}
|
||||
|
||||
impl RuntimeState {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
temp_allocator: Allocator::new(TEMP_ALLOC_BASE, TEMP_ALLOC_SIZE),
|
||||
library_allocator: Allocator::new(LIB_ALLOC_BASE, LIB_ALLOC_SIZE),
|
||||
malloc_allocator: Allocator::new(MALLOC_ADDRESS, MALLOC_SIZE),
|
||||
errno_address: None,
|
||||
library_blobs: HashMap::new(),
|
||||
loaded_libraries: Vec::new(),
|
||||
file_handles: Vec::new(),
|
||||
library_root: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
624
src/stub.rs
Normal file
624
src/stub.rs
Normal file
@@ -0,0 +1,624 @@
|
||||
use std::fs::{self, OpenOptions};
|
||||
use std::io::{Read, Write};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use unicorn_engine::{RegisterARM64, Unicorn};
|
||||
|
||||
use crate::constants::{
|
||||
ENOENT, IMPORT_ADDRESS, IMPORT_LIBRARY_STRIDE, O_ACCMODE, O_CREAT, O_NOFOLLOW, O_RDWR, O_WRONLY,
|
||||
};
|
||||
use crate::debug::{debug_print, debug_trace};
|
||||
use crate::emu::{
|
||||
ensure_errno_address, load_library_by_name, read_c_string,
|
||||
resolve_symbol_from_loaded_library_by_name, set_errno,
|
||||
};
|
||||
use crate::errors::VmError;
|
||||
use crate::runtime::RuntimeState;
|
||||
use crate::util::bytes_to_hex;
|
||||
|
||||
pub fn dispatch_import_stub(
|
||||
uc: &mut Unicorn<'_, RuntimeState>,
|
||||
address: u64,
|
||||
) -> Result<(), VmError> {
|
||||
if address < IMPORT_ADDRESS {
|
||||
return Err(VmError::InvalidImportAddress(address));
|
||||
}
|
||||
|
||||
let offset = address - IMPORT_ADDRESS;
|
||||
let library_index = (offset / IMPORT_LIBRARY_STRIDE) as usize;
|
||||
let symbol_index = ((offset % IMPORT_LIBRARY_STRIDE) / 4) as usize;
|
||||
|
||||
let symbol_name =
|
||||
{
|
||||
let state = uc.get_data();
|
||||
let library = state
|
||||
.loaded_libraries
|
||||
.get(library_index)
|
||||
.ok_or(VmError::LibraryNotLoaded(library_index))?;
|
||||
|
||||
let symbol = library.symbols.get(symbol_index).ok_or_else(|| {
|
||||
VmError::SymbolIndexOutOfRange {
|
||||
library: library.name.clone(),
|
||||
index: symbol_index,
|
||||
}
|
||||
})?;
|
||||
|
||||
symbol.name.clone()
|
||||
};
|
||||
|
||||
handle_stub_by_name(uc, &symbol_name)
|
||||
}
|
||||
|
||||
fn handle_stub_by_name(
|
||||
uc: &mut Unicorn<'_, RuntimeState>,
|
||||
symbol_name: &str,
|
||||
) -> Result<(), VmError> {
|
||||
match symbol_name {
|
||||
"malloc" => stub_malloc(uc),
|
||||
"free" => stub_free(uc),
|
||||
"strncpy" => stub_strncpy(uc),
|
||||
"mkdir" => stub_mkdir(uc),
|
||||
"umask" => stub_umask(uc),
|
||||
"chmod" => stub_chmod(uc),
|
||||
"lstat" => stub_lstat(uc),
|
||||
"fstat" => stub_fstat(uc),
|
||||
"open" => stub_open(uc),
|
||||
"ftruncate" => stub_ftruncate(uc),
|
||||
"read" => stub_read(uc),
|
||||
"write" => stub_write(uc),
|
||||
"close" => stub_close(uc),
|
||||
"dlopen" => stub_dlopen(uc),
|
||||
"dlsym" => stub_dlsym(uc),
|
||||
"dlclose" => stub_dlclose(uc),
|
||||
"pthread_once" => stub_return_zero(uc),
|
||||
"pthread_create" => stub_return_zero(uc),
|
||||
"pthread_mutex_lock" => stub_return_zero(uc),
|
||||
"pthread_rwlock_unlock" => stub_return_zero(uc),
|
||||
"pthread_rwlock_destroy" => stub_return_zero(uc),
|
||||
"pthread_rwlock_wrlock" => stub_return_zero(uc),
|
||||
"pthread_rwlock_init" => stub_return_zero(uc),
|
||||
"pthread_mutex_unlock" => stub_return_zero(uc),
|
||||
"pthread_rwlock_rdlock" => stub_return_zero(uc),
|
||||
"gettimeofday" => stub_gettimeofday(uc),
|
||||
"__errno" => stub_errno_location(uc),
|
||||
"__system_property_get" => stub_system_property_get(uc),
|
||||
"arc4random" => stub_arc4random(uc),
|
||||
other => {
|
||||
debug_print(other);
|
||||
Err(VmError::UnhandledImport(other.to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn stub_return_zero(uc: &mut Unicorn<'_, RuntimeState>) -> Result<(), VmError> {
|
||||
uc.reg_write(RegisterARM64::X0, 0)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stub_malloc(uc: &mut Unicorn<'_, RuntimeState>) -> Result<(), VmError> {
|
||||
let request = uc.reg_read(RegisterARM64::X0)?;
|
||||
let address = {
|
||||
let state = uc.get_data_mut();
|
||||
state.malloc_allocator.alloc(request)?
|
||||
};
|
||||
|
||||
debug_trace(format!("malloc(0x{request:X})=0x{address:X}"));
|
||||
uc.reg_write(RegisterARM64::X0, address)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stub_free(uc: &mut Unicorn<'_, RuntimeState>) -> Result<(), VmError> {
|
||||
uc.reg_write(RegisterARM64::X0, 0)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stub_strncpy(uc: &mut Unicorn<'_, RuntimeState>) -> Result<(), VmError> {
|
||||
let dst = uc.reg_read(RegisterARM64::X0)?;
|
||||
let src = uc.reg_read(RegisterARM64::X1)?;
|
||||
let length = uc.reg_read(RegisterARM64::X2)? as usize;
|
||||
|
||||
let input = uc.mem_read_as_vec(src, length)?;
|
||||
let copy_len = input
|
||||
.iter()
|
||||
.position(|byte| *byte == 0)
|
||||
.unwrap_or(length)
|
||||
.min(length);
|
||||
|
||||
let mut output = vec![0_u8; length];
|
||||
output[..copy_len].copy_from_slice(&input[..copy_len]);
|
||||
|
||||
uc.mem_write(dst, &output)?;
|
||||
uc.reg_write(RegisterARM64::X0, dst)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stub_mkdir(uc: &mut Unicorn<'_, RuntimeState>) -> Result<(), VmError> {
|
||||
let path_ptr = uc.reg_read(RegisterARM64::X0)?;
|
||||
let mode = uc.reg_read(RegisterARM64::X1)?;
|
||||
let path = read_c_string(uc, path_ptr, 0x1000)?;
|
||||
debug_trace(format!("mkdir('{path}', {mode:#o})"));
|
||||
|
||||
// Only allow creating ./anisette directory (matches Python reference impl)
|
||||
if path != "./anisette" {
|
||||
debug_print(format!("mkdir: rejecting invalid path '{path}'"));
|
||||
set_errno(uc, ENOENT)?;
|
||||
uc.reg_write(RegisterARM64::X0, u64::MAX)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match fs::create_dir_all(&path) {
|
||||
Ok(()) => {
|
||||
uc.reg_write(RegisterARM64::X0, 0)?;
|
||||
}
|
||||
Err(_) => {
|
||||
set_errno(uc, ENOENT)?;
|
||||
uc.reg_write(RegisterARM64::X0, u64::MAX)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stub_umask(uc: &mut Unicorn<'_, RuntimeState>) -> Result<(), VmError> {
|
||||
uc.reg_write(RegisterARM64::X0, 0o777)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stub_chmod(uc: &mut Unicorn<'_, RuntimeState>) -> Result<(), VmError> {
|
||||
let path_ptr = uc.reg_read(RegisterARM64::X0)?;
|
||||
let mode = uc.reg_read(RegisterARM64::X1)?;
|
||||
let path = read_c_string(uc, path_ptr, 0x1000)?;
|
||||
debug_trace(format!("chmod('{path}', {mode:#o})"));
|
||||
uc.reg_write(RegisterARM64::X0, 0)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_python_stat_bytes(mode: u32, size: u64) -> Vec<u8> {
|
||||
let mut stat = Vec::with_capacity(128);
|
||||
|
||||
stat.extend_from_slice(&[0_u8; 8]); // st_dev
|
||||
stat.extend_from_slice(&[0_u8; 8]); // st_ino
|
||||
stat.extend_from_slice(&mode.to_le_bytes()); // st_mode
|
||||
stat.extend_from_slice(&[0_u8; 4]); // st_nlink
|
||||
stat.extend_from_slice(&[0xA4, 0x81, 0x00, 0x00]); // st_uid
|
||||
stat.extend_from_slice(&[0_u8; 4]); // st_gid
|
||||
stat.extend_from_slice(&[0_u8; 8]); // st_rdev
|
||||
stat.extend_from_slice(&[0_u8; 8]); // __pad1
|
||||
stat.extend_from_slice(&size.to_le_bytes()); // st_size
|
||||
stat.extend_from_slice(&[0_u8; 4]); // st_blksize
|
||||
stat.extend_from_slice(&[0_u8; 4]); // __pad2
|
||||
stat.extend_from_slice(&[0_u8; 8]); // st_blocks
|
||||
stat.extend_from_slice(&[0_u8; 8]); // st_atime
|
||||
stat.extend_from_slice(&[0_u8; 8]); // st_atime_nsec
|
||||
stat.extend_from_slice(&[0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00]); // st_mtime
|
||||
stat.extend_from_slice(&[0_u8; 8]); // st_mtime_nsec
|
||||
stat.extend_from_slice(&[0_u8; 8]); // st_ctime
|
||||
stat.extend_from_slice(&[0_u8; 8]); // st_ctime_nsec
|
||||
stat.extend_from_slice(&[0_u8; 4]); // __unused4
|
||||
stat.extend_from_slice(&[0_u8; 4]); // __unused5
|
||||
|
||||
stat
|
||||
}
|
||||
|
||||
fn write_python_stat(
|
||||
uc: &mut Unicorn<'_, RuntimeState>,
|
||||
out_ptr: u64,
|
||||
mode: u32,
|
||||
size: u64,
|
||||
stat_blksize: u64,
|
||||
stat_blocks: u64,
|
||||
) -> Result<(), VmError> {
|
||||
debug_print(format!("{size} {stat_blksize} {stat_blocks}"));
|
||||
|
||||
let fake_blksize = 512_u64;
|
||||
let fake_blocks = size.div_ceil(512);
|
||||
debug_print(format!("{size} {fake_blksize} {fake_blocks}"));
|
||||
|
||||
debug_print(format!("0x{mode:X} = {mode}"));
|
||||
let stat_bytes = build_python_stat_bytes(mode, size);
|
||||
debug_print(format!("{}", stat_bytes.len()));
|
||||
debug_print(format!("Write to ptr: 0x{out_ptr:X}"));
|
||||
uc.mem_write(out_ptr, &stat_bytes)?;
|
||||
debug_print("Stat struct written to guest memory");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stat_path_into_guest(
|
||||
uc: &mut Unicorn<'_, RuntimeState>,
|
||||
path: &str,
|
||||
out_ptr: u64,
|
||||
) -> Result<(), VmError> {
|
||||
let metadata = match fs::symlink_metadata(path) {
|
||||
Ok(metadata) => metadata,
|
||||
Err(_) => {
|
||||
debug_print(format!("Unable to stat '{path}'"));
|
||||
set_errno(uc, ENOENT)?;
|
||||
uc.reg_write(RegisterARM64::X0, u64::MAX)?;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
write_python_stat(
|
||||
uc,
|
||||
out_ptr,
|
||||
metadata.mode(),
|
||||
metadata.size(),
|
||||
metadata.blksize(),
|
||||
metadata.blocks(),
|
||||
)?;
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
write_python_stat(uc, out_ptr, 0, metadata.len(), 0, 0)?;
|
||||
}
|
||||
|
||||
uc.reg_write(RegisterARM64::X0, 0)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stat_fd_into_guest(
|
||||
uc: &mut Unicorn<'_, RuntimeState>,
|
||||
fd: u64,
|
||||
out_ptr: u64,
|
||||
) -> Result<(), VmError> {
|
||||
let fd_index = usize::try_from(fd).map_err(|_| VmError::InvalidFileDescriptor(fd))?;
|
||||
|
||||
let metadata = {
|
||||
let state = uc.get_data_mut();
|
||||
let slot = state
|
||||
.file_handles
|
||||
.get_mut(fd_index)
|
||||
.ok_or(VmError::InvalidFileDescriptor(fd))?;
|
||||
let file = slot.as_mut().ok_or(VmError::InvalidFileDescriptor(fd))?;
|
||||
file.metadata()
|
||||
};
|
||||
|
||||
let metadata = match metadata {
|
||||
Ok(metadata) => metadata,
|
||||
Err(_) => {
|
||||
debug_print(format!("Unable to stat '{fd}'"));
|
||||
set_errno(uc, ENOENT)?;
|
||||
uc.reg_write(RegisterARM64::X0, u64::MAX)?;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
write_python_stat(
|
||||
uc,
|
||||
out_ptr,
|
||||
metadata.mode(),
|
||||
metadata.size(),
|
||||
metadata.blksize(),
|
||||
metadata.blocks(),
|
||||
)?;
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
write_python_stat(uc, out_ptr, 0, metadata.len(), 0, 0)?;
|
||||
}
|
||||
|
||||
uc.reg_write(RegisterARM64::X0, 0)?;
|
||||
Ok(())
|
||||
}
|
||||
fn stub_lstat(uc: &mut Unicorn<'_, RuntimeState>) -> Result<(), VmError> {
|
||||
let path_ptr = uc.reg_read(RegisterARM64::X0)?;
|
||||
let out_ptr = uc.reg_read(RegisterARM64::X1)?;
|
||||
let path = read_c_string(uc, path_ptr, 0x1000)?;
|
||||
debug_trace(format!(
|
||||
"lstat(0x{path_ptr:X}:'{path}', [x1:0x{out_ptr:X}])"
|
||||
));
|
||||
stat_path_into_guest(uc, &path, out_ptr)
|
||||
}
|
||||
|
||||
fn stub_fstat(uc: &mut Unicorn<'_, RuntimeState>) -> Result<(), VmError> {
|
||||
let fd = uc.reg_read(RegisterARM64::X0)?;
|
||||
let out_ptr = uc.reg_read(RegisterARM64::X1)?;
|
||||
debug_trace(format!("fstat({fd}, [...])"));
|
||||
stat_fd_into_guest(uc, fd, out_ptr)
|
||||
}
|
||||
|
||||
fn stub_open(uc: &mut Unicorn<'_, RuntimeState>) -> Result<(), VmError> {
|
||||
let path_ptr = uc.reg_read(RegisterARM64::X0)?;
|
||||
let flags = uc.reg_read(RegisterARM64::X1)?;
|
||||
let mode = uc.reg_read(RegisterARM64::X2)?;
|
||||
let path = read_c_string(uc, path_ptr, 0x1000)?;
|
||||
if path.is_empty() {
|
||||
return Err(VmError::EmptyPath);
|
||||
}
|
||||
|
||||
debug_trace(format!("open('{path}', {flags:#o}, {mode:#o})"));
|
||||
// Only allow access to ./anisette/adi.pb (matches Python reference impl)
|
||||
if path != "./anisette/adi.pb" {
|
||||
debug_print(format!("open: rejecting invalid path '{path}'"));
|
||||
set_errno(uc, ENOENT)?;
|
||||
uc.reg_write(RegisterARM64::X0, u64::MAX)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if flags != O_NOFOLLOW && flags != (O_NOFOLLOW | O_CREAT | O_WRONLY) {
|
||||
debug_print(format!("open: rejecting unsupported flags {flags:#o}"));
|
||||
set_errno(uc, ENOENT)?;
|
||||
uc.reg_write(RegisterARM64::X0, u64::MAX)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut options = OpenOptions::new();
|
||||
let access_mode = flags & O_ACCMODE;
|
||||
let _write_only = access_mode == O_WRONLY;
|
||||
let create = (flags & O_CREAT) != 0;
|
||||
|
||||
match access_mode {
|
||||
0 => {
|
||||
options.read(true);
|
||||
}
|
||||
O_WRONLY => {
|
||||
options.write(true).truncate(true);
|
||||
}
|
||||
O_RDWR => {
|
||||
options.read(true).write(true);
|
||||
}
|
||||
_ => {
|
||||
set_errno(uc, ENOENT)?;
|
||||
uc.reg_write(RegisterARM64::X0, u64::MAX)?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
if create {
|
||||
options.create(true).read(true).write(true);
|
||||
if let Some(parent) = std::path::Path::new(&path).parent() {
|
||||
let _ = fs::create_dir_all(parent);
|
||||
}
|
||||
}
|
||||
|
||||
if (flags & O_NOFOLLOW) == 0 {
|
||||
debug_trace("open without O_NOFOLLOW");
|
||||
}
|
||||
|
||||
match options.open(&path) {
|
||||
Ok(file) => {
|
||||
let fd = {
|
||||
let state = uc.get_data_mut();
|
||||
state.file_handles.push(Some(file));
|
||||
(state.file_handles.len() - 1) as u64
|
||||
};
|
||||
|
||||
uc.reg_write(RegisterARM64::X0, fd)?;
|
||||
}
|
||||
Err(_) => {
|
||||
set_errno(uc, ENOENT)?;
|
||||
uc.reg_write(RegisterARM64::X0, u64::MAX)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stub_ftruncate(uc: &mut Unicorn<'_, RuntimeState>) -> Result<(), VmError> {
|
||||
let fd = uc.reg_read(RegisterARM64::X0)?;
|
||||
let length = uc.reg_read(RegisterARM64::X1)?;
|
||||
debug_trace(format!("ftruncate({fd}, {length})"));
|
||||
let fd_index = usize::try_from(fd).map_err(|_| VmError::InvalidFileDescriptor(fd))?;
|
||||
|
||||
let result = {
|
||||
let state = uc.get_data_mut();
|
||||
let slot = state
|
||||
.file_handles
|
||||
.get_mut(fd_index)
|
||||
.ok_or(VmError::InvalidFileDescriptor(fd))?;
|
||||
let file = slot.as_mut().ok_or(VmError::InvalidFileDescriptor(fd))?;
|
||||
file.set_len(length)
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(()) => uc.reg_write(RegisterARM64::X0, 0)?,
|
||||
Err(_) => {
|
||||
set_errno(uc, ENOENT)?;
|
||||
uc.reg_write(RegisterARM64::X0, u64::MAX)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stub_read(uc: &mut Unicorn<'_, RuntimeState>) -> Result<(), VmError> {
|
||||
let fd = uc.reg_read(RegisterARM64::X0)?;
|
||||
let buf_ptr = uc.reg_read(RegisterARM64::X1)?;
|
||||
let count = uc.reg_read(RegisterARM64::X2)? as usize;
|
||||
|
||||
let fd_index = usize::try_from(fd).map_err(|_| VmError::InvalidFileDescriptor(fd))?;
|
||||
|
||||
let mut buffer = vec![0_u8; count];
|
||||
|
||||
let read_size = {
|
||||
let state = uc.get_data_mut();
|
||||
let slot = state
|
||||
.file_handles
|
||||
.get_mut(fd_index)
|
||||
.ok_or(VmError::InvalidFileDescriptor(fd))?;
|
||||
let file = slot.as_mut().ok_or(VmError::InvalidFileDescriptor(fd))?;
|
||||
file.read(&mut buffer)
|
||||
};
|
||||
debug_trace(format!("read({fd}, 0x{buf_ptr:X}, {count})={read_size:?}"));
|
||||
match read_size {
|
||||
Ok(read_size) => {
|
||||
uc.mem_write(buf_ptr, &buffer[..read_size])?;
|
||||
uc.reg_write(RegisterARM64::X0, read_size as u64)?;
|
||||
}
|
||||
Err(_) => {
|
||||
set_errno(uc, ENOENT)?;
|
||||
uc.reg_write(RegisterARM64::X0, u64::MAX)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stub_write(uc: &mut Unicorn<'_, RuntimeState>) -> Result<(), VmError> {
|
||||
let fd = uc.reg_read(RegisterARM64::X0)?;
|
||||
let buf_ptr = uc.reg_read(RegisterARM64::X1)?;
|
||||
let count = uc.reg_read(RegisterARM64::X2)? as usize;
|
||||
debug_trace(format!("write({fd}, 0x{buf_ptr:X}, {count})"));
|
||||
let fd_index = usize::try_from(fd).map_err(|_| VmError::InvalidFileDescriptor(fd))?;
|
||||
|
||||
let bytes = uc.mem_read_as_vec(buf_ptr, count)?;
|
||||
|
||||
let write_size = {
|
||||
let state = uc.get_data_mut();
|
||||
let slot = state
|
||||
.file_handles
|
||||
.get_mut(fd_index)
|
||||
.ok_or(VmError::InvalidFileDescriptor(fd))?;
|
||||
let file = slot.as_mut().ok_or(VmError::InvalidFileDescriptor(fd))?;
|
||||
file.write_all(&bytes)
|
||||
};
|
||||
|
||||
match write_size {
|
||||
Ok(()) => uc.reg_write(RegisterARM64::X0, count as u64)?,
|
||||
Err(_) => {
|
||||
set_errno(uc, ENOENT)?;
|
||||
uc.reg_write(RegisterARM64::X0, u64::MAX)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stub_close(uc: &mut Unicorn<'_, RuntimeState>) -> Result<(), VmError> {
|
||||
let fd = uc.reg_read(RegisterARM64::X0)?;
|
||||
let fd_index = usize::try_from(fd).map_err(|_| VmError::InvalidFileDescriptor(fd))?;
|
||||
|
||||
let state = uc.get_data_mut();
|
||||
let slot = state
|
||||
.file_handles
|
||||
.get_mut(fd_index)
|
||||
.ok_or(VmError::InvalidFileDescriptor(fd))?;
|
||||
*slot = None;
|
||||
|
||||
uc.reg_write(RegisterARM64::X0, 0)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stub_dlopen(uc: &mut Unicorn<'_, RuntimeState>) -> Result<(), VmError> {
|
||||
let path_ptr = uc.reg_read(RegisterARM64::X0)?;
|
||||
let path = read_c_string(uc, path_ptr, 0x1000)?;
|
||||
|
||||
let library_name = path.rsplit('/').next().ok_or(VmError::EmptyPath)?;
|
||||
debug_trace(format!("dlopen('{path}' ({library_name}))"));
|
||||
let library_index = load_library_by_name(uc, library_name)?;
|
||||
|
||||
uc.reg_write(RegisterARM64::X0, (library_index + 1) as u64)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stub_dlsym(uc: &mut Unicorn<'_, RuntimeState>) -> Result<(), VmError> {
|
||||
let handle = uc.reg_read(RegisterARM64::X0)?;
|
||||
if handle == 0 {
|
||||
return Err(VmError::InvalidDlopenHandle(handle));
|
||||
}
|
||||
|
||||
let symbol_ptr = uc.reg_read(RegisterARM64::X1)?;
|
||||
let symbol_name = read_c_string(uc, symbol_ptr, 0x1000)?;
|
||||
let library_index = (handle - 1) as usize;
|
||||
|
||||
{
|
||||
let state = uc.get_data();
|
||||
if let Some(library) = state.loaded_libraries.get(library_index) {
|
||||
debug_trace(format!(
|
||||
"dlsym({handle:X} ({}), '{}')",
|
||||
library.name, symbol_name
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let symbol_address =
|
||||
resolve_symbol_from_loaded_library_by_name(uc, library_index, &symbol_name)?;
|
||||
debug_print(format!("Found at 0x{symbol_address:X}"));
|
||||
uc.reg_write(RegisterARM64::X0, symbol_address)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stub_dlclose(uc: &mut Unicorn<'_, RuntimeState>) -> Result<(), VmError> {
|
||||
uc.reg_write(RegisterARM64::X0, 0)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stub_gettimeofday(uc: &mut Unicorn<'_, RuntimeState>) -> Result<(), VmError> {
|
||||
let time_ptr = uc.reg_read(RegisterARM64::X0)?;
|
||||
let tz_ptr = uc.reg_read(RegisterARM64::X1)?;
|
||||
debug_trace(format!("gettimeofday(0x{time_ptr:X}, 0x{tz_ptr:X})"));
|
||||
if tz_ptr != 0 {
|
||||
return Err(VmError::UnhandledImport(format!(
|
||||
"gettimeofday tz pointer must be null, got 0x{tz_ptr:X}"
|
||||
)));
|
||||
}
|
||||
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default();
|
||||
let sec = now.as_secs();
|
||||
let usec = now.subsec_micros() as i64;
|
||||
|
||||
let mut timeval = [0_u8; 16];
|
||||
timeval[0..8].copy_from_slice(&sec.to_le_bytes());
|
||||
timeval[8..16].copy_from_slice(&usec.to_le_bytes());
|
||||
debug_print(format!(
|
||||
"{{'tv_sec': {sec}, 'tv_usec': {usec}}} {} {}",
|
||||
bytes_to_hex(&timeval),
|
||||
timeval.len()
|
||||
));
|
||||
|
||||
uc.mem_write(time_ptr, &timeval)?;
|
||||
uc.reg_write(RegisterARM64::X0, 0)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stub_errno_location(uc: &mut Unicorn<'_, RuntimeState>) -> Result<(), VmError> {
|
||||
if uc.get_data().errno_address.is_none() {
|
||||
debug_print("Checking errno before first error (!)");
|
||||
}
|
||||
let errno_address = ensure_errno_address(uc)?;
|
||||
uc.reg_write(RegisterARM64::X0, errno_address)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stub_system_property_get(uc: &mut Unicorn<'_, RuntimeState>) -> Result<(), VmError> {
|
||||
let name_ptr = uc.reg_read(RegisterARM64::X0)?;
|
||||
let name = read_c_string(uc, name_ptr, 0x1000)?;
|
||||
debug_trace(format!("__system_property_get({name}, [...])"));
|
||||
let value_ptr = uc.reg_read(RegisterARM64::X1)?;
|
||||
let value = b"no s/n number";
|
||||
uc.mem_write(value_ptr, value)?;
|
||||
uc.reg_write(RegisterARM64::X0, value.len() as u64)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stub_arc4random(uc: &mut Unicorn<'_, RuntimeState>) -> Result<(), VmError> {
|
||||
uc.reg_write(RegisterARM64::X0, 0xDEAD_BEEF)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Allocator;
|
||||
|
||||
#[test]
|
||||
fn allocator_aligns_to_pages() {
|
||||
let mut allocator = Allocator::new(0x1000_0000, 0x20_000);
|
||||
let a = allocator.alloc(1).expect("alloc 1");
|
||||
let b = allocator.alloc(0x1500).expect("alloc 2");
|
||||
|
||||
assert_eq!(a, 0x1000_0000);
|
||||
assert_eq!(b, 0x1000_1000);
|
||||
}
|
||||
}
|
||||
37
src/util.rs
Normal file
37
src/util.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use std::fmt::Write as _;
|
||||
|
||||
use crate::errors::VmError;
|
||||
|
||||
pub(crate) fn bytes_to_hex(bytes: &[u8]) -> String {
|
||||
let mut out = String::with_capacity(bytes.len() * 2);
|
||||
for byte in bytes {
|
||||
let _ = write!(out, "{byte:02x}");
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub(crate) fn align_up(value: u64, align: u64) -> u64 {
|
||||
if align == 0 {
|
||||
return value;
|
||||
}
|
||||
(value + align - 1) & !(align - 1)
|
||||
}
|
||||
|
||||
pub(crate) fn align_down(value: u64, align: u64) -> u64 {
|
||||
if align == 0 {
|
||||
return value;
|
||||
}
|
||||
value & !(align - 1)
|
||||
}
|
||||
|
||||
pub(crate) fn add_i64(base: u64, addend: i64) -> u64 {
|
||||
if addend >= 0 {
|
||||
base.wrapping_add(addend as u64)
|
||||
} else {
|
||||
base.wrapping_sub((-addend) as u64)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn as_usize(value: u64) -> Result<usize, VmError> {
|
||||
usize::try_from(value).map_err(|_| VmError::IntegerOverflow(value))
|
||||
}
|
||||
Reference in New Issue
Block a user