Protected MBR

Sep. 21, 2023

Every programmer who wants to deeply understand the principles of the computer’s low-level work rules thinks about writing their own operating system. No matter how complicated your OS is going to be, the first thing you need to do is to implement the Main Boot Record (MBR). MBR is the first program that BIOS executes on the bootable device. That article describes how to implement custom MBR protected with a password.

Theory of OS loading, and why we need MBR

As described in the first paragraph, MBR is the first program that BIOS loads and executes on your bootable device (hard drive, floppy or USB drive). BIOS performs the following steps:

MBR structure

MBR contains:

In general, MBR checks the partition table, finds the active partition and starts partition loading.

In our case, we want to protect our MBR with a password. For simplifying our task we will set the limitations:

Which instruments we need:

First steps

Firstly, we will create a sample project and write the following code:

org 7c00h
use16
  
password_str db 'Enter password:', 0
password db 6 dup(0)
original_password db 'OtUs77', 0
db 510-($-$$) dup(0), 0x55, 0xaa
buf:

org 7c00h is a directive that tells FASM “our code will be loaded at address 7c00h (or 0x7c00 - it is the same). The directive is important, because without it FASM can’t correctly define the offsets of subprograms, and may jump to incorrect addresses in memory.

use16 tells FASM to generate 16-bit code.

password_str, password, and original_password are the strings for storing “Enter password:”, the password that the user will enter, and the password which protects our MBR from unauthenticated users.

The last two strings just place bytes 0x55 0xAA at the end of the MBR.

That code will do nothing because it does not define any operation, but now we have a “boilerplate”. Let’s try to show the string “Enter password:” to the user.

Printing on the screen

To print any ASCII character on the screen we have an interrupt int10h. This interrupt has a few functions, but we need only printing on the screen, so we use the 0Eh function of this interrupt.

Use the int10h as follows:

For printing our string we will use loops. A little disclaimer: in this article, we will implement a few procedures, so we will use the calling convention stdcall. It means that the arguments of the callable procedure will be placed into the stack, from right to left (the last parameter is the first).

org 7c00h
use16

; Load the “Enter password:” string address into AX
lea ax, [password_str]
; Push it to the stack
push ax
; Call “print” subprogram
call print

; Print function
print:
; Prologue. Save original BP value to the stack
    push bp
; Copy SP value to BP register, next time we will use BP instead of SP
; Because we don't need to corrupt its value
    mov bp, sp
; Set the CX register to zero, assembler’s loop uses CX as a loop counter, but we will print the characters, while we don’t get zero, means the end of the string (null-terminated strings indicate the end of the string by appending zero bytes)
    xor cx, cx
; Copy the first argument - password_str address from the stack to the SI register
    mov si, [bp+4]
; Printing loop
    loop_print:
; lodsb loads a byte from the address that is placed into the SI register and increments the SI value to the next byte. Byte, loaded by lodsb, is placed into AL register
        lodsb
; Check AL for the string terminator
        test al, al
; If we get the end of the string - loop ends
        jz end_print
; If not, we call the int10h with 0Eh function to print the character from AL
        mov ah, 0Eh
        int 10h
    loop loop_print
; Epilogue - restore original BP value and return
    end_print:
    mov sp, bp
    pop bp
    ret
  
password_str db 'Enter password:', 0
password db 6 dup(0)
original_password db 'OtUs77', 0
db 510-($-$$) dup(0), 0x55, 0xaa
buf:

Try to compile it and run it in any emulator. You will see the “Enter password:” string on the screen! That’s much better, don’t you think?

Reading user input

We have successfully dealt with printing, now we need to read the user’s password. If we have the interrupt to print we have the interrupt to read. Int 16h and its 00h function do the reading of keyboard input. We need to read the input and store the values into our password string.

org 7c00h
use16

jmp loop_protect

lea ax, [password_str]
push ax
call print

; Save the address of the memory area, used to store the user's input
lea di, [password]
; Push it to the stack as the argument to the function
push di
; Call read function
call read
    
print:
    push bp
    mov bp, sp
    xor cx, cx
    mov si, [bp+4]
    loop_print:
        lodsb
        test al, al
        jz end_print
        mov ah, 0Eh
        int 10h
    loop loop_print
    
    end_print:
    mov sp, bp
    pop bp
    ret

read:
; Prologue. Save BP and copy SP to BP
    push bp
    mov bp, sp
; We suppose that our password contains 6 characters or less, so users can input up to 6 characters, but not more. CX is used as the loop counter.
    mov cx, 6
; Copy the password’s memory area address to the DI register. DI is used by the stosb command that saves byte from AH to the memory block, addressed by DI, and increases DI value
    mov di, [bp+4]
    
    loop_read:
; Input interrupt, we wait for the user's input
        mov ah, 0
        int 16h    
; Check the value, if the user presses Enter (1Ch code) - it is the end of the input
        cmp ah, 1Ch
; If input ends we don’t need to read more
        je end_read
; Save byte from AH to memory (address stored into DI)
        stosb
; Print ‘*’ to the screen so that the user may see the keyboard works :)
        mov ah, 0Eh
        mov al, 2Ah
        int 10h
    loop loop_read
            
    end_read:
    mov sp, bp
    pop bp
    ret
  
password_str db 'Enter password:', 0
password db 6 dup(0)
original_password db 'OtUs77', 0
db 510-($-$$) dup(0), 0x55, 0xaa
buf:

Ending up our print-read cycle, we have “Enter password:” output, and user input processing. Next, we need to compare the passwords. For that reason, we will implement the “check_password” function.

org 7c00h
use16

lea ax, [password_str]
push ax
call print
lea di, [password]
push di
call read

; Load user input to DI and push it to the stack
lea di, [password]
push di
; Load the original password to DI and push it to the stack
lea di, [original_password]
push di
; Call check
call check_password
    
print:
; Print

read:
; Read

check_password:
    push bp
    mov bp, sp
; First of all, we need to clear registers AX, BX, and DX.
; We will use only the lodsb command that loads a byte into the AL register, so we need to load a byte of the user’s input, and original password, and compare them. DX will be used as temp SI value, because lodsb increases SI, but we have two different memory areas, from which we load bytes.
    xor ax, ax
    xor bx, bx
    xor dx, dx
; Only 6 characters
    mov cx, 6
; Load the first argument from the stack
    mov si, [bp+4]

; Load the second argument from the stack
    mov dx, [bp+6]

; Loop for checking bytes equality one-by-one
    loop_check:
; Load the first byte of the original password
        lodsb
; Move it to BX, because the next lodsb will change the AL value
        mov bx, ax
; Save SI - memory address of original password byte, that will be read next time we call lodsb
        push si
; Restore user’s password memory address from DX and read byte to AL
        mov si, dx
        lodsb
; compare AX with BX. CMP will set ZF flag if AX and BX are equal
        cmp bx, ax
; If the bytes are not equal ZF will be 0 and we will end the check
        jz end_check
; Move to the next bytes, save SI for the user’s password to DX, and restore SI value of the original password from the stack. The check will go to compare the next bytes and stop when CX becomes zero (all bytes are checked), or when the unequal bytes were found.
        mov dx, si
        pop si 
        
    loop loop_check
    
    end_check:
    mov sp, bp
    pop bp
    ret  
  
password_str db 'Enter password:', 0
password db 6 dup(0)
original_password db 'OtUs77', 0
db 510-($-$$) dup(0), 0x55, 0xaa
buf:

All right! Now we can:

All we need is the implementation of the original MBR loading and executing. To do that we need to perform the following steps:

org 7c00h
use16

; While users are not passing the correct password we will ask them to input
jmp loop_protect

loop_protect:
    lea ax, [password_str]
    push ax
    call print
    lea di, [password]
    push di
    call read
    
    lea di, [password]
    push di
    lea di, [original_password]
    push di
    call check_password

; Clear the screen
    mov ax, 0
    int 10h
; If check_password doesn’t clear the ZF flag - passwords are equal, we can proceed
    jnz load_mbr

loop loop_protect

; Save our custom MBR and do a different location
load_mbr:
; Clear interrupts
        cli
; Clear the registers and save the original MBR entry point to the stack
        MOV SP, 0x7c00
        XOR AX, AX
        MOV SS, AX
        MOV ES, AX
        MOV DS, AX
        PUSH DX
; From which memory address we will get bytes
        mov si, 0x7c00
; Where we will store bytes
        mov di, 0x0600
; How many bytes to copy (0x200 = 512)
        mov cx, 0x200
; Clear direction flag
        cld
; Repeatedly copy bytes from DI to SI memory addresses while CX is not zero
        rep movsb
; Jump to stage2 - executing of our original MBR (i used constant addresses because FASM calculates offsets from 0x7c00)
        jmp near stage2 - $$ + 0x0600 ; calc right offset to jump

; load the original MBR
stage2:
; Enable interrupts
    sti
; Prepare to read, BX indicates the original MBR location because int 13h will load sectors to ES:BX address
    mov bx, 0x7c00
; Read the first sector from the hard drive by int 13h interrupt
; AH = 02h means “read sector” function
    mov ah, 02h
; Sectors count
    mov al, 1
; HDD number (80h means the first HDD)
    mov dl, 80h
; Track number
    mov ch, 0
; Head number
    mov dh, 0
; Sector number
    mov cl, 1
; Call the interrupt
    int 13h
; Jump to original MBR code, loaded to 0x7c00
    jmp $$ + 0x7600 ; in case of jmp problems
    
print:
; print

read:
; read

check_password:
; check password
  
password_str db 'Enter password:', 0
password db 6 dup(0)
original_password db 'OtUs77', 0
db 510-($-$$) dup(0), 0x55, 0xaa
buf:

Now you can build and execute the code.

Useful materials: