Published on 02/08/2024 00:01 by Jacob Latonis
100 Days of Yara in 2024: Day 39
Following the theme of the last few days of my #100DaysofYARA posts, I am once again refactoring a portion of a PR to follow the new parsing format and methodology for the Mach-O module and YARA-X. If you remember way back in Day 04, I parsed out the LC_VERSION_MIN_*
load commands. Unfortunately, it now needs to be refactored as the PR (#56) wasn’t merged in before the refactor. As such, we have some work to do!
Original Way
The old way involves the endianness swapping, a handler function, and a parsing function for the minimum version load command. There’s also a bit of logic to populate the right protobuf structure.
const LC_VERSION_MIN_MACOSX: u32 = 0x00000024;
const LC_VERSION_MIN_IPHONEOS: u32 = 0x00000025;
const LC_VERSION_MIN_TVOS: u32 = 0x0000002f;
const LC_VERSION_MIN_WATCHOS: u32 = 0x00000030;
/// `MinVersionCommand`: Represents a minimum version command in the Mach-O file.
/// Fields: cmd, cmdsize, version, sdk
#[repr(C)]
#[derive(Debug, Default, Clone, Copy)]
struct MinVersionCommand {
cmd: u32,
cmdsize: u32,
version: u32,
sdk: u32,
}
/// Swaps the endianness of fields within a Mach-O minimum version
/// command from BigEndian to LittleEndian in-place.
///
/// # Arguments
///
/// * `command`: A mutable reference to the Mach-O minimum version command.
fn swap_min_version_command(command: &mut MinVersionCommand) {
command.cmd = BigEndian::read_u32(&command.cmd.to_le_bytes());
command.cmdsize = BigEndian::read_u32(&command.cmdsize.to_le_bytes());
command.version = BigEndian::read_u32(&command.version.to_le_bytes());
command.sdk = BigEndian::read_u32(&command.sdk.to_le_bytes());
}
/// Parse a Mach-O MinVersionCommand, transforming raw bytes into a structured
/// format.
///
/// # Arguments
///
/// * `input`: A slice of bytes containing the raw MinVersionCommand data.
///
/// # Returns
///
/// A `nom` IResult containing the remaining unparsed input and the parsed
/// MinVersionCommand structure, or a `nom` error if the parsing fails.
///
/// # Errors
///
/// Returns a `nom` error if the input data is insufficient or malformed.
fn parse_min_version_command(
input: &[u8],
) -> IResult<&[u8], MinVersionCommand> {
let (input, cmd) = le_u32(input)?;
let (input, cmdsize) = le_u32(input)?;
let (input, version) = le_u32(input)?;
let (input, sdk) = le_u32(input)?;
Ok((input, MinVersionCommand { cmd, cmdsize, version, sdk }))
}
/// Handles the LC_VERSION_MIN_MACOSX, LC_VERSION_MIN_IPHONEOS, LC_VERSION_MIN_TVOS, and LC_VERSION_MIN_WATCHOS
/// commands for Mach-O files, parsing the data and populating a protobuf representation of the minimum version
/// load command.
///
/// # Arguments
///
/// * `command_data`: The raw byte data of the minimum version command.
/// * `size`: The size of the minimum version command data.
/// * `macho_file`: Mutable reference to the protobuf representation of the
/// Mach-O file.
///
/// # Returns
///
/// Returns a `Result<(), MachoError>` indicating the success or failure of the
/// operation.
///
/// # Errors
///
/// * `MachoError::FileSectionTooSmall`: Returned when the segment size is
/// smaller than the expected MinVersionCommand struct size.
/// * `MachoError::ParsingError`: Returned when there is an error parsing the
/// minumum version command data.
/// * `MachoError::MissingHeaderValue`: Returned when the "magic" header value
/// is missing, needed for determining if bytes should be swapped.
fn handle_min_version_command(
command_data: &[u8],
size: usize,
macho_file: &mut File,
) -> Result<(), MachoError> {
if size < std::mem::size_of::<MinVersionCommand>() {
return Err(MachoError::FileSectionTooSmall(
"MinVersionCommand".to_string(),
));
}
let (_, mut mvc) = parse_min_version_command(command_data)
.map_err(|e| MachoError::ParsingError(format!("{:?}", e)))?;
if should_swap_bytes(
macho_file
.magic
.ok_or(MachoError::MissingHeaderValue("magic".to_string()))?,
) {
swap_min_version_command(&mut mvc);
}
// X.Y.Z is encoded in nibbles xxxx.yy.zz
let ver_string: String = convert_to_version_string(mvc.version);
// X.Y.Z is encoded in nibbles xxxx.yy.zz
let sdk_string: String = convert_to_version_string(mvc.sdk);
match mvc.cmd {
LC_VERSION_MIN_MACOSX => {
let min_version_command = MinVersionMacOS {
cmd: Some(mvc.cmd),
cmdsize: Some(mvc.cmdsize),
version: Some(ver_string),
sdk: Some(sdk_string),
..Default::default()
};
macho_file.min_version_mac_os =
MessageField::some(min_version_command);
}
LC_VERSION_MIN_IPHONEOS => {
let min_version_command = MinVersionIphoneOS {
cmd: Some(mvc.cmd),
cmdsize: Some(mvc.cmdsize),
version: Some(ver_string),
sdk: Some(sdk_string),
..Default::default()
};
macho_file.min_version_iphone_os =
MessageField::some(min_version_command);
}
LC_VERSION_MIN_TVOS => {
let min_version_command = MinVersionTvOS {
cmd: Some(mvc.cmd),
cmdsize: Some(mvc.cmdsize),
version: Some(ver_string),
sdk: Some(sdk_string),
..Default::default()
};
macho_file.min_version_tv_os =
MessageField::some(min_version_command);
}
LC_VERSION_MIN_WATCHOS => {
let min_version_command = MinVersionWatchOS {
cmd: Some(mvc.cmd),
cmdsize: Some(mvc.cmdsize),
version: Some(ver_string),
sdk: Some(sdk_string),
..Default::default()
};
macho_file.min_version_watch_os =
MessageField::some(min_version_command);
}
_ => {}
}
Ok(())
}
Originally, I defined four protobuf structures, but I think this can be done with one structure and an enum now.
message MinVersionMacOS {
optional uint32 cmd = 1;
optional uint32 cmdsize = 2;
optional string version = 3;
optional string sdk = 4;
}
message MinVersionIphoneOS {
optional uint32 cmd = 1;
optional uint32 cmdsize = 2;
optional string version = 3;
optional string sdk = 4;
}
message MinVersionTvOS {
optional uint32 cmd = 1;
optional uint32 cmdsize = 2;
optional string version = 3;
optional string sdk = 4;
}
message MinVersionWatchOS {
optional uint32 cmd = 1;
optional uint32 cmdsize = 2;
optional string version = 3;
optional string sdk = 4;
}
New Way
Coming at you live with a slightly changed and refactored parser for the LC_VERSION_MIN*
load commands. I decided to do an enum in the protobuf for the device type, instead of having four different proto representations.
message MinVersion {
optional uint32 device = 1;
optional string version = 2;
optional string sdk = 3;
}
enum DEVICE_TYPE {
option (yara.enum_options).inline = true;
MACOSX = 0x00000024;
IPHONEOS = 0x00000025;
TVOS = 0x0000002f;
WATCHOS = 0x00000030;
}
[...]
LC_VERSION_MIN_MACOSX
| LC_VERSION_MIN_IPHONEOS
| LC_VERSION_MIN_TVOS
| LC_VERSION_MIN_WATCHOS => {
let (_, mut mv) =
self.min_version_command()(command_data)?;
mv.device = command;
self.min_version = Some(mv);
}
[...]
fn min_version_command(
&self,
) -> impl FnMut(&'a [u8]) -> IResult<&'a [u8], MinVersion> + '_ {
move |input: &'a [u8]| {
let (input, (version, sdk)) = tuple((
u32(self.endianness), // version
u32(self.endianness), // sdk,
))(input)?;
Ok((input, MinVersion { device: 0, version, sdk }))
}
}
[...]
struct MinVersion {
device: u32,
version: u32,
sdk: u32,
}
[...]
if let Some(bv) = &macho.build_version {
result.build_version = MessageField::some(bv.into());
}
[...]
impl From<&MinVersion> for protos::macho::MinVersion {
fn from(mv: &MinVersion) -> Self {
let mut result = protos::macho::MinVersion::new();
result.set_device(mv.device);
result.set_version(convert_to_version_string(mv.version));
result.set_sdk(convert_to_version_string(mv.sdk));
result
}
}
Finished Work
This is just part of the work from #56 that I am cleaning up after the refactor. There will be more posts like this :’).
I closed #56 as I folded this work into #78.
This specific work implemented today can be seen in #78 on YARA-X.
Written by Jacob Latonis
← Back to blog