#!/usr/bin/perl # Create a UEFI "Firmware Volume" (FV) # typedef struct { # UINT8 ZeroVector[16]; // 0x00 # EFI_GUID FileSystemGuid; // 0x10 # UINT64 FvLength; // 0x20 # UINT32 Signature; // 0x28 # EFI_FVB_ATTRIBUTES Attributes; // 0x2C (uint32) # UINT16 HeaderLength; // 0x30 # UINT16 Checksum; // 0x32 # UINT8 Reserved[3]; // 0x34 # UINT8 Revision; // 0x37 # EFI_FV_BLOCK_MAP_ENTRY FvBlockMap[1]; // 0x38 (unused) #} EFI_FIRMWARE_VOLUME_HEADER; use warnings; use strict; use FindBin; use lib "$FindBin::Bin/../lib"; use Getopt::Long; use File::Temp 'tempfile'; use Digest::SHA 'sha1'; use GUID; my $usage = <<""; Usage: $0 -o output.fv [options] file.ffs [...] Options: -o | --output output.ffs Output file (default is stdout) -s | --size BYTES Size in bytes (default is 4 MB) -g | --guid GUID This file GUID (default is hash of Name) -f | --force Force FV creation even if FFS are corrupted -v | --verbose Print each file #-z | --compress Enable LZMA compression my $output = '-'; my $guid_str = "8C8CE578-8A3D-4F1C-9935-896185C32DD3"; my $size = 4 * 1024 * 1024; my $force; my $verbose; my $hdr_len = 0x48; my $block_size = 0x2000; # force alignment of files to this spacing GetOptions( "o|output=s" => \$output, "g|guid=s" => \$guid_str, "s|size=o" => \$size, #"z|compress+" => \$compress, "f|force+" => \$force, "v|verboes+" => \$verbose, ) or die $usage; my $guid = guid($guid_str) or die "$guid_str: Unable to parse GUID\n"; my $fv_hdr = '' . (chr(0x00) x 0x10) # 0x00 Zero vector . $guid # 0x10 . pack("Q", $size) # 0x20 . '_FVH' # 0x28 . pack("V", 0x000CFEFF) # 0x2C attributes . pack("v", $hdr_len) # 0x30 header length . pack("v", 0x0000) # 0x32 checksum . chr(0x00) x 3 # 0x34 reserved . chr(0x02) # 0x37 version . pack("V", $size / $block_size) # 0x38 number blocks . pack("V", $block_size) # 0x3C block size . (chr(0x00) x 0x08) # 0x40 map (unused) ; die "FV Header length ", length $fv_hdr, " != $hdr_len\n" unless $hdr_len == length $fv_hdr; # update the header checksum my $sum = 0; for(my $i = 0 ; $i < $hdr_len ; $i += 2) { $sum -= unpack("v", substr($fv_hdr, $i, 2)); } substr($fv_hdr, 0x32, 2) = pack("v", $sum & 0xFFFF); my $data = $fv_hdr; # Read entire files at a time and append a new file # for each input read. local $/ = undef; while(<>) { # quick sanity check on the file my $length = length $_; my $ffs_length = unpack("V", substr($_, 0x14, 4)) & 0xFFFFFF; if ($ffs_length == 0xFFFFFF) { # ffs2 with extended length field $ffs_length = unpack("Q", substr($_, 0x18, 8)); } if ($ffs_length != $length) { warn sprintf "%s: file size 0x%x != header size 0x%x\n", $ARGV, $length, $ffs_length, ; die "\n" unless $force; } # force at least 8 byte alignment for the section my $unaligned = $length % 8; $_ .= chr(0xFF) x (8 - $unaligned) if $unaligned != 0; # if the current offset does not align with the block size, # we should add a pad section until the next block my $block_unaligned = $block_size - (length($data) % $block_size); $block_unaligned += $block_size if $block_unaligned < 0x18; $data .= pad($block_unaligned - 0x18); warn sprintf "%s: 0x%x bytes offset %x\n", $ARGV, length $_, length $data if $verbose; # finally add the section $data .= $_; } if (length $data > $size) { warn sprintf "%s: data size 0x%x > volume size 0x%x\n", $output, length $data, $size ; die "\n" unless $force; } else { # pad out with empty space to the end of the volume warn sprintf "%s: 0x%x out of 0x%x\n", $output, length $data, $size, if $verbose; $data .= chr(0xFF) x ($size - length($data)); } if ($output eq '-') { print $data; } else { open OUTPUT, ">", $output or die "$output: Unable to open: $!\n"; print OUTPUT $data; close OUTPUT; } # EFI_FFS_FILE_HEADER: # 0x0000 16 Name (EFI_GUID) # 0x0010 1 IntegrityCheck.Header (Header Checksum) # 0x0011 1 IntegrityCheck.File -> set to 0xAA (FFS_FIXED_CHECKSUM) and clear bit 0x40 of Attributes # 0x0012 1 FileType -> 0x07 = EFI_FV_FILETYPE_DRIVER # 0x0013 1 Attributes -> 0x00 # 0x0014 3 Size, including header and all other sections # 0x0017 1 State (unused) -> 0X00 sub pad { my $len = shift; warn sprintf "pad 0x%x\n", $len if $verbose; my $ffs = '' . chr(0xFF) x 16 # 0x00 guid . chr(0x00) # 0x10 checksum . chr(0x00) # 0x11 checksum . chr(0xf0) # 0x12 FFS_PAD type . chr(0x00) # 0x13 attributes . chr(($len >> 0) & 0xFF) # 0x14 three size bytes . chr(($len >> 8) & 0xFF) # 0x15 three size bytes . chr(($len >> 16) & 0xFF) # 0x16 three size bytes . chr(0xf8) # 0x17 state ; # fixup the header checksum my $sum = 0; for my $i (0..length($ffs)-2) { $sum -= ord(substr($ffs, $i, 1)); } substr($ffs, 0x10, 2) = chr($sum & 0xFF) . chr(0xAA); return $ffs . chr(0xFF) x ($len - 0x18); }