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.
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 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:
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.
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?
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: