#!/usr/bin/env perl use warnings; use strict; # # Copyright (C) 2017-2019 Hamish Coleman # # Given an FL2 file from the Lenovo BIOS update, try to recognise which # packing system was used and allow the EC firmware image inside it to # be extracted or reinserted # # Note that this script is just intended to deal with the container and # any actions needed to extract or insert into the container - It does # not check any checksums within the IMG - that should be left to # the img manipulating tools (like mec tools). # # This script now supports extracting a couple of FL1 files, so its name # is starting to be wrong. package log; use warnings; use strict; # # Some simple logging # my @log; sub add { my @entry = caller(); push @entry, @_; push @log, \@entry; } sub print { my $self = shift; for my $entry (@log) { my $package = shift @{$entry}; my $filename = shift @{$entry}; my $line = shift @{$entry}; printf("%s:%i %s ", $filename, $line, $package); printf(@{$entry}); print("\n"); } } 1; package FL2::base; use warnings; use strict; # # The base class for all the various types of FL2 encapsulation # use IO::File; sub new { my $class = shift; my $fh = shift || return undef; # must have a file handle my $self = {}; bless $self, $class; $self->{fh} = $fh; $self->{filesize} = (stat($self->{fh}))[7]; return $self->_check(); } sub set_offset_size { my $self = shift; $self->{offset} = shift; $self->{size} = shift; return $self; } sub offset { return shift->{offset}; } sub size { return shift->{size}; } sub get_block { my $self = shift; my $offset = shift; my $size = shift; return undef if (!$self->{fh}->seek($offset, 0)); my $buf; my $count = $self->{fh}->sysread($buf, $size); return undef if ($count != $size); return \$buf; } # The base class simply says "no" to any checks - override to use sub _check { return undef; } # Once a subclass has set the length and size, we can go and double check it # has the expected copyright message sub _check_copyright { my $self = shift; my $expect = "(C) Copyright IBM Corp. 2001, 2005 All Rights Reserved "; my $check_size = length($expect); my @offsets = ( 0x268, # ARCompact 32bit platforms 0x264, # older 16bit platforms 0x1480, # _EC file with second header (p15) ); while (@offsets) { my $check_offset = shift @offsets; my $buf = $self->get_block($self->offset()+$check_offset, $check_size); return undef if (!defined($buf)); if ($$buf eq $expect) { $self->{flag}{copyright}=$check_offset; return $self; } } log::add("no copyright (offset=0x%x)",$self->offset()); return undef; } # Helper function, called by the real classes if they support extraction sub _extract { my $self = shift; my $imgfile = shift; my $buf = $self->get_block($self->offset(),$self->size()); die("bad read") if (!defined($buf)); my $fh = IO::File->new($imgfile, "w"); if (!defined($fh)) { warn("Could not open $imgfile: $!"); return undef; } my $count = $fh->syswrite($$buf); if ($count != $self->size()) { unlink($imgfile); die("bad write"); } return $self; } # Helper function, called by the real classes if they support insertion sub _insert { my $self = shift; my $imgfile = shift; my $fh = IO::File->new($imgfile, "r"); if (!defined($fh)) { warn("Could not open $imgfile: $!"); return undef; } my $buf; my $count = $fh->sysread($buf, $self->size()); if ($count != $self->size()) { unlink($imgfile); die("bad read"); } if (!$self->{fh}->seek($self->offset(), 0)) { unlink($imgfile); die("bad seek"); } $count = $self->{fh}->syswrite($buf); if ($count != $self->size()) { unlink($imgfile); die("bad write"); } return $self; } 1; package FL2::prefix_ff; use warnings; use strict; # # Look for FL2 files that consist entirely of 0xff up until the IMG # # All the files that used to match this pattern now match one of the checkers # with better signatures # use base qw(FL2::base); sub _check { my $self = shift; # List of known file sizes (basically a doublecheck on the signature) my $known = { 8523776 => [0x500000, 0x20000], # x220 8DHT34WW 12718080 => [0x500000, 0x30000], # xx30 16912384 => [0x500000, 0x30000], }; my $check_offset = 0; my $check_size = 0x1000; my $buf = $self->get_block($check_offset, $check_size); if (!defined($buf)) { log::add("bad get_block"); return undef; } my $try_data = $known->{$self->{filesize}}; if (!defined($try_data)) { log::add("not in known sizes"); return undef; } if ($$buf ne "\xff"x$check_size) { log::add("failed 0xff check"); return undef; } $self->{flag}{encrypted}="yes"; # All current examples of this format were encrypted # Loop through the known offset,size pairs for this filesize, # looking for one that has a matching copyright while (scalar(@{$try_data})) { my $try_offset = shift @{$try_data}; my $try_size = shift @{$try_data}; $self->set_offset_size($try_offset, $try_size); my $match = $self->_check_copyright(); if ($match) { return $match; } } log::add("no copyright message"); return undef; } sub extract { return shift->_extract(shift); } sub insert { return shift->_insert(shift); } 1; package FL2::prefix_garbage; use warnings; use strict; # # Look for FL2 files that have garbage at the beginning and large areas of # 0xff before the IMG # use base qw(FL2::base); sub _check { my $self = shift; # List of known file sizes (basically a doublecheck on the signature) # When found, Data is: # [0] offset of EC firmware # [1] size of EC firmware # [2] offset of block to check for all 0xff as a signature check my $known = { 4213270 => [0x290000, 0x20000, 0x21000], # 4240490 => [0x290000, 0x20000, 0x21000], # at least two x200 images }; # If the filesize is not in our known database, return no match my $known_data = $known->{$self->{filesize}}; if (!defined($known_data)) { log::add("not in known sizes"); return undef; } my $check_size = 0x1000; my $buf = $self->get_block($known_data->[2], $check_size); if (!defined($buf)) { log::add("bad get_block"); return undef; } if ($$buf ne "\xff"x$check_size) { log::add("failed 0xff check"); return undef; } $self->{flag}{encrypted}="no"; # All current examples of this format were not encrypted $self->set_offset_size(@{$known_data}); return $self->_check_copyright(); } sub extract { return shift->_extract(shift); } sub insert { return shift->_insert(shift); } 1; package FL2::prefix_head_NAPI; use warnings; use strict; # # Look for FL2 files that have a "NAPI" header, followed by 0xff, followed # by the IMG # # This was first seen in old x60 FL2 files use base qw(FL2::base); sub _find_napi { my $self = shift; my $offset = 0; while ($offset < $self->{filesize}) { my $buf = $self->get_block($offset, 4); return undef if (!defined($buf)); $buf = $$buf; if ($buf eq 'NAPI') { return $offset; } $offset+=0x10000; } return undef; # not found } sub _check { my $self = shift; my $header_offset = $self->_find_napi(); if (!defined($header_offset)) { log::add("no NAPI header found"); return undef; } my $header_size = 0x20; my $buf = $self->get_block($header_offset, $header_size); if (!defined($buf)) { log::add("bad get_block"); return undef; } my @fields = qw( signature1 unk1 unk2 signature2 unk3 unk4 unk5 signature3 unk6 unk7 unk8 all_00 ); my @values = unpack("a4vCa4vvva4vVVc",$$buf); map { $self->{header}{$fields[$_]} = $values[$_] } (0..scalar(@fields)-1); if ($self->{header}{signature1} ne "NAPI") { log::add("failed signature"); return undef; } if ($self->{header}{signature2} ne "ESCD") { log::add("failed signature"); return undef; } if ($self->{header}{signature3} ne "ACFG") { log::add("failed signature"); return undef; } # because I have no idea about the actual format of this header, we check # every field if (($self->{header}{unk1} != 0x000f) || ($self->{header}{unk2} != 0x8d) || ($self->{header}{unk3} != 0x000f) || ($self->{header}{unk4} != 0x000e) || ($self->{header}{unk5} != 0x000e) || ($self->{header}{unk6} != 0x0200) || ($self->{header}{unk7} != 0x00000000) || ($self->{header}{unk8} != 0x0000fedf) || ($self->{header}{all_00} != 0x00) ) { die("Unexpected NAPI header data"); # die instead of simply logging, since we have matched a signature # and dont want to let this fall through to another checker } # TODO # - there is one more field at offset 0x1fff, containing a 0x02 my $check_offset = 0x2000; my $check_size = 0x1000; $buf = $self->get_block($check_offset, $check_size); if (!defined($buf)) { log::add("bad get_block"); return undef; } if ($$buf ne "\xff"x$check_size) { log::add("failed 0xff check"); return undef; } $self->{flag}{encrypted}="no"; # All current examples of this format were not encrypted $self->set_offset_size($header_offset + 0x10000, 0x20000); return $self->_check_copyright(); } sub extract { return shift->_extract(shift); } sub insert { return shift->_insert(shift); } 1; package FL2::prefix_nothing; use warnings; use strict; # # Some FL2 files are simply the IMG data, with no header or container. # For these, we can check the copyright string from inside the IMG # use base qw(FL2::base); sub _check { my $self = shift; # List of known file sizes (basically a doublecheck on the signature) my $known = { 196608 => [0, 0x30000], # x240 GIHT32WW }; if (!defined($known->{$self->{filesize}})) { log::add("not in known sizes"); return undef; } $self->{flag}{encrypted}="yes"; # All current examples of this format were encrypted $self->set_offset_size(@{$known->{$self->{filesize}}}); return $self->_check_copyright(); } sub extract { return shift->_extract(shift); } sub insert { return shift->_insert(shift); } 1; package FL2::prefix_head_EC; use warnings; use strict; # # Look for FL2 files that have a "_EC" prefix header at their start # use base qw(FL2::base); sub _check { my $self = shift; # List of known file sizes (basically a doublecheck on the signature) # We also store the encryption flag in here my $known = { 196896 => 'yes', 229728 => 'unk', # p15 262176 => 'no', #262432 # p15g2 286752 => 'no', }; my $header_offset = 0; my $header_size = 0x20; my $trailer_size = 0x100; my $buf = $self->get_block($header_offset, $header_size+0x20); if (!defined($buf)) { log::add("bad get_block"); return undef; } my @fields = qw( signature version filesize imgsize hash_algo sign_algo hash_crc16 header_crc16 unk1 signature2 unk2 imgmax imgsize2 ); my @values = unpack("a3CVVVVSSa8a4a17VV",$$buf); map { $self->{header}{$fields[$_]} = $values[$_] } (0..scalar(@fields)-1); if ($self->{header}{signature} ne "_EC") { log::add("failed signature"); return undef; } if ($self->{header}{version} != 1) { log::add("failed version"); return undef; } if ($self->{header}{filesize} != $self->{filesize}) { log::add("failed header filesize"); return undef; } if (!defined($known->{$self->{filesize}})) { log::add("not in known sizes"); return undef; } my $imgsize = $self->{header}{imgsize}; if ($self->{header}{signature2} eq "^M;*") { # a second header with a different imgsize value in it !? log::add("found signature2"); $header_size = 0x60; $imgsize = $self->{header}{imgsize2}; } # p15g2 has a second _EC header, with what looks like the same fields # as the first one (unlike the above ^M;* header) but it also has a # ^M;* header slightly later in the file ... if ($imgsize+$header_size+$trailer_size == $self->{filesize}) { # there is an additional block of 0x100 appended to the FL2, outside # of the header defined IMG file # - I expect this is the digital signature $self->{flag}{trailer}="external"; log::add("trailer external"); } elsif ($imgsize+$header_size == $self->{filesize}) { # the additional block still looks like it is there, but they have # changed the accounting to count it in the IMG - these files are also # significantly larger in size $self->{flag}{trailer}="internal"; log::add("trailer internal"); } else { log::add("unexpected filesize/imgsize results"); return undef; } # FIXME # - is that a csum? how to generate it and check it? $self->{flag}{encrypted} = $known->{$self->{filesize}}; $self->set_offset_size($header_size, $imgsize); return $self->_check_copyright(); } sub extract { return shift->_extract(shift); } # Note, no insert() will work until we know how to generate the checksum #sub insert { # my $self = shift; # my $imgfile = shift; # # my $write = $self->_insert($imgfile); # # Calculate checksum # write checksum to header #} 1; package FL1::PFH_header; use warnings; use strict; # # Look for FL1 files that have a "$PFH" prefix header near their end # # Seen in at least the l530 firmware updates # # Thanks for skochinsky for the details: # https://github.com/hamishcoleman/thinkpad-ec/issues/46 # use base qw(FL2::base); sub _find_pfh { my $self = shift; my $offset = $self->{filesize}-4; while ($offset > 0) { my $buf = $self->get_block($offset, 4); return undef if (!defined($buf)); $buf = $$buf; if ($buf eq '$PFH') { log::add("found PFH header signature at offset=0x%x", $offset); return $offset; } $offset-=0x04; } log::add("no PFH found"); return undef; # not found } sub _check { my $self = shift; # List of known file sizes (basically a doublecheck on the signature) my $known = { 4681808 => 1, # l440 9437264 => 1, # l430 13631568 => 1, # e330 12587008 => 1, }; my $capsule_offset_hack = $self->{capsule_offset_hack} || 0; my $header_offset = $self->_find_pfh(); return undef if (!defined($header_offset)); my $header_size = 4+4+4+2+4+2+4+4; my $buf = $self->get_block($header_offset, $header_size); if (!defined($buf)) { log::add("bad get_block"); return undef; } if (!defined($known->{$self->{filesize}})) { log::add("not in known sizes"); } my @fields = qw( signature version headersize headerchecksum totalimagesize totalimagechecksum numberofimages imagetableoffset ); my @values = unpack("a4VVvVvVV",$$buf); map { $self->{header}{$fields[$_]} = $values[$_] } (0..scalar(@fields)-1); if ($self->{header}{signature} ne '$PFH') { log::add("failed signature"); return undef; } if ($self->{header}{version} ne 0x10002) { log::add("failed version"); return undef; } # now load the partition table $buf = $self->get_block( $self->{header}{imagetableoffset}+$capsule_offset_hack, (4+4+8+4)*$self->{header}{numberofimages} ); if (!defined($buf)) { log::add("bad get_block"); return undef; } $buf = $$buf; while ($buf) { my $dir = {}; @fields = qw(FileOffset Size FlashAddress NameOffset _rest); @values = unpack("VVQVa*",$buf); map { $dir->{$fields[$_]} = $values[$_] } (0..scalar(@fields)-1); $buf = $dir->{_rest}; delete $dir->{_rest}; my $buf2 = $self->get_block( $dir->{NameOffset}+$capsule_offset_hack, 32 # TODO - just a guess at the max name size ); if (!defined($buf2)) { log::add("bad get_block"); return undef; } my $name = unpack("Z*",$$buf2); $dir->{Name} = $name; push @{$self->{header}{dir}}, $dir; if ($name eq 'Ec') { $self->set_offset_size( $dir->{FileOffset}+$capsule_offset_hack, $dir->{Size}, ); return $self; } } log::add("no subsection found with name 'Ec'"); return undef; } sub extract { return shift->_extract(shift); } sub insert { # FIXME: # - there is a "totalimagechecksum" field # (which we probably should be updating return shift->_insert(shift); } 1; package EFI::Capsule::PFH_header; use warnings; use strict; # # Adjust the usual PFH loader to cope with an offset # (presumably caused by the Capsule wrappers) # use base qw(FL1::PFH_header); sub _check { my $self = shift; $self->{capsule_offset_hack} = 0x1d0; # This is probably: # header1_size + header2_size + ? # 0x50 + 0x168 + 0x18 #$self->{capsule_offset_hack} # = $self->{header1}{HeaderSize} + $self->{header2}{HeaderLength}; return $self->SUPER::_check(); } 1; package EFI::Capsule::UNK_header; use warnings; use strict; # # Some FL1 files appear to have an unknown header after the EFI headers # use base qw(FL2::base); sub _check { my $self = shift; my $header3_offset = $self->{header1}{HeaderSize} + $self->{header2}{HeaderLength}; my $header3_length = 0x30; my $buf = $self->get_block($header3_offset, $header3_length); if (!defined($buf)) { log::add("bad get_block"); return undef; } # d95fe99c196ee7409a8febad7f78e0c5 my $maybe_uuid = "\xd9\x5f\xe9\x9c\x19\x6e\xe7\x40\x9a\x8f\xeb\xad\x7f\x78\xe0\xc5"; my @fields = qw( all_ff unk01 maybe_uuid maybe_offset unk02 ); my @values = unpack("a16a8a16VV",$$buf); map { $self->{header3}{$fields[$_]} = $values[$_] } (0..scalar(@fields)-1); if ($self->{header3}{maybe_uuid} ne $maybe_uuid) { log::add("failed header3 maybe_uuid"); return undef; } # FIXME: This is just a guess my $ec_size = 0x20000; $self->set_offset_size( $header3_offset + $header3_length + $self->{header3}{maybe_offset}, $ec_size ); return $self; } sub extract { return shift->_extract(shift); } # no insert() will work until we know how to generate the checksums 1; package EFI::Capsule; use warnings; use strict; # # Look for FL1 files that have a known EFI capsule prefix # use base qw(FL2::base); sub _check { my $self = shift; # bd86663b760d3040b70eb5519e2fc5a0 my $capsule_uuid = "\xbd\x86\x66\x3b\x76\x0d\x30\x40\xb7\x0e\xb5\x51\x9e\x2f\xc5\xa0"; my $buf = $self->get_block(0,16); if (!defined($buf)) { log::add("bad get_block"); return undef; } $buf = $$buf; if ($buf ne $capsule_uuid) { log::add("no capsule_uuid"); return undef; } $buf = $self->get_block(0x10, 4); if (!defined($buf)) { log::add("bad get_block"); return undef; } my $header1_size = unpack("V", $$buf); $buf = $self->get_block(0, $header1_size); if (!defined($buf)) { log::add("bad get_block"); return undef; } my @fields = qw( UUID HeaderSize Flags CapsuleImageSize SequenceNumber InstanceId ); my @values = unpack("a16VVVVa16",$$buf); map { $self->{header1}{$fields[$_]} = $values[$_] } (0..scalar(@fields)-1); if ($self->{header1}{CapsuleImageSize} != $self->{filesize}) { log::add("failed header1 filesize"); return undef; } # d954937a68044a4481ce0bf617d890df my $firmware_uuid = "\xd9\x54\x93\x7a\x68\x04\x4a\x44\x81\xce\x0b\xf6\x17\xd8\x90\xdf"; $buf = $self->get_block($header1_size, 0x38); if (!defined($buf)) { log::add("bad get_block"); return undef; } @fields = qw( ZeroVector FileSystemGuid FvLength Signature Attributes HeaderLength Checksum Reserved Revision ); @values = unpack("a16a16Q{header2}{$fields[$_]} = $values[$_] } (0..scalar(@fields)-1); #if ($self->{header2}{ZeroVector} ne '\x00'x16) { # log::add("failed header2 zerovector"); # return undef; #} if ($self->{header2}{FileSystemGuid} ne $firmware_uuid) { log::add("failed header2 FileSystemGuid"); return undef; } if ($self->{header2}{FvLength} != ($self->{filesize}-$header1_size)) { log::add("failed header1 filesize"); return undef; } if ($self->{header2}{Signature} ne '_FVH') { log::add("failed header2 Signature"); return undef; } # TODO: # - check Checksum # "A 16-bit checksum of the firmware volume header. A valid header sums to zero" if ($self->{header2}{Revision} == 1) { # So far, the only revision 1 headers seen have also had a PFH header # (with an annoying offset) bless $self, 'EFI::Capsule::PFH_header'; return $self->_check(); } if ($self->{header2}{Revision} == 2) { # l440 has this bless $self, 'EFI::Capsule::UNK_header'; return $self->_check(); } return undef; } sub extract { return shift->_extract(shift); } # no insert() will work until we know how to generate the checksums 1; # TODO # - 8muj19us.iso has yet another format: # It is all 0xff until 0x500000, however it then looks like it has a header. # There is certainly not the normal copyright string, and the EC version # string is in entirely the wrong place # 00500000 45 43 20 49 4d 41 47 45 30 00 00 00 00 00 00 00 |EC IMAGE0.......| # 00500010 d0 08 38 4d 48 54 37 39 57 57 00 00 00 20 02 00 |..8MHT79WW... ..| # 00500020 00 00 00 00 31 36 01 00 00 00 00 00 0d b3 9e 00 |....16..........| package main; use warnings; use strict; use Data::Dumper; $Data::Dumper::Indent = 1; $Data::Dumper::Sortkeys = 1; $Data::Dumper::Quotekeys = 0; use IO::File; # Call each of the detectors in turn to try to find the location of the IMG sub detect_img { my $fl2name = shift; my $fh = IO::File->new($fl2name, "r+"); if (!defined($fh)) { warn("Could not open $fl2name: $!"); return undef; } my $object; # These checks have a good signature detection if (!defined($object)) { $object = FL2::prefix_head_EC->new($fh); } if (!defined($object)) { $object = EFI::Capsule->new($fh); } # These checks always do a file search if (!defined($object)) { $object = FL1::PFH_header->new($fh); } if (!defined($object)) { $object = FL2::prefix_head_NAPI->new($fh); } # these checks have very little in the way of signatures, so only # check for them after everything else if (!defined($object)) { $object = FL2::prefix_ff->new($fh); } if (!defined($object)) { $object = FL2::prefix_garbage->new($fh); } if (!defined($object)) { $object = FL2::prefix_nothing->new($fh); } return $object; } sub main() { # get args my $cmd = shift @ARGV; if (!defined($cmd)) { die("Need command"); } my $fl2file = shift @ARGV; if (!defined($fl2file)) { die("Need FL2 filename"); } my $imgfile = shift @ARGV; if ($cmd ne 'check' && !defined($imgfile)) { die("Need IMG filename"); } # validity check args if ($cmd ne "from_fl2" && $cmd ne "to_fl2" && $cmd ne "check") { die("direction must be one of 'from_fl2' or 'to_fl2'"); } my $verbose = shift @ARGV; if (defined($verbose) && $verbose eq 'verbose') { $verbose = 1; } else { $verbose = 0; } if ( ! -e $fl2file ) { die("FL2 file $fl2file must exist"); } my $object = detect_img($fl2file); if (!defined($object)) { printf("Could not determine IMG details for %s\n", $fl2file); if ($verbose) { log::print(); } exit(1); } printf("IMG at offset 0x%x size 0x%x (%s %s)\n", $object->offset(), $object->size(), ref($object), $fl2file, ); if ($verbose) { print(Dumper($object)); log::print(); } if ($cmd eq 'from_fl2') { if (!$object->can('extract')) { die("FL2 container type cannot extract"); } return $object->extract($imgfile); } if ($cmd eq 'to_fl2') { if (!$object->can('insert')) { die("FL2 container type cannot insert"); } return $object->insert($imgfile); } } unless(caller) { main(); }