用汇编语言自己写MBR:实现开机前密码验证

重要:本文中所提及的操作会涉及到磁盘主引导记录(MBR)及相邻扇区、磁道的修改,建议您先在虚拟机环境下完成测试,再在实体机进行测试!操作有风险,回车需谨慎!作者不对由于应用本文提及的技术或方法造成的数据等损失负责。

作者:邓昊晴

原载于:www.denghaoqing.com/?p=133

转载请注明以上信息


本文将介绍使用汇编语言编写一个MBR引导程序,以实现在开机进行密码校验并启动的整个设计思路及开发调试过程。其中会涉及到MBR记录、计算机(操作系统)启动过程、磁盘等基本知识。

0x00:准备

什么是MBR?

主引导记录(Master Boot Record,MBR),是计算机磁盘最前面的一段二进制数据,我们常说的主引导记录是指启动磁盘的第0磁道第0磁头的第一个扇区(或第0个逻辑扇区),在这一引导扇区中,包含主引导程序(MBR)和分区表(DPT)在传统操作系统引导中主引导程序是BIOS调用的第一个程序。如今UEFI日益普及,基于EFI引导也成为了另外一种比较主流的引导方式。

主引导扇区的组成

表1.一个经典的主引导扇区的结构

磁盘地址

描述

大小
(bytes)

十六进制

十进制

+000hex

+0

引导程序代码(指令)

446

+1BEhex

+446

分区起始信息 №1

主分区分区表

16

+1CEhex

+462

分区起始信息 №2

16

+1DEhex

+478

分区起始信息 №3

16

+1EEhex

+494

分区起始信息 №4

16

+1FEhex

+510

55hex

MBR 分区签名

2

+1FFhex

+511

AAhex

Total size: 446 + 4×16 + 2

512

表2.现代操作系统常采用的分区表

磁盘地址

描述

大小
(
bytes)

十六进制

十进制

+000hex

+0

MBR引导程序二进制代码 (第一部分)

218

+0DAhex

+218

0000hex

磁盘时间戳(在MS-DOS 7.1–8.0 和 Windows 95B/98/98SE/ME 等操作系统中使用, 在新的引导程序中,这部分也会被当作是一个OEM的签名)

2

+0DChex

+220

Original physical drive (80hexFFhex)

1

+0DDhex

+221

Seconds (0–59)

1

+0DEhex

+222

Minutes (0–59)

1

+0DFhex

+223

Hours (0–23)

1

+0E0hex

+224

引导程序二进制代码 (第二部分,其入口在第一部分)

216
(或222)

+1B8hex

+440

32位磁盘签名

磁盘签名 (在UEFI, Windows NT/2000/Vista/7/8/10 等现代操作系统中使用)

4

+1BChex

+444

0000hex
(
若有保护则为5A5Ahex )

2

+1BEhex

+446

分区起始信息 №1

主分区分区表

16

+1CEhex

+462

分区起始信息 №2

16

+1DEhex

+478

分区起始信息№3

16

+1EEhex

+494

分区起始信息№4

16

+1FEhex

+510

55hex

引导扇区签名

2

+1FFhex

+511

AAhex

Total size: 218 + 6 + 216 + 6 + 4×16 + 2

512

注:以上两个表格摘录自维基百科,笔者自行翻译

在如今, 表二所提及的引导扇区结构更为常用,因为磁盘签名(0x1B8,0x1BC)被大多数操作系统所用,因此,本文主要是基于表二的结构构建MBR记录。但对其进行了一些修改,删除了磁盘时间戳那一部分,变为如下样式:

表3.本文所采用的引导扇区结构

磁盘地址

描述

大小
(
bytes)

十六进制

十进制

+000hex

+0

MBR引导程序二进制代码

440

+1B8hex

+440

32位磁盘签名

磁盘签名 

4

+1BChex

+444

0000hex
(若有保护则为
5A5Ahex )

2

+1BEhex

+446

分区起始信息 №1

主分区分区表

16

+1CEhex

+462

分区起始信息 №2

16

+1DEhex

+478

分区起始信息№3

16

+1EEhex

+494

分区起始信息№4

16

+1FEhex

+510

55hex

引导扇区签名

2

+1FFhex

+511

AAhex

Total size: 440 + 6 + 4×16 + 2

512

在所有的主引导扇区的结尾,都是以十六进制的55 AA结束,这相当是一个对MBR记录的签名,标记该引导扇区有效。

程序在计算机中的执行

所有的计算机程序,在计算机中的执行过程都是类似的,我们首先将程序指令加载到内存的指定位置上,然后通过指令让CPU从该地址开始逐条逐条地执行指令。一个程序要执行,肯定就需要上一个程序去帮他执行上述的操作(加载到内存,让CPU从所在地址开始执行)。

鸡生蛋,蛋生鸡?

正如前面所说,计算机要执行一个程序,肯定需要有个东西去调用这个程序,那么,计算机运行的第一个程序,是如何调用的呢?

计算机的启动过程

计算机在通电后,首先会有一套预置的指令去自动读取加载BIOS固件,启动时BIOS对计算机进行自检等启动前的工作,在处理完成后,BIOS会读检查启动磁盘的引导扇区,检查其最后两byte是否为55 AA,以判定其是否为有效的引导扇区,若该扇区为有效的引导扇区,BIOS会将该扇区数据加载到内存的0x7C00地址处,并让CPU从内存的0x7C00处开始执行相关指令,此时,计算机的控制权从BIOS交接到了MBR,也就是主引导记录了,其过程如下图所示。

此时,MBR程序是运行在实模式下的。在实模式下,计算机是以16位进行工作的,且其寻址空间有限,而目前几乎所有的操作系统都是运行在保护模式中,因此在启动过程中,引导程序还会执行很多的指令,让计算机从实模式切换到保护模式,读取分区中的操作系统引导器,最终才进入到操作系统。这些指令在如今往往一个MBR扇区装不下,因此一个完整的引导程序往往需要占用多个扇区。下图为以windows为例各级引导程序的分布及其运行的环境:

当然,不同的操作系统/引导器的在硬盘中占用的扇区,运行的流程也是有差异的,下图是Grub引导器在硬盘中扇区占用的情况:

汇编与汇编器

本文所涉及到的程序通过x86处理器的汇编语言完成,使用的汇编器是NASM汇编器,NASM全程The Netwide Assembler,是一个为可移植性与模块化的一个80×86汇编器,其语法和Intel语法相似,它可以生成多种目标文件格式,最重要是它还可以输出二进制文件。

0x01: 大概要怎么干?

回到本文讨论的主要内容,正如题目所说的“自己做个MBR”,要让计算机开机执行我们自己写的密码输入程序,就需要修改主引导记录,然而认证通过后我们又需要原本的引导程序得以加载,那么我们就需要想一个办法去调用原本的主引导程序了。

我们需要做的程序的运作过程也大致清晰了:

  1. BIOS加载我们的引导程序
  2. 程序提示用户输入密码
  3. 密码正确,加载并调用原本的引导程序
  4. 原本的主引导程序完成后续的操作系统引导工作

下图则是基于上述过程的一个密码验证程序的工作示意图。

在验证密码通过后,通过调用磁盘读取真正的引导扇区到内存,并跳到该地址继续执行,把控制权交给真正的MBR分区。

0x02. 实际开发

在引导扇区的程序都是较为低级的计算机程序,通常使用汇编语言进行开发,而我们需要重点解决的,则是密码的输入、密码的校验、磁盘的读取、内存的写入,以及交互界面这五方面的开发。其中,涉及到IO的部分我们都是调用BIOS的中断来实现的。所谓中断,就是CPU暂停执行本程序,并转到执行处理一些内部或外部事物的过程。

我们在代码最后面写入了一些程序运行时需要使用的提示字符串,密码的资料,以供后续使用:

1
2
3
4
5
6
7
8
9
10
11
SysName:
db "PC Secure System --by:Haoqing Deng"
LenName equ ($-SysName)
Note2:
db "Please input your password to load the MBR program."
LenNote2 equ ($-Note2)
Note3:
db "Password:"
LenNote3 equ ($-Note3)
Password:
db 'passwd',0

密码的输入:

调用0x16中断(键盘服务),从键盘上读取输入用户输入的字符,置AX为0(传入参数,并作为返回值,用户键入的字符储存在AL处,即AX的低八位),在开始读取操作前BX为0,读入字符串可以写成如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
getInput:
xor ax,ax
int 0x16
cmp al,0x8     ;匹配是否为退格键
je backspace
cmp al,0x0d      ;匹配是否为回车键
je verification
mov [bx],al      ;写入到段储存器中
add bx,1
inc cx
mov ah,0eh
mov al,'*'
int 10h    ;此处调用0x10中断显示个星号作为回显
jmp getInput

密码的校验

密码的校验也是比较简单的,只需要逐个字符对输入密码和设定密码比对即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
verification:
mov ax,cs
mov es,ax
xor bx,bx
mov si,Password
compare:
mov al,[ds:bx]     ;从段储存器中获取一个字符
mov ah,[es:si]     ;从附加段寄存器获取正确字符
cmp ah,0     ;若密码达到了\0空字符,则判断为密码校验完毕,跳到后续不走
je After
cmp al,ah
jne Error
dec cx
add bx,2
inc si
jmp compare
After:
cmp cx,0      ;但如果此时,记录键盘输入的字串长度的寄存器不为0,则判断为非法密码
jne Error

jmp Success

其中,上述跳到的Error执行的是重启的指令,让计算机重启

从硬盘读取扇区到内存

在校验完毕后,我们需要从硬盘上读取原MBR扇区的数据到内存制定地址,并通过jmp指令跨段跳转到原来的MBR程序,将控制权交出。我们通过对GRUB2引导程序的安装过程进行分析(grub-install /dev/nvme0n1 -v),发现在本人计算机中,GRUB2实际上占用了102个扇区,因此我打算在第103个扇区设立旧的MBR备份,并在自己的引导程序中调用。其中0x13是磁盘操作的中断,在本案例中,我们调用了其读磁盘扇区到内存的功能,其调用方法如下

1
2
3
4
5
6
7
8
mov bx,OffSetOfMBR   ;欲写入的内存地址
mov ah,2 ;读扇区功能号
mov al,1 ;读扇区数量
mov dl,80h ;驱动器号,因为是HDD设备,故为80h,即0x80
mov dh,1 ;磁头号
mov ch,0 ;磁道号
mov cl,40 ;扇区号
int 13h

其中,OffSetOfMBR是该扇区加载到内存的地址,我们设置为0x8C00,在加载完后,只需要跳转到该地址,即可将控制权交给真正的引导程序了。

1
jmp OffSetOfMBR

磁盘的磁道,磁头,扇区与相对扇区(逻辑扇区)的关系

传统磁盘是由盘片和磁头组成的,一个盘片有两面,因此一个盘片需要有两个磁头,磁盘的盘片每个同心圆是一个磁道,每个磁道又有一定数量的扇区。虽然在目前,固态硬盘日益推广,但在固态硬盘上,为了向下兼容,磁头磁盘磁道等参数依然被使用。在前面提到的“第103个扇区” 严谨地说,应该是第102个逻辑扇区(从0开始数起)然而参数中我们需要输入的确是磁头,磁道,扇区的信息,通过软件得知我的磁盘每磁道有63个扇区。也许是为了能够减少磁盘磁头臂的移动,在0磁道,0磁头,63扇区后的下一个扇区,为0磁道,1磁头,1扇区,即盘片的另外一面。因此,文中的第103个扇区,应该标示为,0磁道,1磁头,40扇区(因为103/63=1……40)。下图为一个硬盘盘片的结构,其中A(红色圆圈)为一个磁道,B(蓝色)是一个几何上的扇区(扇形),C(红蓝相交)为磁道上的一个扇区,而D则是磁盘的一个簇,一个簇由多个磁盘扇区组成,这里不进行深入讨论。

用0x00填充剩余区域

由于MBR扇区是512扇区,引导程序占用的是440byte,而上述程序很显然并没有占用这么多空间,但我们还是需要将其填充满440byte,以便与分区表结合形成一个有效的MBR记录。
因此,最后的这行实质上是通过伪指令db来把剩余的所有区域填充完成的。
1
times 440-($-$$) db 0
在代码写完后,执行“nasm 文件名”即可汇编出二进制文件,一个密码校验的引导程序就完成了,但是文章还没完,因为上述的做法和思路,在一些引导器上的确能工作,但也有不少引导器上出现了问题:输入密码后无法正常引导。于是我对出现问题的原引导记录进行了反汇编,返汇编我们使用的是nasm自带的返汇编程序:
首先我们提取原引导记录:
# dd if=/dev/your_Device_Name of=./mbr bs=512 count=1
# ndisasm mbr

此时,终端输出了相应的汇编码,我们这里也节选了一部分(去掉一些无用代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
00000000  EB63              jmp short 0x65
00000002  90                nop
0000005B  800100            add byte [bx+di],0x0
0000005E  0000              add [bx+si],al
00000060  0000              add [bx+si],al
00000062  0000              add [bx+si],al
00000064  FF                db 0xff
00000065  FA                cli
00000066  90                nop
00000067  90                nop
00000068  F6C280            test dl,0x80
0000006B  7405              jz 0x72
0000006D  F6C270            test dl,0x70
00000070  7402              jz 0x74
00000072  B280              mov dl,0x80
00000074  EA797C0000        jmp word 0x0:0x7c79
00000079  31C0              xor ax,ax
0000007B  8ED8              mov ds,ax
0000007D  8ED0              mov ss,ax
0000007F  BC0020            mov sp,0x2000

可见,原MBR记录(00000074)进行了段间转移,其目标地址为0x7c79,即MBR程序原本应该存放的地址,而这段内存却被我们的程序所使用,因此而导致了不兼容的情况。因此我们需要对我们的理论进行修改,其流程变成下图:

而其中的引导扇区加载器,只是一段简单的代码,将后面扇区的数据加载回0x7C00,至此,这个小程序已经完成.

0x03. 部署与测试

测试建议先在虚拟机、可启动U盘等设备上进行测试,测试通过后在尝试在实体机器上进行。以下操作我们均在Linux下进行,你也可以在Windows等系统下,利用类似命令来进行操作。但所有的操作需要小心!做好备份等准备,谨防误操作导致数据丢失!

注意:下面的/dev/nvme0n1请修改为你的启动磁盘,不要照搬。

第一步、备份要修改的扇区的数据,在本案例中,笔者增加了第103,104两个扇区

,同时修改了第一个扇区,因此笔者备份了从第一到第104共104个扇区:
$ sudo dd if=/dev/nvme0n1 of=./sector_backup bs=512 skip=0 count=104
第二步、提取MBR记录,分区表记录、和中间所夹的101个扇区
MBR记录:
sudo dd if=/dev/nvme0n1 of=./mbr_0 bs=512 skip=0 count=1
分区表记录:
dd if=./mbr_0 of=./ptable  bs=1 skip=440
中间所夹的扇区(剩余引导程序):
sudo dd if=/dev/nvme0n1 of=./mbr_rest bs=512 skip=1 count=101
用cat命令合并上述文件,形成新的扇区记录:
cat mbr_encrypt ptable mbr_rest mbr_decrypt mbr_0>product.mbr
写入磁盘:
sudo dd if=product.mbr of=/dev/nvme0n1 bs=512
重启,即可测试其效果了,其运行效果如图:

0x04. 后记

安全性的讨论

至于设立这样的一个密码输入校验机制,在安全性上能够增加多少呢?如果仅仅如上机制,安全性的确有所增加,但增加幅度有限。因此,我也做了一个在首次开机会清除分区表的程序,在密码输入正确之前,分区表不会被恢复,在这种情况下,其对数据的保护是有一定的作用的。但是,如果对于有技术的人来说,破解也不会是难事。搞这些东西,也许更多的是防君子防小人,但防不了懂技术的小人了。因此,请不要把安全性依赖于这几十行代码中。

代码下载地址:https://github.com/sunnyden/MBRLock


若您觉得这些内容对您有帮助,希望您能为我提供捐助,以便让我们更好地运营下去。

Bitcoins donations ar accepted

3MNvM1gW2sLPm3HTcM2Zp4GdaHDHeFkjMh

发表评论

电子邮件地址不会被公开。 必填项已用*标注