mirror of
https://github.com/Momoko-Ayase/Senbei.git
synced 2026-09-19 03:57:59 -04:00
refactor: align platform crate boundaries
This commit is contained in:
@@ -7,7 +7,6 @@ license.workspace = true
|
||||
description = "Platform unpacking engines for Senbei"
|
||||
|
||||
[dependencies]
|
||||
goblin.workspace = true
|
||||
memmap2.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
@@ -15,6 +14,8 @@ sha2.workspace = true
|
||||
tempfile.workspace = true
|
||||
thiserror.workspace = true
|
||||
senbei-crypto.workspace = true
|
||||
senbei-elf.workspace = true
|
||||
senbei-pe.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
//! Shared Android engine filesystem and digest helpers.
|
||||
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use sha2::{Digest, Sha256};
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
pub(crate) fn absolute(path: &Path) -> std::io::Result<PathBuf> {
|
||||
if path.is_absolute() {
|
||||
Ok(path.to_path_buf())
|
||||
} else {
|
||||
std::env::current_dir().map(|current| current.join(path))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn write_atomic(path: &Path, data: &[u8]) -> std::io::Result<()> {
|
||||
let parent = path.parent().unwrap_or_else(|| Path::new("."));
|
||||
std::fs::create_dir_all(parent)?;
|
||||
let mut temporary = NamedTempFile::new_in(parent)?;
|
||||
temporary.write_all(data)?;
|
||||
temporary.as_file().sync_all()?;
|
||||
temporary.persist(path).map_err(|error| error.error)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn sha256(data: &[u8]) -> String {
|
||||
let mut digest = Sha256::new();
|
||||
digest.update(data);
|
||||
senbei_crypto::hex_digest(&digest.finalize())
|
||||
}
|
||||
@@ -14,7 +14,7 @@ pub enum Error {
|
||||
Elf {
|
||||
path: PathBuf,
|
||||
#[source]
|
||||
source: goblin::error::Error,
|
||||
source: senbei_elf::Error,
|
||||
},
|
||||
#[error("serialize extraction index: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
use std::collections::{BTreeMap, BTreeSet, HashSet};
|
||||
use std::fs::{File, create_dir_all};
|
||||
use std::io::Write;
|
||||
use std::fs::File;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use memmap2::MmapOptions;
|
||||
use senbei_crypto::android::{Module9bConfig, decode_container};
|
||||
use serde_json::to_vec_pretty;
|
||||
use sha2::{Digest, Sha256};
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
use super::super::common;
|
||||
use super::error::{Error, Result, invalid};
|
||||
use super::report::{
|
||||
ArtifactReport, DecoderReport, ExtractionReport, ModuleRegistryEntry, RecordReport,
|
||||
@@ -86,7 +84,7 @@ pub fn extract_stage2(options: &ExtractOptions) -> Result<ExtractionReport> {
|
||||
return invalid("refusing to overwrite the protected ELF with Stage 2 output");
|
||||
}
|
||||
}
|
||||
create_dir_all(&output_dir)
|
||||
std::fs::create_dir_all(&output_dir)
|
||||
.map_err(|source| Error::io("create Stage 2 output directory", &output_dir, source))?;
|
||||
|
||||
let file = File::open(&input_path)
|
||||
@@ -497,42 +495,14 @@ fn write_json_atomic(path: &Path, value: &impl serde::Serialize) -> Result<()> {
|
||||
}
|
||||
|
||||
fn write_atomic(path: &Path, data: &[u8]) -> Result<()> {
|
||||
let parent = path.parent().unwrap_or_else(|| Path::new("."));
|
||||
create_dir_all(parent)
|
||||
.map_err(|source| Error::io("create output directory", parent, source))?;
|
||||
let mut temporary = NamedTempFile::new_in(parent)
|
||||
.map_err(|source| Error::io("create temporary output", parent, source))?;
|
||||
temporary
|
||||
.write_all(data)
|
||||
.and_then(|()| temporary.as_file().sync_all())
|
||||
.map_err(|source| Error::io("write temporary output", temporary.path(), source))?;
|
||||
temporary
|
||||
.persist(path)
|
||||
.map_err(|error| Error::io("replace output", path, error.error))?;
|
||||
Ok(())
|
||||
common::write_atomic(path, data)
|
||||
.map_err(|source| Error::io("write temporary output", path, source))
|
||||
}
|
||||
|
||||
fn absolute(path: &Path) -> Result<PathBuf> {
|
||||
if path.is_absolute() {
|
||||
Ok(path.to_path_buf())
|
||||
} else {
|
||||
std::env::current_dir()
|
||||
.map(|current| current.join(path))
|
||||
.map_err(|source| Error::io("query current directory", path, source))
|
||||
}
|
||||
common::absolute(path).map_err(|source| Error::io("query current directory", path, source))
|
||||
}
|
||||
|
||||
fn sha256(data: &[u8]) -> String {
|
||||
let mut digest = Sha256::new();
|
||||
digest.update(data);
|
||||
hex_digest(&digest.finalize())
|
||||
}
|
||||
/// Lowercase hex of a digest output (sha2 0.11's `Array` no longer formats as
|
||||
/// hex directly).
|
||||
fn hex_digest(data: &[u8]) -> String {
|
||||
let mut out = String::with_capacity(data.len() * 2);
|
||||
for byte in data {
|
||||
out.push_str(&format!("{byte:02x}"));
|
||||
}
|
||||
out
|
||||
common::sha256(data)
|
||||
}
|
||||
|
||||
@@ -7,9 +7,6 @@ use super::stage1::{self, DEFAULT_CIPHER_CONSTANT, DEFAULT_OUTER_SIZE};
|
||||
/// Return whether `data` has a supported protected AArch64 IL2CPP layout.
|
||||
#[must_use]
|
||||
pub fn is_protected_libil2cpp(data: &[u8]) -> bool {
|
||||
if !stage1::looks_protected(data) {
|
||||
return false;
|
||||
}
|
||||
let Ok(stage1) = stage1::inspect(
|
||||
data,
|
||||
Path::new("<probe>"),
|
||||
|
||||
@@ -1,44 +1,13 @@
|
||||
use std::path::Path;
|
||||
|
||||
use goblin::elf::{Elf, header::EM_AARCH64};
|
||||
use senbei_elf::{AARCH64_MACHINE, Error as ElfError, parse};
|
||||
|
||||
use super::error::{Error, Result, invalid};
|
||||
|
||||
pub(crate) const SHT_LOUSER: u32 = 0x8000_0000;
|
||||
pub(crate) use senbei_elf::SHT_LOUSER;
|
||||
pub const DEFAULT_CIPHER_CONSTANT: u32 = 0xbf20_165d;
|
||||
pub const DEFAULT_OUTER_SIZE: usize = 0x23c;
|
||||
|
||||
pub(crate) fn looks_protected(data: &[u8]) -> bool {
|
||||
let Ok(elf) = Elf::parse(data) else {
|
||||
return false;
|
||||
};
|
||||
if elf.header.e_machine != EM_AARCH64
|
||||
|| elf
|
||||
.section_headers
|
||||
.iter()
|
||||
.filter(|section| section.sh_type == SHT_LOUSER)
|
||||
.count()
|
||||
!= 1
|
||||
{
|
||||
return false;
|
||||
}
|
||||
[
|
||||
".dynsym",
|
||||
".dynstr",
|
||||
".gnu.hash",
|
||||
".gnu.version",
|
||||
".gnu.version_r",
|
||||
]
|
||||
.into_iter()
|
||||
.all(|wanted| {
|
||||
elf.section_headers.iter().any(|section| {
|
||||
elf.shdr_strtab
|
||||
.get_at(section.sh_name)
|
||||
.is_some_and(|name| name == wanted)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) struct Stage1Header {
|
||||
pub key: u32,
|
||||
@@ -70,13 +39,13 @@ pub(crate) fn inspect(
|
||||
outer_size: usize,
|
||||
cipher_constant: u32,
|
||||
) -> Result<Stage1Result> {
|
||||
let elf = Elf::parse(data).map_err(|source| Error::Elf {
|
||||
let elf = parse(data).map_err(|source: ElfError| Error::Elf {
|
||||
path: path.to_path_buf(),
|
||||
source,
|
||||
})?;
|
||||
if elf.header.e_machine != EM_AARCH64 {
|
||||
if elf.header.e_machine != AARCH64_MACHINE {
|
||||
return invalid(format!(
|
||||
"expected AArch64 ELF (machine 0x{EM_AARCH64:X}), got 0x{:X}",
|
||||
"expected AArch64 ELF (machine 0x{AARCH64_MACHINE:X}), got 0x{:X}",
|
||||
elf.header.e_machine
|
||||
));
|
||||
}
|
||||
@@ -92,6 +61,15 @@ pub(crate) fn inspect(
|
||||
matches.len()
|
||||
));
|
||||
}
|
||||
for wanted in senbei_elf::PROBE_SECTION_NAMES {
|
||||
if !elf.section_headers.iter().any(|section| {
|
||||
elf.shdr_strtab
|
||||
.get_at(section.sh_name)
|
||||
.is_some_and(|name| name == wanted)
|
||||
}) {
|
||||
return invalid(format!("protected ELF lacks required section {wanted}"));
|
||||
}
|
||||
}
|
||||
let (section_index, section) = matches[0];
|
||||
let section_offset = usize::try_from(section.sh_offset)
|
||||
.map_err(|_| Error::Invalid("SHT_LOUSER offset exceeds usize".to_owned()))?;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! Android AArch64 extraction and ELF restoration.
|
||||
|
||||
mod common;
|
||||
mod extract;
|
||||
mod restore;
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ pub enum Error {
|
||||
Json(#[from] serde_json::Error),
|
||||
#[error(transparent)]
|
||||
Crypto(#[from] senbei_crypto::android::Error),
|
||||
#[error(transparent)]
|
||||
Elf(#[from] senbei_elf::Error),
|
||||
#[error("{0}")]
|
||||
Invalid(String),
|
||||
}
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
use super::error::{Error, Result, invalid};
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn elf_hash(name: &[u8]) -> u32 {
|
||||
let mut value = 0_u32;
|
||||
for &byte in name {
|
||||
value = value.wrapping_shl(4).wrapping_add(u32::from(byte));
|
||||
let high = value & 0xf000_0000;
|
||||
if high != 0 {
|
||||
value ^= high >> 24;
|
||||
value &= !high;
|
||||
}
|
||||
}
|
||||
value
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn gnu_hash(name: &[u8]) -> u32 {
|
||||
name.iter().fold(5381_u32, |value, &byte| {
|
||||
value.wrapping_mul(33).wrapping_add(u32::from(byte))
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn build_sysv_hash(names: &[Vec<u8>]) -> Result<Vec<u8>> {
|
||||
if names.len() < 2 {
|
||||
return invalid("dynamic symbol table is unexpectedly empty");
|
||||
}
|
||||
let bucket_count = names.len();
|
||||
let symbol_count = names.len();
|
||||
let mut buckets = vec![0_u32; bucket_count];
|
||||
let mut chains = vec![0_u32; symbol_count];
|
||||
for (symbol_index, name) in names.iter().enumerate().skip(1) {
|
||||
let bucket_index = elf_hash(name) as usize % bucket_count;
|
||||
let symbol_index32 = u32::try_from(symbol_index)
|
||||
.map_err(|_| Error::Invalid("dynamic symbol index exceeds u32".to_owned()))?;
|
||||
if buckets[bucket_index] == 0 {
|
||||
buckets[bucket_index] = symbol_index32;
|
||||
continue;
|
||||
}
|
||||
let mut chain_index = buckets[bucket_index] as usize;
|
||||
while chains[chain_index] != 0 {
|
||||
chain_index = chains[chain_index] as usize;
|
||||
}
|
||||
chains[chain_index] = symbol_index32;
|
||||
}
|
||||
let mut output = Vec::with_capacity((2 + bucket_count + symbol_count) * 4);
|
||||
output.extend_from_slice(
|
||||
&u32::try_from(bucket_count)
|
||||
.map_err(|_| Error::Invalid("SysV bucket count exceeds u32".to_owned()))?
|
||||
.to_le_bytes(),
|
||||
);
|
||||
output.extend_from_slice(
|
||||
&u32::try_from(symbol_count)
|
||||
.map_err(|_| Error::Invalid("SysV symbol count exceeds u32".to_owned()))?
|
||||
.to_le_bytes(),
|
||||
);
|
||||
for value in buckets.into_iter().chain(chains) {
|
||||
output.extend_from_slice(&value.to_le_bytes());
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
pub(crate) fn build_gnu_hash(names: &[Vec<u8>]) -> Result<Vec<u8>> {
|
||||
let hashes = names
|
||||
.iter()
|
||||
.skip(1)
|
||||
.map(|name| gnu_hash(name))
|
||||
.collect::<Vec<_>>();
|
||||
if hashes.is_empty() {
|
||||
return invalid("GNU hash requires at least one dynamic symbol");
|
||||
}
|
||||
let bloom_shift = 5_u32;
|
||||
let mut bloom_word = 0_u64;
|
||||
for &value in &hashes {
|
||||
bloom_word |= 1_u64 << (value & 63);
|
||||
bloom_word |= 1_u64 << ((value >> bloom_shift) & 63);
|
||||
}
|
||||
let mut chains = hashes
|
||||
.into_iter()
|
||||
.map(|value| value & !1)
|
||||
.collect::<Vec<_>>();
|
||||
let last = chains
|
||||
.last_mut()
|
||||
.ok_or_else(|| Error::Invalid("GNU hash chain is empty".to_owned()))?;
|
||||
*last |= 1;
|
||||
let mut output = Vec::with_capacity(28 + chains.len() * 4);
|
||||
for value in [1_u32, 1, 1, bloom_shift] {
|
||||
output.extend_from_slice(&value.to_le_bytes());
|
||||
}
|
||||
output.extend_from_slice(&bloom_word.to_le_bytes());
|
||||
output.extend_from_slice(&1_u32.to_le_bytes());
|
||||
for value in chains {
|
||||
output.extend_from_slice(&value.to_le_bytes());
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn standard_elf_hash_is_stable() {
|
||||
assert_eq!(elf_hash(b"printf"), 0x0779_05a6);
|
||||
assert_eq!(gnu_hash(b"printf"), 0x156b_2bb8);
|
||||
}
|
||||
}
|
||||
@@ -1,636 +0,0 @@
|
||||
use super::error::{Error, Result, invalid};
|
||||
|
||||
pub(crate) const SHT_NOBITS: u32 = 8;
|
||||
pub(crate) const SHT_STRTAB: u32 = 3;
|
||||
pub(crate) const SHT_LOUSER: u32 = 0x8000_0000;
|
||||
pub(crate) const SHF_ALLOC: u64 = 2;
|
||||
const PT_LOAD: u32 = 1;
|
||||
pub(crate) const PF_R: u32 = 4;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) struct LoadSegment {
|
||||
pub offset: u64,
|
||||
pub virtual_address: u64,
|
||||
pub file_size: u64,
|
||||
pub memory_size: u64,
|
||||
pub flags: u32,
|
||||
pub alignment: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) struct SectionHeader {
|
||||
pub name: u32,
|
||||
pub section_type: u32,
|
||||
pub flags: u64,
|
||||
pub address: u64,
|
||||
pub offset: u64,
|
||||
pub size: u64,
|
||||
pub link: u32,
|
||||
pub info: u32,
|
||||
pub alignment: u64,
|
||||
pub entry_size: u64,
|
||||
}
|
||||
|
||||
impl SectionHeader {
|
||||
pub const SIZE: usize = 0x40;
|
||||
|
||||
fn parse(data: &[u8], offset: usize) -> Result<Self> {
|
||||
Ok(Self {
|
||||
name: read_u32(data, offset)?,
|
||||
section_type: read_u32(data, offset + 4)?,
|
||||
flags: read_u64(data, offset + 8)?,
|
||||
address: read_u64(data, offset + 0x10)?,
|
||||
offset: read_u64(data, offset + 0x18)?,
|
||||
size: read_u64(data, offset + 0x20)?,
|
||||
link: read_u32(data, offset + 0x28)?,
|
||||
info: read_u32(data, offset + 0x2c)?,
|
||||
alignment: read_u64(data, offset + 0x30)?,
|
||||
entry_size: read_u64(data, offset + 0x38)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn encode(self) -> [u8; Self::SIZE] {
|
||||
let mut output = [0_u8; Self::SIZE];
|
||||
output[0..4].copy_from_slice(&self.name.to_le_bytes());
|
||||
output[4..8].copy_from_slice(&self.section_type.to_le_bytes());
|
||||
output[8..0x10].copy_from_slice(&self.flags.to_le_bytes());
|
||||
output[0x10..0x18].copy_from_slice(&self.address.to_le_bytes());
|
||||
output[0x18..0x20].copy_from_slice(&self.offset.to_le_bytes());
|
||||
output[0x20..0x28].copy_from_slice(&self.size.to_le_bytes());
|
||||
output[0x28..0x2c].copy_from_slice(&self.link.to_le_bytes());
|
||||
output[0x2c..0x30].copy_from_slice(&self.info.to_le_bytes());
|
||||
output[0x30..0x38].copy_from_slice(&self.alignment.to_le_bytes());
|
||||
output[0x38..0x40].copy_from_slice(&self.entry_size.to_le_bytes());
|
||||
output
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct ElfLayout {
|
||||
pub entrypoint: u64,
|
||||
pub program_header_offset: usize,
|
||||
pub program_header_size: usize,
|
||||
pub program_header_count: usize,
|
||||
pub program_headers: Vec<LoadSegment>,
|
||||
pub section_headers: Vec<SectionHeader>,
|
||||
pub section_name_index: usize,
|
||||
pub private_section_index: usize,
|
||||
}
|
||||
|
||||
impl ElfLayout {
|
||||
pub fn parse(data: &[u8], require_private: bool) -> Result<Self> {
|
||||
let ident = slice(data, 0, 6)?;
|
||||
if ident[..4] != *b"\x7fELF" || ident[4] != 2 || ident[5] != 1 {
|
||||
return invalid("input is not a little-endian ELF64 file");
|
||||
}
|
||||
if read_u16(data, 0x12)? != 0xb7 {
|
||||
return invalid("input is not an AArch64 ELF");
|
||||
}
|
||||
let entrypoint = read_u64(data, 0x18)?;
|
||||
let program_header_offset = usize_from_u64(read_u64(data, 0x20)?, "program header offset")?;
|
||||
let section_header_offset = usize_from_u64(read_u64(data, 0x28)?, "section header offset")?;
|
||||
let program_header_size = usize::from(read_u16(data, 0x36)?);
|
||||
let program_header_count = usize::from(read_u16(data, 0x38)?);
|
||||
let section_header_size = usize::from(read_u16(data, 0x3a)?);
|
||||
let section_header_count = usize::from(read_u16(data, 0x3c)?);
|
||||
let section_name_index = usize::from(read_u16(data, 0x3e)?);
|
||||
if program_header_size != 0x38 || section_header_size != SectionHeader::SIZE {
|
||||
return invalid("unexpected ELF program/section header size");
|
||||
}
|
||||
|
||||
let mut program_headers = Vec::new();
|
||||
for index in 0..program_header_count {
|
||||
let offset = checked_index(program_header_offset, index, program_header_size)?;
|
||||
if read_u32(data, offset)? != PT_LOAD {
|
||||
continue;
|
||||
}
|
||||
let segment = LoadSegment {
|
||||
flags: read_u32(data, offset + 4)?,
|
||||
offset: read_u64(data, offset + 8)?,
|
||||
virtual_address: read_u64(data, offset + 0x10)?,
|
||||
file_size: read_u64(data, offset + 0x20)?,
|
||||
memory_size: read_u64(data, offset + 0x28)?,
|
||||
alignment: read_u64(data, offset + 0x30)?,
|
||||
};
|
||||
let file_end = segment
|
||||
.offset
|
||||
.checked_add(segment.file_size)
|
||||
.ok_or_else(|| Error::Invalid(format!("PT_LOAD {index} file range overflow")))?;
|
||||
if file_end > data.len() as u64 {
|
||||
return invalid(format!("PT_LOAD {index} exceeds input file"));
|
||||
}
|
||||
program_headers.push(segment);
|
||||
}
|
||||
if program_headers.is_empty() {
|
||||
return invalid("input ELF contains no PT_LOAD segments");
|
||||
}
|
||||
|
||||
let mut section_headers = Vec::with_capacity(section_header_count);
|
||||
for index in 0..section_header_count {
|
||||
let offset = checked_index(section_header_offset, index, section_header_size)?;
|
||||
section_headers.push(SectionHeader::parse(data, offset)?);
|
||||
}
|
||||
if section_name_index >= section_headers.len() {
|
||||
return invalid("ELF section-name index is out of range");
|
||||
}
|
||||
let private = section_headers
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, section)| (section.section_type == SHT_LOUSER).then_some(index))
|
||||
.collect::<Vec<_>>();
|
||||
let private_section_index = match private.as_slice() {
|
||||
[index] => *index,
|
||||
[] if !require_private => usize::MAX,
|
||||
_ => {
|
||||
return invalid(format!(
|
||||
"expected {} SHT_LOUSER section, found {}",
|
||||
if require_private {
|
||||
"one"
|
||||
} else {
|
||||
"at most one"
|
||||
},
|
||||
private.len()
|
||||
));
|
||||
}
|
||||
};
|
||||
let layout = Self {
|
||||
entrypoint,
|
||||
program_header_offset,
|
||||
program_header_size,
|
||||
program_header_count,
|
||||
program_headers,
|
||||
section_headers,
|
||||
section_name_index,
|
||||
private_section_index,
|
||||
};
|
||||
// Section roles are resolved from the ELF's own string table. Validate
|
||||
// it at the format boundary so callers cannot silently continue with
|
||||
// fabricated or lossy section names.
|
||||
layout.section_names(data)?;
|
||||
Ok(layout)
|
||||
}
|
||||
|
||||
pub fn private_section(&self) -> Result<SectionHeader> {
|
||||
self.section_headers
|
||||
.get(self.private_section_index)
|
||||
.copied()
|
||||
.ok_or_else(|| Error::Invalid("ELF has no private section".to_owned()))
|
||||
}
|
||||
|
||||
pub fn load_end(&self) -> Result<u64> {
|
||||
self.program_headers
|
||||
.iter()
|
||||
.map(|segment| {
|
||||
segment
|
||||
.virtual_address
|
||||
.checked_add(segment.memory_size)
|
||||
.ok_or_else(|| Error::Invalid("PT_LOAD memory end overflow".to_owned()))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?
|
||||
.into_iter()
|
||||
.max()
|
||||
.ok_or_else(|| Error::Invalid("ELF has no PT_LOAD memory range".to_owned()))
|
||||
}
|
||||
|
||||
pub fn file_load_end(&self) -> Result<u64> {
|
||||
self.program_headers
|
||||
.iter()
|
||||
.map(|segment| {
|
||||
segment
|
||||
.offset
|
||||
.checked_add(segment.file_size)
|
||||
.ok_or_else(|| Error::Invalid("PT_LOAD file end overflow".to_owned()))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?
|
||||
.into_iter()
|
||||
.max()
|
||||
.ok_or_else(|| Error::Invalid("ELF has no PT_LOAD file range".to_owned()))
|
||||
}
|
||||
|
||||
pub fn load_alignment(&self) -> Result<u64> {
|
||||
let alignment = self
|
||||
.program_headers
|
||||
.iter()
|
||||
.map(|segment| segment.alignment)
|
||||
.max()
|
||||
.ok_or_else(|| Error::Invalid("ELF has no PT_LOAD alignment".to_owned()))?;
|
||||
if alignment == 0 || !alignment.is_power_of_two() {
|
||||
return invalid(format!("invalid PT_LOAD alignment 0x{alignment:x}"));
|
||||
}
|
||||
Ok(alignment)
|
||||
}
|
||||
|
||||
pub fn append_load_segment(&self, output: &mut [u8], segment: LoadSegment) -> Result<Self> {
|
||||
if self.program_header_size != 0x38 {
|
||||
return invalid("unexpected ELF program header size");
|
||||
}
|
||||
if segment.file_size == 0 {
|
||||
return invalid("new PT_LOAD has no file contents");
|
||||
}
|
||||
if segment.memory_size < segment.file_size {
|
||||
return invalid("new PT_LOAD memory size is smaller than file size");
|
||||
}
|
||||
if segment.alignment == 0 || !segment.alignment.is_power_of_two() {
|
||||
return invalid(format!(
|
||||
"invalid new PT_LOAD alignment 0x{:x}",
|
||||
segment.alignment
|
||||
));
|
||||
}
|
||||
if segment.offset % segment.alignment != segment.virtual_address % segment.alignment {
|
||||
return invalid("new PT_LOAD offset and address are misaligned");
|
||||
}
|
||||
let segment_file_end = segment
|
||||
.offset
|
||||
.checked_add(segment.file_size)
|
||||
.ok_or_else(|| Error::Invalid("new PT_LOAD file range overflow".to_owned()))?;
|
||||
let segment_memory_end = segment
|
||||
.virtual_address
|
||||
.checked_add(segment.memory_size)
|
||||
.ok_or_else(|| Error::Invalid("new PT_LOAD memory range overflow".to_owned()))?;
|
||||
if segment_file_end > output.len() as u64 {
|
||||
return invalid("new PT_LOAD exceeds output mapping");
|
||||
}
|
||||
for existing in &self.program_headers {
|
||||
let existing_file_end = existing
|
||||
.offset
|
||||
.checked_add(existing.file_size)
|
||||
.ok_or_else(|| Error::Invalid("PT_LOAD file range overflow".to_owned()))?;
|
||||
if segment.offset < existing_file_end && existing.offset < segment_file_end {
|
||||
return invalid("new PT_LOAD overlaps an existing file range");
|
||||
}
|
||||
let existing_memory_end = existing
|
||||
.virtual_address
|
||||
.checked_add(existing.memory_size)
|
||||
.ok_or_else(|| Error::Invalid("PT_LOAD memory range overflow".to_owned()))?;
|
||||
if segment.virtual_address < existing_memory_end
|
||||
&& existing.virtual_address < segment_memory_end
|
||||
{
|
||||
return invalid("new PT_LOAD overlaps an existing memory range");
|
||||
}
|
||||
}
|
||||
let new_count = self
|
||||
.program_header_count
|
||||
.checked_add(1)
|
||||
.ok_or_else(|| Error::Invalid("program header count overflow".to_owned()))?;
|
||||
let new_count_u16 = u16::try_from(new_count)
|
||||
.map_err(|_| Error::Invalid("program header count exceeds u16".to_owned()))?;
|
||||
let header_offset = checked_index(
|
||||
self.program_header_offset,
|
||||
self.program_header_count,
|
||||
self.program_header_size,
|
||||
)?;
|
||||
let header_end = header_offset
|
||||
.checked_add(self.program_header_size)
|
||||
.ok_or_else(|| Error::Invalid("new program header range overflow".to_owned()))?;
|
||||
slice(output, header_offset, self.program_header_size)?;
|
||||
let first_file_section = self
|
||||
.section_headers
|
||||
.iter()
|
||||
.filter(|section| section.section_type != SHT_NOBITS && section.size != 0)
|
||||
.map(|section| section.offset)
|
||||
.min();
|
||||
if first_file_section.is_some_and(|offset| header_end as u64 > offset) {
|
||||
return invalid("no space for an additional program header");
|
||||
}
|
||||
|
||||
let mut header = [0_u8; 0x38];
|
||||
header[0..4].copy_from_slice(&PT_LOAD.to_le_bytes());
|
||||
header[4..8].copy_from_slice(&segment.flags.to_le_bytes());
|
||||
header[8..0x10].copy_from_slice(&segment.offset.to_le_bytes());
|
||||
header[0x10..0x18].copy_from_slice(&segment.virtual_address.to_le_bytes());
|
||||
header[0x18..0x20].copy_from_slice(&segment.virtual_address.to_le_bytes());
|
||||
header[0x20..0x28].copy_from_slice(&segment.file_size.to_le_bytes());
|
||||
header[0x28..0x30].copy_from_slice(&segment.memory_size.to_le_bytes());
|
||||
header[0x30..0x38].copy_from_slice(&segment.alignment.to_le_bytes());
|
||||
output
|
||||
.get_mut(header_offset..header_end)
|
||||
.ok_or_else(|| Error::Invalid("new program header exceeds output".to_owned()))?
|
||||
.copy_from_slice(&header);
|
||||
output
|
||||
.get_mut(0x38..0x3a)
|
||||
.ok_or_else(|| Error::Invalid("ELF header is truncated".to_owned()))?
|
||||
.copy_from_slice(&new_count_u16.to_le_bytes());
|
||||
|
||||
let mut updated = self.clone();
|
||||
updated.program_header_count = new_count;
|
||||
updated.program_headers.push(segment);
|
||||
Ok(updated)
|
||||
}
|
||||
|
||||
/// Resolve every section's name from the ELF `shstrtab` section.
|
||||
///
|
||||
/// The returned names are source data, not role labels supplied by the
|
||||
/// caller. Any malformed string-table reference is an input error.
|
||||
pub fn section_names(&self, data: &[u8]) -> Result<Vec<String>> {
|
||||
let table = self
|
||||
.section_headers
|
||||
.get(self.section_name_index)
|
||||
.copied()
|
||||
.ok_or_else(|| Error::Invalid("ELF section-name index is out of range".to_owned()))?;
|
||||
if table.section_type != SHT_STRTAB {
|
||||
return invalid(format!(
|
||||
"ELF section-name table has unexpected type 0x{:x}",
|
||||
table.section_type
|
||||
));
|
||||
}
|
||||
let strings = slice_u64(data, table.offset, table.size)?;
|
||||
if strings.is_empty() || strings[0] != 0 {
|
||||
return invalid("ELF section-name table does not start with NUL");
|
||||
}
|
||||
if strings.last().copied() != Some(0) {
|
||||
return invalid("ELF section-name table is not NUL terminated");
|
||||
}
|
||||
self.section_headers
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, section)| {
|
||||
let offset = section.name as usize;
|
||||
if offset >= strings.len() {
|
||||
return invalid(format!(
|
||||
"ELF section {index} name offset 0x{offset:x} exceeds section-name table"
|
||||
));
|
||||
}
|
||||
let end = strings[offset..]
|
||||
.iter()
|
||||
.position(|&byte| byte == 0)
|
||||
.map(|length| offset + length)
|
||||
.ok_or_else(|| {
|
||||
Error::Invalid(format!(
|
||||
"ELF section {index} name at 0x{offset:x} is unterminated"
|
||||
))
|
||||
})?;
|
||||
let name = std::str::from_utf8(&strings[offset..end]).map_err(|error| {
|
||||
Error::Invalid(format!(
|
||||
"ELF section {index} name at 0x{offset:x} is not UTF-8: {error}"
|
||||
))
|
||||
})?;
|
||||
if index == 0 && section.name != 0 {
|
||||
return invalid("ELF null section has a nonzero name offset");
|
||||
}
|
||||
Ok(name.to_owned())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn file_offset_to_virtual_address(&self, offset: u64, size: u64) -> Result<u64> {
|
||||
let end = offset
|
||||
.checked_add(size)
|
||||
.ok_or_else(|| Error::Invalid("file range overflow".to_owned()))?;
|
||||
for segment in &self.program_headers {
|
||||
let segment_end = segment
|
||||
.offset
|
||||
.checked_add(segment.file_size)
|
||||
.ok_or_else(|| Error::Invalid("PT_LOAD file range overflow".to_owned()))?;
|
||||
if segment.offset <= offset && end <= segment_end {
|
||||
return segment
|
||||
.virtual_address
|
||||
.checked_add(offset - segment.offset)
|
||||
.ok_or_else(|| Error::Invalid("virtual address overflow".to_owned()));
|
||||
}
|
||||
}
|
||||
invalid(format!(
|
||||
"file range 0x{offset:x}..0x{end:x} is not in PT_LOAD"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn slice(data: &[u8], offset: usize, size: usize) -> Result<&[u8]> {
|
||||
let end = offset
|
||||
.checked_add(size)
|
||||
.ok_or_else(|| Error::Invalid("byte range overflow".to_owned()))?;
|
||||
data.get(offset..end).ok_or_else(|| {
|
||||
Error::Invalid(format!(
|
||||
"byte range 0x{offset:x}..0x{end:x} is out of bounds"
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn slice_u64(data: &[u8], offset: u64, size: u64) -> Result<&[u8]> {
|
||||
slice(
|
||||
data,
|
||||
usize_from_u64(offset, "file offset")?,
|
||||
usize_from_u64(size, "file size")?,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn read_u16(data: &[u8], offset: usize) -> Result<u16> {
|
||||
let bytes: [u8; 2] = slice(data, offset, 2)?
|
||||
.try_into()
|
||||
.map_err(|_| Error::Invalid("invalid u16 range".to_owned()))?;
|
||||
Ok(u16::from_le_bytes(bytes))
|
||||
}
|
||||
|
||||
pub(crate) fn read_u32(data: &[u8], offset: usize) -> Result<u32> {
|
||||
let bytes: [u8; 4] = slice(data, offset, 4)?
|
||||
.try_into()
|
||||
.map_err(|_| Error::Invalid("invalid u32 range".to_owned()))?;
|
||||
Ok(u32::from_le_bytes(bytes))
|
||||
}
|
||||
|
||||
pub(crate) fn read_u64(data: &[u8], offset: usize) -> Result<u64> {
|
||||
let bytes: [u8; 8] = slice(data, offset, 8)?
|
||||
.try_into()
|
||||
.map_err(|_| Error::Invalid("invalid u64 range".to_owned()))?;
|
||||
Ok(u64::from_le_bytes(bytes))
|
||||
}
|
||||
|
||||
pub(crate) fn read_i64(data: &[u8], offset: usize) -> Result<i64> {
|
||||
let bytes: [u8; 8] = slice(data, offset, 8)?
|
||||
.try_into()
|
||||
.map_err(|_| Error::Invalid("invalid i64 range".to_owned()))?;
|
||||
Ok(i64::from_le_bytes(bytes))
|
||||
}
|
||||
|
||||
pub(crate) fn usize_from_u64(value: u64, field: &str) -> Result<usize> {
|
||||
usize::try_from(value).map_err(|_| Error::Invalid(format!("{field} 0x{value:x} exceeds usize")))
|
||||
}
|
||||
|
||||
pub(crate) fn checked_index(base: usize, index: usize, stride: usize) -> Result<usize> {
|
||||
index
|
||||
.checked_mul(stride)
|
||||
.and_then(|value| base.checked_add(value))
|
||||
.ok_or_else(|| Error::Invalid("table index overflow".to_owned()))
|
||||
}
|
||||
|
||||
pub(crate) fn align_up(value: u64, alignment: u64) -> Result<u64> {
|
||||
if alignment == 0 || !alignment.is_power_of_two() {
|
||||
return invalid(format!("invalid alignment {alignment}"));
|
||||
}
|
||||
value
|
||||
.checked_add(alignment - 1)
|
||||
.map(|aligned| aligned & !(alignment - 1))
|
||||
.ok_or_else(|| Error::Invalid("alignment overflow".to_owned()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn layout(name_index: u32) -> ElfLayout {
|
||||
ElfLayout {
|
||||
entrypoint: 0,
|
||||
program_header_offset: 0,
|
||||
program_header_size: 0x38,
|
||||
program_header_count: 0,
|
||||
program_headers: Vec::new(),
|
||||
section_headers: vec![
|
||||
SectionHeader {
|
||||
name: 0,
|
||||
section_type: 0,
|
||||
flags: 0,
|
||||
address: 0,
|
||||
offset: 0,
|
||||
size: 0,
|
||||
link: 0,
|
||||
info: 0,
|
||||
alignment: 0,
|
||||
entry_size: 0,
|
||||
},
|
||||
SectionHeader {
|
||||
name: name_index,
|
||||
section_type: 1,
|
||||
flags: 0,
|
||||
address: 0,
|
||||
offset: 0,
|
||||
size: 0,
|
||||
link: 0,
|
||||
info: 0,
|
||||
alignment: 0,
|
||||
entry_size: 0,
|
||||
},
|
||||
SectionHeader {
|
||||
name: 1,
|
||||
section_type: SHT_STRTAB,
|
||||
flags: 0,
|
||||
address: 0,
|
||||
offset: 0,
|
||||
size: 8,
|
||||
link: 0,
|
||||
info: 0,
|
||||
alignment: 1,
|
||||
entry_size: 0,
|
||||
},
|
||||
],
|
||||
section_name_index: 2,
|
||||
private_section_index: usize::MAX,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn section_names_resolve_from_elf_string_table() {
|
||||
let names = layout(1)
|
||||
.section_names(b"\0text\0\0\0")
|
||||
.expect("valid names");
|
||||
assert_eq!(names, ["", "text", "text"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn section_names_reject_out_of_range_name_offsets() {
|
||||
let error = layout(8)
|
||||
.section_names(b"\0text\0\0\0")
|
||||
.expect_err("invalid offset");
|
||||
assert!(error.to_string().contains("exceeds section-name table"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn section_names_reject_invalid_utf8() {
|
||||
let mut elf_layout = layout(1);
|
||||
elf_layout.section_headers[1].name = 1;
|
||||
let error = elf_layout
|
||||
.section_names(b"\0\xff\0\0\0\0\0\0")
|
||||
.expect_err("invalid UTF-8");
|
||||
assert!(error.to_string().contains("is not UTF-8"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn section_names_reject_non_string_table() {
|
||||
let mut elf_layout = layout(1);
|
||||
elf_layout.section_headers[2].section_type = 1;
|
||||
let error = elf_layout
|
||||
.section_names(b"\0text\0\0\0")
|
||||
.expect_err("wrong section type");
|
||||
assert!(error.to_string().contains("unexpected type"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn section_names_reject_unterminated_table() {
|
||||
let elf_layout = layout(1);
|
||||
let error = elf_layout
|
||||
.section_names(b"\0text\0\x01\x01")
|
||||
.expect_err("unterminated table");
|
||||
assert!(error.to_string().contains("not NUL terminated"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_load_segment_updates_program_headers() {
|
||||
let elf_layout = ElfLayout {
|
||||
entrypoint: 0,
|
||||
program_header_offset: 0,
|
||||
program_header_size: 0x38,
|
||||
program_header_count: 0,
|
||||
program_headers: Vec::new(),
|
||||
section_headers: Vec::new(),
|
||||
section_name_index: 0,
|
||||
private_section_index: usize::MAX,
|
||||
};
|
||||
let mut output = vec![0_u8; 0x2000];
|
||||
let updated = elf_layout
|
||||
.append_load_segment(
|
||||
&mut output,
|
||||
LoadSegment {
|
||||
offset: 0x1000,
|
||||
virtual_address: 0x2000,
|
||||
file_size: 0x20,
|
||||
memory_size: 0x20,
|
||||
flags: PF_R,
|
||||
alignment: 0x1000,
|
||||
},
|
||||
)
|
||||
.expect("append segment");
|
||||
assert_eq!(updated.program_header_count, 1);
|
||||
assert_eq!(updated.program_headers[0].virtual_address, 0x2000);
|
||||
assert_eq!(&output[0..4], &PT_LOAD.to_le_bytes());
|
||||
assert_eq!(&output[0x38..0x3a], &1_u16.to_le_bytes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_load_segment_rejects_program_header_overlap() {
|
||||
let mut elf_layout = ElfLayout {
|
||||
entrypoint: 0,
|
||||
program_header_offset: 0,
|
||||
program_header_size: 0x38,
|
||||
program_header_count: 0,
|
||||
program_headers: Vec::new(),
|
||||
section_headers: Vec::new(),
|
||||
section_name_index: 0,
|
||||
private_section_index: usize::MAX,
|
||||
};
|
||||
elf_layout.section_headers.push(SectionHeader {
|
||||
name: 0,
|
||||
section_type: 1,
|
||||
flags: 0,
|
||||
address: 0,
|
||||
offset: 0x20,
|
||||
size: 1,
|
||||
link: 0,
|
||||
info: 0,
|
||||
alignment: 1,
|
||||
entry_size: 0,
|
||||
});
|
||||
let mut output = vec![0_u8; 0x100];
|
||||
let error = elf_layout
|
||||
.append_load_segment(
|
||||
&mut output,
|
||||
LoadSegment {
|
||||
offset: 0x80,
|
||||
virtual_address: 0x1080,
|
||||
file_size: 0x20,
|
||||
memory_size: 0x20,
|
||||
flags: PF_R,
|
||||
alignment: 0x1000,
|
||||
},
|
||||
)
|
||||
.expect_err("overlapping program header");
|
||||
assert!(error.to_string().contains("additional program header"));
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
mod artifact;
|
||||
mod error;
|
||||
mod hash;
|
||||
mod layout;
|
||||
mod pipeline;
|
||||
|
||||
pub use error::Error;
|
||||
|
||||
@@ -12,35 +12,19 @@ use serde::Serialize;
|
||||
use sha2::{Digest, Sha256};
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
use super::super::common;
|
||||
use super::artifact::load_artifacts;
|
||||
use super::error::{Error, Result, invalid};
|
||||
use super::hash::{build_gnu_hash, build_sysv_hash};
|
||||
use super::layout::{
|
||||
ElfLayout, LoadSegment, PF_R, SHF_ALLOC, SHT_LOUSER, SHT_NOBITS, SectionHeader, align_up,
|
||||
read_i64, read_u32, read_u64, slice, slice_u64, usize_from_u64,
|
||||
use senbei_elf::{
|
||||
DT_GNU_HASH, DT_HASH, DT_JMPREL, DT_PLTRELSZ, DT_RELA, DT_RELACOUNT, DT_RELASZ, DT_STRSZ,
|
||||
DT_STRTAB, DT_SYMTAB, DT_VERNEED, DT_VERSYM, ELF64_RELA_SIZE, ELF64_SYMBOL_SIZE, ElfLayout,
|
||||
LoadSegment, PF_R, R_AARCH64_ABS64, R_AARCH64_GLOB_DAT, R_AARCH64_JUMP_SLOT,
|
||||
R_AARCH64_RELATIVE, SHF_ALLOC, SHT_LOUSER, SHT_NOBITS, SectionHeader, VER_NDX_GLOBAL, align_up,
|
||||
build_gnu_hash, build_sysv_hash, read_i64, read_u32, read_u64, slice, slice_u64,
|
||||
usize_from_u64,
|
||||
};
|
||||
|
||||
const CHUNK_SIZE: usize = 16 * 1024 * 1024;
|
||||
const ELF64_SYMBOL_SIZE: usize = 0x18;
|
||||
const ELF64_RELA_SIZE: usize = 0x18;
|
||||
const R_AARCH64_ABS64: u32 = 0x101;
|
||||
const R_AARCH64_GLOB_DAT: u32 = 0x401;
|
||||
const R_AARCH64_JUMP_SLOT: u32 = 0x402;
|
||||
const R_AARCH64_RELATIVE: u32 = 0x403;
|
||||
const VER_NDX_GLOBAL: u16 = 1;
|
||||
|
||||
const DT_PLTRELSZ: u64 = 2;
|
||||
const DT_HASH: u64 = 4;
|
||||
const DT_STRTAB: u64 = 5;
|
||||
const DT_SYMTAB: u64 = 6;
|
||||
const DT_RELA: u64 = 7;
|
||||
const DT_RELASZ: u64 = 8;
|
||||
const DT_STRSZ: u64 = 10;
|
||||
const DT_JMPREL: u64 = 23;
|
||||
const DT_GNU_HASH: u64 = 0x6fff_fef5;
|
||||
const DT_VERSYM: u64 = 0x6fff_fff0;
|
||||
const DT_RELACOUNT: u64 = 0x6fff_fff9;
|
||||
const DT_VERNEED: u64 = 0x6fff_fffe;
|
||||
|
||||
/// Inputs and optional diagnostics for one `libil2cpp.so` restoration.
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -187,9 +171,7 @@ fn read_file(path: &Path) -> Result<Vec<u8>> {
|
||||
}
|
||||
|
||||
fn sha256_bytes(data: &[u8]) -> String {
|
||||
let mut digest = Sha256::new();
|
||||
digest.update(data);
|
||||
hex_digest(&digest.finalize())
|
||||
common::sha256(data)
|
||||
}
|
||||
|
||||
fn sha256_file(path: &Path) -> Result<String> {
|
||||
@@ -205,7 +187,7 @@ fn sha256_file(path: &Path) -> Result<String> {
|
||||
}
|
||||
digest.update(&buffer[..read]);
|
||||
}
|
||||
Ok(hex_digest(&digest.finalize()))
|
||||
Ok(senbei_crypto::hex_digest(&digest.finalize()))
|
||||
}
|
||||
|
||||
fn copy_range(source: &[u8], output: &mut File, size: usize, path: &Path) -> Result<()> {
|
||||
@@ -286,7 +268,7 @@ impl FileLayoutWriter<'_> {
|
||||
"decoded write 0x{virtual_address:x}..0x{end:x} is not covered by PT_LOAD memory"
|
||||
));
|
||||
}
|
||||
usize_from_u64(written, "written byte count")
|
||||
Ok(usize_from_u64(written, "written byte count")?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -772,18 +754,8 @@ fn dynamic_contains_tag(output: &[u8], dynamic: SectionHeader, wanted: u64) -> R
|
||||
}
|
||||
|
||||
fn required_section_indices(names: &[String]) -> Result<HashMap<&'static str, usize>> {
|
||||
const REQUIRED: [&str; 8] = [
|
||||
".dynsym",
|
||||
".gnu.version",
|
||||
".gnu.version_r",
|
||||
".gnu.hash",
|
||||
".dynstr",
|
||||
".rela.dyn",
|
||||
".rela.plt",
|
||||
".dynamic",
|
||||
];
|
||||
let mut result = HashMap::with_capacity(REQUIRED.len());
|
||||
for required in REQUIRED {
|
||||
let mut result = HashMap::with_capacity(senbei_elf::DYNAMIC_SECTION_NAMES.len());
|
||||
for required in senbei_elf::DYNAMIC_SECTION_NAMES {
|
||||
let indices = names
|
||||
.iter()
|
||||
.enumerate()
|
||||
@@ -952,7 +924,7 @@ fn metadata_mapping_length(
|
||||
let end = extension_start
|
||||
.checked_add(cursor)
|
||||
.ok_or_else(|| Error::Invalid("dynamic-table mapping end overflow".to_owned()))?;
|
||||
usize_from_u64(end, "dynamic-table mapping length")
|
||||
Ok(usize_from_u64(end, "dynamic-table mapping length")?)
|
||||
}
|
||||
|
||||
fn table_placements(
|
||||
@@ -1467,29 +1439,11 @@ fn validate_restored_binary(
|
||||
}
|
||||
|
||||
fn absolute(path: &Path) -> Result<PathBuf> {
|
||||
if path.is_absolute() {
|
||||
Ok(path.to_path_buf())
|
||||
} else {
|
||||
std::env::current_dir()
|
||||
.map(|current| current.join(path))
|
||||
.map_err(|error| Error::io("query current directory", path, error))
|
||||
}
|
||||
common::absolute(path).map_err(|error| Error::io("query current directory", path, error))
|
||||
}
|
||||
|
||||
fn write_atomic(path: &Path, data: &[u8]) -> Result<()> {
|
||||
let parent = path.parent().unwrap_or_else(|| Path::new("."));
|
||||
std::fs::create_dir_all(parent)
|
||||
.map_err(|error| Error::io("create output directory", parent, error))?;
|
||||
let mut temporary = NamedTempFile::new_in(parent)
|
||||
.map_err(|error| Error::io("create temporary file", parent, error))?;
|
||||
temporary
|
||||
.write_all(data)
|
||||
.and_then(|_| temporary.as_file().sync_all())
|
||||
.map_err(|error| Error::io("write temporary file", temporary.path(), error))?;
|
||||
temporary
|
||||
.persist(path)
|
||||
.map_err(|error| Error::io("replace output", path, error.error))?;
|
||||
Ok(())
|
||||
common::write_atomic(path, data).map_err(|error| Error::io("write temporary file", path, error))
|
||||
}
|
||||
|
||||
/// Restore the current protected `libil2cpp.so` without executing protector code.
|
||||
@@ -1723,16 +1677,6 @@ pub fn restore_libil2cpp(options: &RestoreOptions) -> Result<RestoreReport> {
|
||||
elapsed_seconds: started.elapsed().as_secs_f64(),
|
||||
})
|
||||
}
|
||||
/// Lowercase hex of a digest output (sha2 0.11's `Array` no longer formats as
|
||||
/// hex directly).
|
||||
fn hex_digest(data: &[u8]) -> String {
|
||||
let mut out = String::with_capacity(data.len() * 2);
|
||||
for byte in data {
|
||||
out.push_str(&format!("{byte:02x}"));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -10,5 +10,13 @@ pub use windows::{
|
||||
|
||||
/// Deterministic worker-thread cap shared by filesystem scanning and engines.
|
||||
pub fn thread_cap() -> usize {
|
||||
windows::thread_cap()
|
||||
if let Ok(value) = std::env::var("SENBEI_THREADS")
|
||||
&& let Ok(count) = value.trim().parse::<usize>()
|
||||
&& count >= 1
|
||||
{
|
||||
return count;
|
||||
}
|
||||
std::thread::available_parallelism()
|
||||
.map(|count| count.get())
|
||||
.unwrap_or(1)
|
||||
}
|
||||
|
||||
@@ -15,23 +15,12 @@ pub fn unpack(input: &[u8]) -> Result<Vec<u8>, UnpackError> {
|
||||
/// Used by the new-layout managed (CLR) metadata restore to locate the COR20
|
||||
/// header and BSJB MetaData stream in the original protected file.
|
||||
fn prot_rva_to_off(file_data: &[u8], pe_header: u32, rva: u32) -> Option<u32> {
|
||||
let nsec = get_u16(file_data, pe_header + 6) as u32;
|
||||
let opt = get_u16(file_data, pe_header + 20) as u32;
|
||||
let tab = pe_header + 24 + opt;
|
||||
for i in 0..nsec {
|
||||
let s = tab + i * 40;
|
||||
if (s as usize + 24) > file_data.len() {
|
||||
return None;
|
||||
}
|
||||
let va = get_u32(file_data, s + 12);
|
||||
let vs = get_u32(file_data, s + 8);
|
||||
let rsz = get_u32(file_data, s + 16);
|
||||
let rp = get_u32(file_data, s + 20);
|
||||
if va <= rva && rva < va + vs.max(rsz) {
|
||||
return Some(rp + (rva - va));
|
||||
}
|
||||
let headers = senbei_pe::parse(file_data).ok()?;
|
||||
if headers.pe_offset != pe_header as usize {
|
||||
return None;
|
||||
}
|
||||
None
|
||||
let offset = senbei_pe::rva_to_offset(file_data, headers, rva).ok()?;
|
||||
u32::try_from(offset).ok()
|
||||
}
|
||||
|
||||
pub fn unpack_v(input: &[u8], verbose: bool) -> Result<Vec<u8>, UnpackError> {
|
||||
|
||||
@@ -42,47 +42,26 @@ fn rd_u32(d: &[u8], off: u32) -> Option<u32> {
|
||||
.map(|s| u32::from_le_bytes([s[0], s[1], s[2], s[3]]))
|
||||
}
|
||||
|
||||
/// A parsed section-table entry (only the fields we translate against).
|
||||
struct Section {
|
||||
va: u32,
|
||||
vsize: u32,
|
||||
raw_ptr: u32,
|
||||
raw_size: u32,
|
||||
chars: u32,
|
||||
}
|
||||
/// PE format section data used by the integrity policy.
|
||||
type Section = senbei_pe::Section;
|
||||
|
||||
/// Walk the output's own section table and translate an RVA to a file offset.
|
||||
/// Works for both memory-image output (raw_ptr == va) and compacted disk
|
||||
/// output (real raw pointers), because it consults whatever the output declares.
|
||||
/// Returns the offset only if the translated range `[off, off+need)` lies inside
|
||||
/// the file.
|
||||
fn rva_to_off(secs: &[Section], file_len: usize, rva: u32, need: u32) -> Option<u32> {
|
||||
for s in secs {
|
||||
// The mapped span is the larger of virtual and raw size, so an RVA that
|
||||
// falls in the virtual tail of a section still resolves.
|
||||
let span = s.vsize.max(s.raw_size);
|
||||
if span == 0 {
|
||||
continue;
|
||||
}
|
||||
if rva >= s.va && rva < s.va.wrapping_add(span) {
|
||||
let delta = rva - s.va;
|
||||
let off = s.raw_ptr.checked_add(delta)?;
|
||||
let end = off.checked_add(need)?;
|
||||
if (end as usize) <= file_len {
|
||||
return Some(off);
|
||||
}
|
||||
return None;
|
||||
}
|
||||
}
|
||||
None
|
||||
fn rva_to_off(data: &[u8], headers: senbei_pe::Headers, rva: u32, need: u32) -> Option<u32> {
|
||||
let offset = u32::try_from(senbei_pe::rva_to_offset(data, headers, rva).ok()?).ok()?;
|
||||
let end = offset.checked_add(need)?;
|
||||
(usize::try_from(end).ok()? <= data.len()).then_some(offset)
|
||||
}
|
||||
|
||||
fn is_executable_rva(secs: &[Section], rva: u32) -> bool {
|
||||
secs.iter().any(|section| {
|
||||
let span = section.vsize.max(section.raw_size);
|
||||
rva >= section.va
|
||||
&& rva < section.va.wrapping_add(span)
|
||||
&& (section.chars & 0x2000_0000) != 0
|
||||
let span = section.virtual_size.max(section.raw_size);
|
||||
rva >= section.virtual_address
|
||||
&& rva < section.virtual_address.wrapping_add(span)
|
||||
&& (section.characteristics & 0x2000_0000) != 0
|
||||
})
|
||||
}
|
||||
|
||||
@@ -151,7 +130,6 @@ pub fn check(out: &[u8]) -> IntegrityReport {
|
||||
return r;
|
||||
}
|
||||
};
|
||||
let opt_hdr_size = rd_u16(out, pe_off.wrapping_add(20)).unwrap_or(0) as u32;
|
||||
let opt = pe_off.wrapping_add(24);
|
||||
let magic = match rd_u16(out, opt) {
|
||||
Some(v) => v,
|
||||
@@ -181,42 +159,38 @@ pub fn check(out: &[u8]) -> IntegrityReport {
|
||||
}
|
||||
|
||||
// --- Section table ------------------------------------------------------
|
||||
let sec_table = opt.wrapping_add(opt_hdr_size);
|
||||
let mut secs: Vec<Section> = Vec::new();
|
||||
for i in 0..num_sections {
|
||||
let base = sec_table.wrapping_add(i * 40);
|
||||
// If the table runs past EOF the image is structurally broken.
|
||||
let (vsize, va, raw_size, raw_ptr, chars) = match (
|
||||
rd_u32(out, base.wrapping_add(8)),
|
||||
rd_u32(out, base.wrapping_add(12)),
|
||||
rd_u32(out, base.wrapping_add(16)),
|
||||
rd_u32(out, base.wrapping_add(20)),
|
||||
rd_u32(out, base.wrapping_add(36)),
|
||||
) {
|
||||
(Some(a), Some(b), Some(c), Some(d), Some(e)) => (a, b, c, d, e),
|
||||
_ => {
|
||||
r.issues
|
||||
.push("section table extends past end of file".into());
|
||||
return r;
|
||||
}
|
||||
};
|
||||
// Raw data must lie within the file for compacted (disk-layout) output.
|
||||
if raw_size != 0 {
|
||||
let end = raw_ptr.wrapping_add(raw_size) as usize;
|
||||
if end > file_len {
|
||||
r.issues.push(format!(
|
||||
"section #{i} raw data [0x{raw_ptr:X}..0x{end:X}] exceeds file size 0x{file_len:X}"
|
||||
));
|
||||
}
|
||||
let headers = match senbei_pe::parse(out) {
|
||||
Ok(headers) => headers,
|
||||
Err(_) => {
|
||||
r.issues
|
||||
.push("section table extends past end of file".into());
|
||||
return r;
|
||||
}
|
||||
secs.push(Section {
|
||||
va,
|
||||
vsize,
|
||||
raw_ptr,
|
||||
raw_size,
|
||||
chars,
|
||||
});
|
||||
}
|
||||
};
|
||||
let parsed_sections = match senbei_pe::sections(out, headers) {
|
||||
Ok(sections) => sections,
|
||||
Err(_) => {
|
||||
r.issues
|
||||
.push("section table extends past end of file".into());
|
||||
return r;
|
||||
}
|
||||
};
|
||||
let secs: Vec<Section> = parsed_sections
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, section)| {
|
||||
if section.raw_size != 0 {
|
||||
let end = section.raw_offset.wrapping_add(section.raw_size) as usize;
|
||||
if end > file_len {
|
||||
r.issues.push(format!(
|
||||
"section #{i} raw data [0x{:X}..0x{end:X}] exceeds file size 0x{file_len:X}",
|
||||
section.raw_offset
|
||||
));
|
||||
}
|
||||
}
|
||||
section
|
||||
})
|
||||
.collect();
|
||||
|
||||
// --- Managed (CLR) detection ------------------------------------------
|
||||
// The COR20 (CLR) data directory, when present and non-zero, marks a managed
|
||||
@@ -266,7 +240,7 @@ pub fn check(out: &[u8]) -> IntegrityReport {
|
||||
r.issues.push("entry point RVA is zero".into());
|
||||
}
|
||||
} else if !is_managed {
|
||||
match rva_to_off(&secs, file_len, ep, 16) {
|
||||
match rva_to_off(out, headers, ep, 16) {
|
||||
None => {
|
||||
r.issues.push(format!(
|
||||
"entry point RVA 0x{ep:X} does not map into any section"
|
||||
@@ -288,8 +262,10 @@ pub fn check(out: &[u8]) -> IntegrityReport {
|
||||
}
|
||||
// The entry must live in an executable section.
|
||||
let exec = secs.iter().any(|s| {
|
||||
let span = s.vsize.max(s.raw_size);
|
||||
ep >= s.va && ep < s.va.wrapping_add(span) && (s.chars & 0x2000_0000) != 0
|
||||
let span = s.virtual_size.max(s.raw_size);
|
||||
ep >= s.virtual_address
|
||||
&& ep < s.virtual_address.wrapping_add(span)
|
||||
&& (s.characteristics & 0x2000_0000) != 0
|
||||
});
|
||||
if !exec {
|
||||
r.issues.push(format!(
|
||||
@@ -316,7 +292,7 @@ pub fn check(out: &[u8]) -> IntegrityReport {
|
||||
if !is_managed {
|
||||
let imp_rva = rd_u32(out, dd_base.wrapping_add(8)).unwrap_or(0);
|
||||
if imp_rva != 0 {
|
||||
match rva_to_off(&secs, file_len, imp_rva, 20) {
|
||||
match rva_to_off(out, headers, imp_rva, 20) {
|
||||
None => r.issues.push(format!(
|
||||
"import directory RVA 0x{imp_rva:X} does not map into any section"
|
||||
)),
|
||||
@@ -331,7 +307,7 @@ pub fn check(out: &[u8]) -> IntegrityReport {
|
||||
if name_rva == 0 {
|
||||
break;
|
||||
}
|
||||
match rva_to_off(&secs, file_len, name_rva, 1) {
|
||||
match rva_to_off(out, headers, name_rva, 1) {
|
||||
None => r.issues.push(format!(
|
||||
"import descriptor {i} DLL name RVA 0x{name_rva:X} does not map into any section"
|
||||
)),
|
||||
@@ -359,7 +335,7 @@ pub fn check(out: &[u8]) -> IntegrityReport {
|
||||
// still refuses to load. Validate: COR20 cb == 0x48, and the MetaData stream
|
||||
// begins with the "BSJB" signature.
|
||||
if is_managed {
|
||||
match rva_to_off(&secs, file_len, clr_rva, 0x48) {
|
||||
match rva_to_off(out, headers, clr_rva, 0x48) {
|
||||
None => r.issues.push(format!(
|
||||
"CLR (COR20) directory RVA 0x{clr_rva:X} does not map into any section"
|
||||
)),
|
||||
@@ -373,7 +349,7 @@ pub fn check(out: &[u8]) -> IntegrityReport {
|
||||
// MetaData RVA/size live at COR20 + 0x08 / + 0x0C.
|
||||
let md_rva = rd_u32(out, coff.wrapping_add(8)).unwrap_or(0);
|
||||
if md_rva != 0 {
|
||||
match rva_to_off(&secs, file_len, md_rva, 4) {
|
||||
match rva_to_off(out, headers, md_rva, 4) {
|
||||
None => r.issues.push(format!(
|
||||
"CLR MetaData RVA 0x{md_rva:X} does not map into any section"
|
||||
)),
|
||||
@@ -416,11 +392,11 @@ mod tests {
|
||||
|
||||
fn executable_text() -> Vec<Section> {
|
||||
vec![Section {
|
||||
va: 0x1000,
|
||||
vsize: 0x4000,
|
||||
raw_ptr: 0x1000,
|
||||
virtual_address: 0x1000,
|
||||
virtual_size: 0x4000,
|
||||
raw_offset: 0x1000,
|
||||
raw_size: 0x4000,
|
||||
chars: 0x6000_0020,
|
||||
characteristics: 0x6000_0020,
|
||||
}]
|
||||
}
|
||||
|
||||
|
||||
@@ -11,11 +11,11 @@ use senbei_crypto::primitives;
|
||||
use std::cell::RefCell;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
pub use crate::thread_cap;
|
||||
pub use dll::{unpack_dll, unpack_dll_v};
|
||||
pub use error::*;
|
||||
pub use exe::{unpack as unpack_exe, unpack_v as unpack_exe_v};
|
||||
pub use integrity::{IntegrityReport, check as check_integrity};
|
||||
pub use parallel::thread_cap;
|
||||
|
||||
/// Maximum plausible PE `SizeOfImage` we are willing to allocate a zero buffer
|
||||
/// for. Guards against a corrupt/crafted header requesting a multi-gigabyte
|
||||
|
||||
@@ -19,20 +19,6 @@
|
||||
use std::sync::Mutex;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
/// Worker-thread cap. `SENBEI_THREADS` overrides it (`1` forces the sequential
|
||||
/// path); otherwise the host's available parallelism; otherwise 1.
|
||||
pub fn thread_cap() -> usize {
|
||||
if let Ok(v) = std::env::var("SENBEI_THREADS")
|
||||
&& let Ok(n) = v.trim().parse::<usize>()
|
||||
&& n >= 1
|
||||
{
|
||||
return n;
|
||||
}
|
||||
std::thread::available_parallelism()
|
||||
.map(|n| n.get())
|
||||
.unwrap_or(1)
|
||||
}
|
||||
|
||||
/// Run `f(i, span_base, span)` for every block `i`, fanning out across worker
|
||||
/// threads when the spans are disjoint and worthwhile, else sequentially.
|
||||
///
|
||||
@@ -102,7 +88,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
let cap = thread_cap();
|
||||
let cap = crate::thread_cap();
|
||||
let per = min_per_thread.max(1);
|
||||
let workers = if cap > 1 && n >= per.saturating_mul(2) {
|
||||
cap.min(n / per)
|
||||
|
||||
Reference in New Issue
Block a user