#!/usr/bin/env perl use warnings; use strict; # # Copyright (C) 2017-2019 Hamish Coleman # # The Lenovo BIOS update ISO images contain an embedded hard drive image, # complete with a partition table. # # Qemu does not support disk images with more than 16 heads, but the lenovo # disk image is created with 64 heads. Since the MBR they used does not use # LBA to access the disk, the mismatch between the Qemu reported disk data # and what is in the partition confuses the MBR and causes the boot to fail. # use Data::Dumper; $Data::Dumper::Indent = 1; $Data::Dumper::Sortkeys = 1; $Data::Dumper::Quotekeys = 0; use IO::File; sub mbr_get { my $imagefile = shift; my $fh = IO::File->new($imagefile,"r"); if (!defined($fh)) { die("Could not open $imagefile: $!"); } my $buf; my $count = $fh->sysread($buf,512); die("bad read") if ($count != 512); return $buf; } sub mbr_put { my $imagefile = shift; my $buf = shift; die("bad buf len") if (length($buf) != 512); my $fh = IO::File->new($imagefile,"r+"); if (!defined($fh)) { die("Could not open $imagefile: $!"); } my $count = $fh->syswrite($buf,512); die("bad write") if ($count != 512); } sub mbr_part_unpack { my $part = shift; my $result = {}; $result->{_input} = unpack('H*', $part); my @fields = qw( flag start_head start_cysec start_cyl type end_head end_cysec end_cyl sector_offset sector_total ); my @values = unpack("CCCCCCCCVV",$part); map { $result->{$fields[$_]} = $values[$_] } (0..scalar(@fields)-1); $result->{start_cyl} |= ($result->{start_cysec} & 0xc0) <<2; $result->{start_sec} = $result->{start_cysec} & 0x3f; delete $result->{start_cysec}; $result->{end_cyl} |= ($result->{end_cysec} & 0xc0) <<2; $result->{end_sec} = $result->{end_cysec} & 0x3f; delete $result->{end_cysec}; return $result; } sub mbr_part_pack { my $part = shift; # check sanity die("start cyl too big") if ($part->{start_cyl} > 1023); die("start sec too big") if ($part->{start_sec} > 0x3f); die("end cyl too big") if ($part->{end_cyl} > 1023); die("end sec too big") if ($part->{end_sec} > 0x3f); # TODO # - add support for larger cylinder numbers # This needs adding support for the bitfiddling to move the highbits to # the other fields. # This should only occur when the .iso image file is larger than 255Meg if ($part->{start_cyl} > 0xff) { printf("Unsupported start_cyl (0x%x)\n", $part->{start_cyl}); die; } if ($part->{end_cyl} > 0xff) { printf("Unsupported end_cyl (0x%x)\n", $part->{end_cyl}); die; } $part->{start_cysec} = $part->{start_sec}; $part->{end_cysec} = $part->{end_sec}; my @fields = qw( flag start_head start_cysec start_cyl type end_head end_cysec end_cyl sector_offset sector_total ); my @values; for my $field (@fields) { push @values, $part->{$field}; } my $result = pack("CCCCCCCCVV",@values); $part->{_output} = unpack('H*', $result); return $result; } sub mbr_unpack { my $mbr = shift; my $result = {}; $result->{_input} = unpack('H*', $mbr); my @fields = qw( bootstrap diskid partitions signature ); my @values = unpack("a436a10a64S",$mbr); map { $result->{$fields[$_]} = $values[$_] } (0..scalar(@fields)-1); my @partitions; for my $part (unpack("(a16)*",$result->{partitions})) { push @partitions, mbr_part_unpack($part); } $result->{partitions} = \@partitions; return $result; } sub mbr_pack { my $mbr = shift; $mbr->{_partitions} = $mbr->{partitions}; my @partitions; for my $part (@{$mbr->{partitions}}) { push @partitions, mbr_part_pack($part); } $mbr->{partitions} = pack("(a16)*", @partitions); my @fields = qw( bootstrap diskid partitions signature ); my @values; for my $field (@fields) { push @values, $mbr->{$field}; } my $result = pack("a436a10a64S",@values); $mbr->{_output} = unpack('H*', $result); return $result; } # This function does the actual work of this script # sub fixup_part { my $part = shift; # dont touch it if the partition is not broken return undef if ($part->{end_head} < 0x10); # convert from zero-based index to the total count $part->{end_head}++; $part->{end_cyl}++; my $total_sec1 = $part->{end_sec} * $part->{end_head} * $part->{end_cyl}; # Reduce the number of heads and increase the number of cylinders while ($part->{end_head} >0x10) { $part->{end_head} = int($part->{end_head}/2); $part->{end_cyl} *= 2; } my $total_sec2 = $part->{end_sec} * $part->{end_head} * $part->{end_cyl}; # Ensure we have at least as many total sectors as before the fixup while ($total_sec2 < $total_sec1) { $part->{end_cyl}++; $total_sec2 = $part->{end_sec} * $part->{end_head} * $part->{end_cyl}; } # convert from total count back to zero-based index $part->{end_head}--; $part->{end_cyl}--; return 1; } # Some boot records downloaded from Lenovo appear to have been corrupted # (perhaps this is an attempt to force people to use a UEFI boot?) # sub fixup_boot { my $buf = shift; if (ord(substr($buf,0,1)) == 0) { # No normal x86 boot instruction starts with a zero. warn("INFO: Original Lenovo ISO contains a zero in MBR bootcode - attempting fix\n"); substr($buf,0,1) = chr(0xfa); } return $buf; } sub main() { if (!defined($ARGV[0])) { die("Need image filename"); } my $imagefile = $ARGV[0]; my $buf = mbr_get($imagefile); my $mbr = mbr_unpack($buf); for my $part (@{$mbr->{partitions}}) { fixup_part($part); } $buf = mbr_pack($mbr); $buf = fixup_boot($buf); if (defined($ARGV[1]) && $ARGV[1] eq 'debug') { print(Dumper($mbr)); } else { mbr_put($imagefile,$buf); } } unless (caller) { # only run main if we are called as a CLI tool main(); }