Malware has been used numerous times by attackers to destroy a computer’s Master Boot Record, rendering it inoperable. By erasing the MBR, the machine is unable to load the operating system. There is no easy way to rewrite the Master Boot Record into place without an operating system, and the machine becomes completely useless and unrecoverable. In addition, many ransomwares infect the master boot record by overwriting it with malicious code. The system is then automatically restarted to allow the infection to take place. When the system restarts, the user is locked out, and the ransomware displays a note demanding payment. Simple money!
To understand how all of this is possible, and how an attacker can achieve it, we must first understand the MBR and the process of its execution.
The boot process
The booting procedure of a system has become simpler over time, but this does not always imply that it is any easier. Every computer, big or small, goes through a start-up procedure known as the “Boot” process. Because different types of hardware operate in different ways, the boot procedure is heavily influenced by the type of CPU architecture and other hardware components.
To avoid confusion, I won’t go into great detail about each stage of the booting process. However, a typical linux booting procedure involves the following phases at a higher level:
Power On Self Test
After BIOS is up and running, it initiates a quick self test to know if all the required hardware components are in working condition.
Find a boot device
This step finds all the bootable devices from the earlier detected hard drives. The way this works is by checking the MBR (Master Boot Record) for each detected devices. MBR is refered to the first 512 bytes of any bootable device.
Load the MBR
MBR is the first 512 bytes. These 512 bytes contains a bootloader, partition table and the magic number. This is loaded into ram and is responsible to read data from drives and start the operating system.
This is a boot loader program which works in 2 stages. First stage is a small machine code binary on MBR. Its sole job is to locate the second stage boot loader and load it in memory. Once the second stage boot loader is in the memory, it presents the user with a graphical screen showing the different operating systems to choose from.
The above OS selection decides what kernel and optional initramfs is to be loaded into memory. The kernel then initializes and configures the computer’s memory and configures the various hardware attached to the system, including all the I/O subsystems. After some more operations, the kernel is completely loaded into memory and is operational. It’s time to set up the user environment.
This is the first userspace program that is started by kernel. Now this starts and manages all the userspace processes like your web browser, file manager, web servers, etc.
MBR and other little things
If you’re not already aware, this is how a typical hard drive appears from the outside.
There are numerous components inside this small semi-metallic box that aid in its proper operation.
But we don’t need to know about all of these components; instead, we’ll concentrate on the disc-like structure in the centre. This is known as a platter. A platter is a single recording disc. A hard disc drive may have one or more platters.
Each platter is divided into several circular tracks, and each track is further divided into several sectors. Each sector on a hard disc drive typically stores 512 bytes of user-accessible data.
The first 512 bytes (or first sector) of a hard drive is where the MBR is located. And since everything in Linux is a “file”, if we want to extract MBR data, all we have to do is to read the first 512 bytes of our bootable hard disk file and then write that content to another local file for further analysis. In most of the linux platforms, we can do this by
dd 2 command.
The above command will read a 512-byte block (once) from
/dev/sda and save it in the
mbr.sample file. Then we can the check the type of this file using
An x86 boot sector is recognised in this file. Interestingly, it also lists the start head, start sector, total number of sectors, offset, and IDs of all the partitions. This was sufficient reason for me to dig up the file’s hexdump and understand how
file command is able to gather all this information.
Dissecting the Master Boot Record
Without any knowledge of layout, simply looking at the
hexdump output is not particularly helpful. Therefore, it is now necessary to understand the MBR layout.
MBR consists of 3 parts - bootloader, partition table, and magic number.
The magic number is found in the final two bytes, as opposed to the regular userspace files. It is
55AA in the file, but be mindful of the processor’s endianness. Since my CPU is little endian, the leftmost bytes are read first. As a result,
AA55 will become the magic number.
After that is subtracted, we are left with
512-2 = 510 bytes. Out of these, the bootloader is stored in the first
446 bytes, and the partition tables are stored in the remaining
64 bytes. To evaluate these components separately, let’s extract them into distinct files using the same old
It’s fantastic that the file command can recognise each of these MBR components separately. Now with separate files, we can carefully examine the partition table and determine what data it can give us.
The MBR system only supports 4 primary partitions since partition tables actually only contain 4 records. We must divide a primary partition into smaller partitions and keep a separate partition table inside of that primary partition if we want to construct more than four partitions. The term “Expanded partitions” is in fact used to describe these extended partitions. We can see from the result above that there are a total of 64 bytes, giving us a total of
64/4 = 16 bytes for each record. Let’s understand the layout of these 16 bytes and then we can analyze the partition table data using
|Size (in bytes)||Purpose|
|1||Boot indicator (0x80 for active and 0x00 for inactive)|
|1||partition start: head|
|1||partition start: sector|
|1||partition start: cylinder|
|1||partition end: head|
|1||partition end: sector|
|1||partition end: cylinder|
|4||Number of sectors before the beginning of this partition (sectors_before)|
|4||Number of sectors in this partition (number_of_sectors)|
There are 16 bytes in all of that. Now we know where the information that the
file command was displaying previously comes from.
Based on the information we have now, we can figure out few things on our own…
For example, this disk only has 2 partitions because the final 2 records are all zeros. Due to the ‘0x80’ byte in the first records, the first partition is bootable. And the ID for that partition is
83. While the second partition is non bootable partition, and the ID of that partition is
82. If you want, you can even calculate the size of each partition with the help of other information present in these records.
We are now down to the first
446 bytes, which include the bootloader.
The bootloader is simply a software that reads and loads other applications from the bootable partition.GRUB typically loads the second stage of itself from disk, however this is not a condition.There are bootloaders that load the kernel directly into memory or, even better, some of them are full-fledged application that just works. 3
Note:- Although I won’t be discussing it today, you can use the
ndisasm disassembler to disassemble the bootloader image.This will require for some knowledge of the interrupts and memory management in the BIOS, which is outside the scope of this blog.
That settles it; now that we are aware of what is contained within an MBR, why don’t we attempt to construct one?
Creating your own bootloader
To start, we’ll make a simple raw binary file and put
AA55 in it. Keep in mind that this is the magic number that belongs in an MBR.
Save this file as
custom bootloader.asm. After compiling it with the
nasm compiler, the results should look like this.
We now have a 2 byte file containing the magic number. However, because the MBR is 512 bytes long, we must fill 510 more bytes. For the time being, let’s just fill it with zeros and see if it’s a valid MBR file.
The above code will write
0 510 times and then write
This is, as expected, a valid MBR file with no information about the partition table or the bootloader. Can we, however, use this to boot the system?
Let’s make an attempt.
I first tried without any mbr data to see what errors I would get when it fails.
And, as I was expecting, it said “no bootable device.”
Let’s run the test again, but this time with the MBR file we made.
It did not give me the error this time. That must imply that our MBR is functional. Since it lacks bootloader code, it does nothing. However, it is not returning the same previous error.
We can now add new instructions to our assembly file. However, we must keep in mind that we do not exceed the file’s 512-byte limit. That means we’ll have to take care of the zeros we’re padding with. Because this is a very simple problem, there are special characters that can assist us in calculating the memory address of the beginning of the file and the current address in the file.
We can calculate the exact number of zeros required for padding using these special characters. Let’s compile it and put it to the test.
This produces the same results as before, and the output file size remains 512 bytes. Let’s add some more instructions to help us write some text on the screen.
Unlike userspace and kernelspace programs, we do not have any helper functions that can take a string and automatically print it to the screen. We’ll have to tell the BIOS to do what we want here. And the only way I’m aware of is through interrupts. It is the same facility that operating systems and application programmes use to access BIOS functions.
Here is a list of common BIOS interrupts. Not all BIOS (especially older ones) support all of these interrupts. The basic idea of using interrupts is we place proper values in some specific registers, and then trigger the interrupt. The interrupt routine will then fetch the values from those registers and based on that, it’ll perform some action.
Anyway, using the above table, I determined that we needed to use interrupt vector
10h (or 0x10) with interrupt vector
03h (or 0x03) in
AH register. Consider it as invoking the 10h function with the parameter value 03h. This returns the cursor’s current position and shape.
We can see that some initial bytes are written to the binary file after compiling and inspecting the hexdump…. And, thanks to
$$, the file size remains 512 bytes.
This switches the BIOS to TTY mode, allowing me to print characters using the same interrupt
10h but with a different value in the
When we compile and run this with qemu, we get the message “HEY” printed on the screen.
Now that we know how to write characters on the screen, let’s make a string and loop through it until the end, printing each character on the screen one by one using the same interrupt combination.
Unfortunately, testing the above code does not produce the desired results, but instead produces some garbage values.
Further investigation revealed that our bootloader in memory is not properly aligned. This led me down another rabbit hole, this time about how the contents of the computer’s physical memory look when the BIOS jumps to my bootloader code. Here is a dedicated page on the same topic here which covers a lot of details about it.
For us, we need to add a few more instructions to our code to properly align it. Finally, our code will look like this.
This time we get the desired result after compiling and testing the above.
We successfully created a bootloader that prints some message on the screen.
We know that a MBR sector is comprised of 3 parts:
- bootloader (446 bytes)
- partition table (64 bytes)
- magic number (2 bytes)
And each component can be extracted separately and treated as a regular binary file. This means that we can create backups of only partition tables if necessary. Alternatively, we can replace the bootloader code with another code without affecting the partition table.(Obviously for fun; like a friendly joke, nothing malicious) 😈 😈
We know our above “Hack the World!!” code does not use all 510 bytes, so why not shrink it a little to fit in 446 bytes? This way we can protect the original partition table.
This will generate the raw data file containing the bootloader program, which we can quickly test in a virtual machine.
Vagrantfile above will launch a quick test VM. We just need to sit back and relax.
After successful bootup and reboot, It displayed the expected message.