diff --git a/src/addr_info.rs b/src/addr_info.rs index 4cf8416..c2e7c02 100644 --- a/src/addr_info.rs +++ b/src/addr_info.rs @@ -1,5 +1,4 @@ use crate::bytes_info::BytesInfo; -use crate::id0::entry_iter::EntryTagContinuousSubkeys; use crate::id0::flag::nalt::x::NALT_DREF_FROM; use crate::id0::flag::nalt::{NALT_ENUM0, NALT_ENUM1, NALT_STRTYPE}; use crate::id0::flag::netnode::nn_res::{ARRAY_ALT_TAG, ARRAY_SUP_TAG}; @@ -242,37 +241,38 @@ impl<'a, K: IDAKind> AddressInfo<'a, K> { ByteType::Unknown => return Ok(None), } - // take the field names and the continuation (optional!) - let mut iter = EntryTagContinuousSubkeys::new( - self.id0, - self.netnode(), - ARRAY_SUP_TAG, - NSUP_TYPEINFO.into(), - ) - .take(0x1000); - let Some(first_entry) = iter.next() else { + // Type information and field names are stored as interleaved continuation streams: + // NSUP_TYPEINFO + 2*n contains type bytes, while NSUP_TYPEINFO + 2*n + 1 contains field + // names. Either stream may end before the other one. + let type_info_start = u64::from(NSUP_TYPEINFO); + let mut chunks = Vec::new(); + for entry in self.id0.sup_range(self.netnode(), ARRAY_SUP_TAG) { + let (index, value) = entry?; + let index = index.into_u64(); + if index < type_info_start { + continue; + } + let offset = index - type_info_start; + if offset >= 0x1000 { + break; + } + chunks.push((offset, value)); + } + let (til_raw, fields_raw) = collect_type_info_chunks(chunks); + if til_raw.is_empty() { return Ok(None); - }; - let mut til_raw: Vec = first_entry.value.to_vec(); + } - // convert the value into fields - // usually this string ends with \x00, but maybe there is no garanty for that. - // TODO what if there is more fields that can fit a id0 entry - let field_names = if let Some(fields_entry) = iter.next() { - let value = parse_maybe_cstr(&fields_entry.value); + // Convert the reassembled field-name stream into individual fields. It usually ends with + // \x00, but there is no guarantee that the terminator is present. + let field_names = if !fields_raw.is_empty() { + let value = parse_maybe_cstr(&fields_raw); crate::ida_reader::split_strings_from_array(value) .ok_or_else(|| anyhow!("Invalid Fields for TIL Type"))? } else { - // no fields - // TODO what if the type requires a continuation but it have no - // fields, does it just skip 0x3001? If so can't use - // EntryTagContinuousSubkeys above - vec![vec![]] + vec![] }; - // condensate the data continuation into a single buffer - til_raw.extend(iter.flat_map(|e| &e.value[..])); - // create the raw type let til = Type::new_from_id0(info, &til_raw, field_names)?; Ok(Some(til)) @@ -297,6 +297,66 @@ impl<'a, K: IDAKind> AddressInfo<'a, K> { } } +fn collect_type_info_chunks<'a>( + chunks: impl IntoIterator, +) -> (Vec, Vec) { + let mut result = [Vec::new(), Vec::new()]; + let mut expected_offset = [0, 1]; + let mut complete = [false, false]; + + for (offset, value) in chunks { + let stream = (offset & 1) as usize; + if complete[stream] { + continue; + } + if offset != expected_offset[stream] { + complete[stream] = true; + continue; + } + result[stream].extend_from_slice(value); + expected_offset[stream] += 2; + } + + let [type_info, fields] = result; + (type_info, fields) +} + +#[cfg(test)] +mod tests { + use super::collect_type_info_chunks; + + #[test] + fn type_info_and_field_continuations_are_reassembled_independently() { + let chunks: [(u64, &[u8]); 5] = [ + (0, b"type-0"), + (1, b"fields-0"), + (2, b"type-1"), + (3, b"fields-1"), + (4, b"type-2"), + ]; + + let (type_info, fields) = collect_type_info_chunks(chunks); + + assert_eq!(type_info, b"type-0type-1type-2"); + assert_eq!(fields, b"fields-0fields-1"); + } + + #[test] + fn a_gap_ends_only_its_own_continuation_stream() { + let chunks: [(u64, &[u8]); 4] = [ + (0, b"type-0"), + (1, b"fields-0"), + (3, b"fields-1"), + (4, b"not-type-1"), + ]; + + let (type_info, fields) = collect_type_info_chunks(chunks); + + assert_eq!(type_info, b"type-0"); + assert_eq!(fields, b"fields-0fields-1"); + } +} + pub fn all_address_info<'a, K: IDAKind>( id0: &'a ID0Section, id1: &ID1Section, diff --git a/src/id0/root_info.rs b/src/id0/root_info.rs index f709642..3386c4d 100644 --- a/src/id0/root_info.rs +++ b/src/id0/root_info.rs @@ -8,7 +8,7 @@ use num_enum::{IntoPrimitive, TryFromPrimitive}; use num_traits::{WrappingAdd, WrappingSub}; use serde::Serialize; -use crate::ida_reader::IdbReadKind; +use crate::ida_reader::{IdbRead, IdbReadKind}; use crate::til::function::CCModel; use crate::til::section::{ TILSectionExtendedSizeofInfo, TILSectionFlags, TILSectionHeader, @@ -589,6 +589,8 @@ impl RootInfo { let cc_size_l = input.read_u8()?; let cc_size_ll = input.read_u8()?; let cc_size_ldbl = input.read_u8()?; + let _extended_calling_convention = + read_extended_calling_convention(input, version)?; let abibits = AbiOptions::new(input.unpack_dd()?)?; let appcall_options = input.unpack_dd()?; @@ -744,6 +746,36 @@ impl RootInfo { } } +fn read_extended_calling_convention( + input: &mut impl IdbRead, + version: u16, +) -> Result> { + // IDA 9.2 added compiler_info_t::_new_callcnv after size_ldbl. + (version >= 920).then(|| input.unpack_dd()).transpose() +} + +#[cfg(test)] +mod tests { + use super::read_extended_calling_convention; + + #[test] + fn extended_calling_convention_was_added_in_ida_9_2() { + let mut old_input = &[0][..]; + assert_eq!( + read_extended_calling_convention(&mut old_input, 919).unwrap(), + None + ); + assert_eq!(old_input, &[0]); + + let mut new_input = &[0][..]; + assert_eq!( + read_extended_calling_convention(&mut new_input, 920).unwrap(), + Some(0) + ); + assert!(new_input.is_empty()); + } +} + /// General idainfo flags #[derive(Debug, Clone, Copy, Serialize)] pub struct Inffl(u8); diff --git a/src/lib.rs b/src/lib.rs index e24e597..009e694 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -182,9 +182,25 @@ fn read_500_600_header( version: IDBSeparatedVersion, input: &mut I, ) -> Result { - let id2_offset = input.read_u32()?; - ensure!(id2_offset == 0); - let checksums: [u32; 5] = bincode::deserialize_from(input)?; + // This field is not a section offset. IDA writes non-zero values here + // (for example, 2 in some version 3 databases), so keep it opaque until + // its meaning is known. + let _unknown = input.read_u32()?; + let checksums: [u32; 5] = bincode::deserialize_from(&mut *input)?; + let id2 = match version { + IDBSeparatedVersion::V1 => None, + IDBSeparatedVersion::V3 | IDBSeparatedVersion::V4 => { + let id2_offset = input.read_u32()?; + let id2_checksum = input.read_u32()?; + SeparatedSection::new_inner::( + id2_offset, + Some(id2_checksum), + )? + } + IDBSeparatedVersion::V5 | IDBSeparatedVersion::V6 => { + unreachable!("versions 5 and 6 use the extended header") + } + }; let id0 = SeparatedSection::new::(&offsets[0..4], Some(checksums[0]))?; @@ -196,7 +212,6 @@ fn read_500_600_header( SeparatedSection::new::(&offsets[12..16], Some(checksums[3]))?; let til = SeparatedSection::new::(&offsets[16..20], Some(checksums[4]))?; - #[cfg(feature = "restrictive")] { // TODO ensure the rest of the header is just zeros @@ -218,7 +233,7 @@ fn read_500_600_header( nam, seg, til, - id2: None, + id2, }, ))) } else { @@ -232,7 +247,7 @@ fn read_500_600_header( nam, seg, til, - id2: None, + id2, }, ))) } diff --git a/src/test.rs b/src/test.rs index 35196a4..8dc3743 100644 --- a/src/test.rs +++ b/src/test.rs @@ -189,6 +189,58 @@ fn parse_idb_inner(file: PathBuf) { } } +#[test] +fn identify_v3_header_with_nonzero_unknown_field() { + let mut header = Vec::new(); + header.extend_from_slice(b"IDA1"); + header.extend_from_slice(&0u16.to_le_bytes()); + for offset in [0x40u32, 0x80, 0xc0, 0, 0x100] { + header.extend_from_slice(&offset.to_le_bytes()); + } + header.extend_from_slice(&0xAABB_CCDDu32.to_le_bytes()); + header.extend_from_slice(&3u16.to_le_bytes()); + header.extend_from_slice(&2u32.to_le_bytes()); + for checksum in [1u32, 2, 3, 0, 4] { + header.extend_from_slice(&checksum.to_le_bytes()); + } + header.extend_from_slice(&0x140u32.to_le_bytes()); + header.extend_from_slice(&5u32.to_le_bytes()); + + let IDBFormats::Separated(IDAVariants::IDA32(sections)) = + identify_idb_file(&mut Cursor::new(header)).unwrap() + else { + panic!("expected a 32-bit separated IDB"); + }; + + assert_eq!(sections.id0_location().unwrap().idb_offset(), 0x40); + assert_eq!(sections.id2_location().unwrap().idb_offset(), 0x140); +} + +#[test] +fn identify_v1_header_without_id2_fields() { + let mut header = Vec::new(); + header.extend_from_slice(b"IDA1"); + header.extend_from_slice(&0u16.to_le_bytes()); + for offset in [0x3eu32, 0x80, 0xc0, 0, 0x100] { + header.extend_from_slice(&offset.to_le_bytes()); + } + header.extend_from_slice(&0xAABB_CCDDu32.to_le_bytes()); + header.extend_from_slice(&1u16.to_le_bytes()); + header.extend_from_slice(&0u32.to_le_bytes()); + for checksum in [1u32, 2, 3, 0, 4] { + header.extend_from_slice(&checksum.to_le_bytes()); + } + + let IDBFormats::Separated(IDAVariants::IDA32(sections)) = + identify_idb_file(&mut Cursor::new(header)).unwrap() + else { + panic!("expected a 32-bit separated IDB"); + }; + + assert_eq!(sections.id0_location().unwrap().idb_offset(), 0x3e); + assert!(sections.id2_location().is_none()); +} + fn parse_idb_format, I: BufRead + Seek>( filename: &str, input: &mut I,