Published on 02/07/2024 00:01 by Jacob Latonis
100 Days of Yara in 2024: Day 38
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 08, I parsed out the LC_BUILD_VERSION
load command. Unfortunately, it now needs to be refactored as the PR (#68) 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 both the build version data and the build too objects.
const LC_BUILD_VERSION: u32 = 0x00000032;
/// `BuildVersionCommand`: Represents a build version command in the Mach-O file.
/// Fields: cmd, cmdsize, platform, minos, sdk, ntools
#[repr(C)]
#[derive(Debug, Default, Clone, Copy)]
struct BuildVersionCommand {
cmd: u32,
cmdsize: u32,
platform: u32,
minos: u32,
sdk: u32,
ntools: u32,
}
/// `BuildToolObject`: Represents a build Tool struct in the Mach-O file following the
/// BuildVersionCommand
/// Fields: tool, version
#[repr(C)]
#[derive(Debug, Default, Clone, Copy)]
struct BuildToolObject {
tool: u32,
version: u32,
}
/// Swaps the endianness of fields within a Mach-O BuildVersionCommand command from
/// BigEndian to LittleEndian in-place.
///
/// # Arguments
///
/// * `command`: A mutable reference to the Mach-O build version command.
fn swap_build_version_command(command: &mut BuildVersionCommand) {
command.cmd = BigEndian::read_u32(&command.cmd.to_le_bytes());
command.cmdsize = BigEndian::read_u32(&command.cmdsize.to_le_bytes());
command.platform = BigEndian::read_u32(&command.platform.to_le_bytes());
command.minos = BigEndian::read_u32(&command.minos.to_le_bytes());
command.sdk = BigEndian::read_u32(&command.sdk.to_le_bytes());
command.ntools = BigEndian::read_u32(&command.ntools.to_le_bytes());
}
/// Swaps the endianness of fields within a Mach-O BuildTool struct from
/// BigEndian to LittleEndian in-place.
///
/// # Arguments
///
/// * `command`: A mutable reference to the Mach-O build tool struct.
fn swap_build_tool(command: &mut BuildToolObject) {
command.tool = BigEndian::read_u32(&command.tool.to_le_bytes());
command.version = BigEndian::read_u32(&command.version.to_le_bytes());
}
/// Parse a Mach-O BuildVersionCommand, transforming raw bytes into a structured
/// format.
///
/// # Arguments
///
/// * `input`: A slice of bytes containing the raw BuildVersionCommand data.
///
/// # Returns
///
/// A `nom` IResult containing the remaining unparsed input and the parsed
/// BuildVersionCommand structure, or a `nom` error if the parsing fails.
///
/// # Errors
///
/// Returns a `nom` error if the input data is insufficient or malformed.
fn parse_build_version_command(
input: &[u8],
) -> IResult<&[u8], BuildVersionCommand> {
let (input, cmd) = le_u32(input)?;
let (input, cmdsize) = le_u32(input)?;
let (input, platform) = le_u32(input)?;
let (input, minos) = le_u32(input)?;
let (input, sdk) = le_u32(input)?;
let (input, ntools) = le_u32(input)?;
Ok((
input,
BuildVersionCommand { cmd, cmdsize, platform, minos, sdk, ntools },
))
}
/// Parse a Mach-O BuiltTool struct, transforming raw bytes into a structured
/// format.
///
/// # Arguments
///
/// * `input`: A slice of bytes containing the raw BuildToolObject data.
///
/// # Returns
///
/// A `nom` IResult containing the remaining unparsed input and the parsed
/// BuildToolObject structure, or a `nom` error if the parsing fails.
///
/// # Errors
///
/// Returns a `nom` error if the input data is insufficient or malformed.
fn parse_build_tool(input: &[u8]) -> IResult<&[u8], BuildToolObject> {
let (input, tool) = le_u32(input)?;
let (input, version) = le_u32(input)?;
Ok((input, BuildToolObject { tool, version }))
}
fn handle_build_version_command(
command_data: &[u8],
size: usize,
macho_file: &mut File,
) -> Result<(), MachoError> {
if size < std::mem::size_of::<BuildVersionCommand>() {
return Err(MachoError::FileSectionTooSmall(
"BuildVersionCommand".to_string(),
));
}
let swap = should_swap_bytes(
macho_file
.magic
.ok_or(MachoError::MissingHeaderValue("magic".to_string()))?,
);
let (_, mut bc) = parse_build_version_command(command_data)
.map_err(|e| MachoError::ParsingError(format!("{:?}", e)))?;
if swap {
swap_build_version_command(&mut bc);
}
macho_file.build_version = MessageField::some(BuildVersion {
platform: Some(bc.platform),
minos: Some(convert_to_version_string(bc.minos)),
sdk: Some(convert_to_version_string(bc.sdk)),
ntools: Some(bc.ntools),
..Default::default()
});
for _n in 0..bc.ntools {
let (_, mut bt) = parse_build_tool(command_data)
.map_err(|e| MachoError::ParsingError(format!("{:?}", e)))?;
if swap {
swap_build_tool(&mut bt)
}
macho_file.build_tools.push(BuildTool {
tool: Some(bt.tool),
version: Some(bt.version),
..Default::default()
});
}
Ok(())
}
New Way
Coming at you live with a slightly changed and refactored parser for the LC_BUILD_VERSION
load command and the associated build tools structs!
/// Parser that parses a LC_BUILD_VERSION command.
fn build_version_command(
&self,
) -> impl FnMut(&'a [u8]) -> IResult<&'a [u8], BuildVersionCommand> + '_
{
move |input: &'a [u8]| {
let (mut remainder, (platform, minos, sdk, ntools)) =
tuple((
u32(self.endianness), // platform,
u32(self.endianness), // minos,
u32(self.endianness), // sdk,
u32(self.endianness), // ntools,
))(input)?;
let mut tools = Vec::<BuildToolObject>::new();
for _ in 0..ntools {
let (data, (tool, version)) = tuple((
u32(self.endianness), // tool,
u32(self.endianness), // version,
))(remainder)?;
remainder = data;
tools.push(BuildToolObject { tool, version })
}
Ok((
&[],
BuildVersionCommand { platform, minos, sdk, ntools, tools },
))
}
}
[...]
if let Some(bv) = &macho.build_version {
result.build_version = MessageField::some(bv.into());
}
[...]
impl From<&BuildVersionCommand> for protos::macho::BuildVersion {
fn from(bv: &BuildVersionCommand) -> Self {
let mut result = protos::macho::BuildVersion::new();
result.set_platform(bv.platform);
result.set_ntools(bv.ntools);
result.set_minos(convert_to_version_string(bv.minos));
result.set_sdk(convert_to_version_string(bv.sdk));
result.tools.extend(bv.tools.iter().map(|tool| tool.into()));
result
}
}
impl From<&BuildToolObject> for protos::macho::BuildTool {
fn from(bt: &BuildToolObject) -> Self {
let mut result = protos::macho::BuildTool::new();
result.set_tool(bt.tool);
result.set_version(convert_to_build_tool_version(bt.version));
result
}
}
Finished Work
This is just part of the work from #68 that I am cleaning up after the refactor. There will be more posts like this :’).
I closed #68 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