Author: 堇姬Naup
kernel位於application和hardware之間,是OS的core也負責溝通hadware和process
負責了像是
I/O、process、memory、driver managemet或是syscall 等事情
kernel也提供了可以跑application的環境
application是run在ring3,而kernel則是ring0
target有像是driver或是kernel
driver:
kernel:
將它拆分成更細
HOST is windows 11 and user vmware + ubuntu 22.04 write exploit
Debug kernel需要兩台windows
先進VM
首先可以先下bcdedit,如果上面沒有debug代表沒開
去找到msconfig,選boot > advanced option > debug打開
現在你去下bcdedit就會有debug: on了,另外如果你確認時跳出boot secure問題,就要進去boot把它關掉
有兩種debug方式,serial port跟net,這邊先用net方式debug
之後下這個command
bcdedit /DBGSETTINGS NET HOSTIP:IP PORT:50000 KEY:w.x.y.z
KEY是為了防止不要讓任何人都可以debug你的kernel
設好就重啟VM
現在回到你的HOST,用windbg的attack kernel,attach上去
如果你有看到attach kernel 代表成功了,不過有時候會失敗
所以我們試試看serial port
之後就到attach kernel 的COM把資訊填上去就行
反正我們已經attach好了
如果卡住按一下break就可以了
附註: 以下的offset可能不同,實際情況用windbg直接追就可以了
甚麼是process應該不用解釋,當create process後會去call win32 API,
之後進到kernel層創建一個PCB(process control block),記錄整個process的資訊
PCB不太完整,準確來說 struct是EPROCESS
PCB裡面長這樣
至於cr3、PML4是甚麼在Virtual address to Physical address提到
createThread時會創建
https://learn.microsoft.com/zh-tw/windows/win32/api/processthreadsapi/nf-processthreadsapi-createthread
Thread間用linklist串在一起
跟process一樣創建一個ETHREAD,裡面的TCB(Thread control block),負責管理整個thread
ETHREAD struct
TCB
將 Object、Process等進行分級,共用六個等級,低level不能存取高level
untrusted, low, medium, high, system、Protected
我們正常的程式都是跑在medium
Level | Description |
---|---|
Untrusted | Started by Anonymous group. Block most write access. |
Low | Used by AppContainer. Block most write access to most object (files and registry) on system. |
Medium | Used by normal application if UAC is enabled. |
High | Used by administrative application when UAC is enabled. |
System | Used by system services or system process. |
Protected | Currently unused by default. |
ACL:
https://learn.microsoft.com/zh-tw/windows/win32/secauthz/access-control-lists
ACE:
https://learn.microsoft.com/zh-tw/windows/win32/secauthz/access-control-entries
是存取控制項目ACE (ACL 可以有零個或多個 ACE。 每個 ACE 都會由指定的信任者控制或監視物件的存取)的清單
安全性實體物件的安全性描述項可以包含兩種類型的ACL:DACL和SACL
https://0xfocu5.github.io/posts/37e301d0/
https://0xfocu5.github.io/posts/37e301d0/
user登入windows時會拿到一組access token代表當前登入者
用來表示process security context
Windows會透過Token來判斷該物件是否可被存取及能做哪些操作
_TOKEN stuct
priviledge (_SEP_TOKEN_PRIVILEDGE
https://connormcgarr.github.io/x64-Kernel-Shellcode-Revisited-and-SMEP-Bypass/
當thread與process對secure Object進行互動時使用
用於描述與process的secure context
https://rootclay.gitbook.io/windows-access-control/access-token
負責控制及確定CPU模式、特性及行為,可以參考該docs的2.5(p.3069)
docs
Control registers (CR0, CR1, CR2, CR3, and CR4; see Figure 2-7) determine operating mode of the processor and the characteristics of the currently executing task.
每個bits 詳細功能也請直接參考該docs,其中最容易遇到的應該是CR4 20 21位的SMEP SMAP
我們在process看到的記憶體是一串Virtual address,他透過映射的方式映射到Physical address(在Access的當下,CPU去將Virtual address轉成Physical address)
以下是Virtual Memory layout
另外透過Virtual Address可實現Isolation Process,只要看你給的address有沒有在Virtual address裡面就可以,頂多影響到自己process擁有的Physicsl address
也解決了跳轉問題,程式只需要跳轉到Virtual address,硬體會幫你映射到對應的Physical address
你就不需要對Physical address直接進行操作了
你可以想像是Memory的最小單位(若以segment作為memory單位太大了)
通常一page是4KB(0x1000),不過具體大小由CPU決定
詳細可以參考這個
https://wiki.osdev.org/Paging
儲存虛擬位址到實體位址的對映
每個表上的對應被稱作PTE(page table entry)
每個PTE長這樣
PFN存的就是offset(一個頁表0x1000,用3byte剛好表示完)
Name of bit | Meaning |
---|---|
Nx | Non - execute |
PFN | Page Frame Number |
Ws | Write bit (software) |
Cw | Copy on write |
Gl | Global |
L | Large Page |
D | Dirty |
A | Accessed |
Cd | Cache disable |
U/S | User mode/supervisor bit |
W | Write bit (hardware) |
V | Valid |
當真正有去摸到那塊physical address
才會分配page給他
實際上R/W/X的時候才會分配physical address及建立PTE(但大多數API在alloc就會做讀寫了)
訪問的Virtual address在Physical Address未被載入時會觸發此錯誤
把很久沒用到的page swap到disk
假設要轉換這個0xffffffff8111c398
0b1111111111111111 | 111111111 | 111111110 | 000001000 | 100011100 | 001110011000
分別對應
第一段跟上述所說的一樣,AMD64只實現48bits,為規範address
接下來分別對應
PML4I 0b111111111 511
PDPI 0b111111110 510
PDI 0b000001000 8
PTI 0b100011100 284
Offset 0b001110011000 920
首先從 CR3 爬出 Page-Map Level-4 Table Base
第 PML4 個 Entry 紀載下一級頁表 PDP 的 Base
第 PDPT 個 Entry 紀載下一級頁表 PD 的 Base
第 PD 個 Entry 紀載下一級頁表 PT 的 Base
第 PT 個 Entry 紀載 Physical Page Frame Base
Physical Page Frame Base + Physical Page Offset 就完成了轉換
更詳細去拆解四個page table可以參考這篇
https://hackmd.io/@LJP/rkxtGgoIO
如何手算可以參考Physical Address
https://www.coresecurity.com/core-labs/articles/getting-physical-extreme-abuse-of-intel-based-paging-systems-part-2-windows
PS: 我要強調,這裡是從page table查到對應的page frame(等於找到page frame base),在該page上通過offset去找實體記憶體位置
首先一樣先attach上去kernel
我們來轉換nt好了
先對windbg下dp nt
來轉換 fffff801`81c00040
首先是可以用vtop
用法是
!vtop <PEB base> <Virtual Address>
用 !dp <address>
可以查看physical address上的值
將轉換出來的physical address查看一下發現值一樣,成功
不過當然這邊也手算一次,我們改用cmd.exe
先切換到cmd.exe process
.process /r /p ffffac8d897a8080
現在下dp ntdll就可以看到cmd.exe的ntdll
通過ntdll去找PTE
試著轉換00007ffae2680000
我寫了腳本
轉換結果是
PML4_offset: 0xff
PDPT_offset: 0x1eb
PD_offset: 0x113
PT_offset: 0x80
PhysicalAddress_offset: 0x0
PML4就是這個 DirBase: 172636000
(也是CR3存的值)
把他加上offset * 8後會找到該表上面的值
這就是第二層(PFN 0xa00000172642867)
記得去掉低12bits跟高1bits,剩下的PFN(0x172642000)
以此類推第二層
第三層(0x00172545000)
第四層(0x00225b46000)
找到 page frame base (0x00010010d000)
最後加上offset 0x0就是physical address了(看到資料一樣代表成功了)
用vtop看的話也確認是正確的
讓程式與device溝通的一個橋樑,通常可以利用syscall溝通
目前主流架構是Windows driver model
一個WDM載入device方式
怎麼寫code可以參考
https://ithelp.ithome.com.tw/articles/10322132
另外在逆向時MajorFunction可能只是index,可以參考這個
那windows driver掛上去了,互動的流程是甚麼
如果要試著寫windows driver可以參考
https://www.alex-ionescu.com/
https://www.apriorit.com/dev-blog/791-driver-windows-driver-model
Debug windows driver
https://ithelp.ithome.com.tw/m/articles/10326802
I/O request packet
https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_irp
Windows 中用於請求 I/O 操作的資料結構。驅動程式可以使用 IRP 來與 Kernel 溝通,例如讀取文件或進行網路傳輸。開發者也可以透過發 IRP 給 I/O Manager,並將 IRP 轉發給相應的驅動程式,從而執行對應的行為
IRP struct如下
由於你的driver沒有簽章驗證,所以你必須開啟testing mode及關閉簽章驗證來掛上去你的driver
bcdedit /set testsigning on
bcdedit /set loadoptions DDISABLE_INTEGRITY_CHECKS
bcdedit /set nointegritychecks on
之後重啟應該會看到testing
接下來把driver掛起來
sc create <your driver name> binPath=<Path of driver on windows> type=kernel
sc start <your driver name>
sc delete <your driver name>
如何掛起kernelmode windows driver可以參考
https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/debug-universal-drivers–-step-by-step-lab–echo-kernel-mode-
他會去與\\.\BreathofShadow
進行交互
https://learn.microsoft.com/zh-tw/windows/win32/api/fileapi/nf-fileapi-createfilew
https://learn.microsoft.com/zh-tw/windows/win32/api/ioapiset/nf-ioapiset-deviceiocontrol
PS: \.\\
是device namespace
https://superuser.com/questions/1583205/what-kind-of-file-path-starts-with
https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file
每次開機時隨機化kernel address,只要不重新開機就不會變動
kernel mode不能執行userspace的code
是否開啟位於cr4 bit 20
kernel mode不能直接存取userspace data
是否開啟位於cr4 bit 21
在linux kernel把它叫做KPTI
kernel/Userland Page Table Isolation
在kernel mode時所有user space的PML4 NX bit會開起來,導致你就算disable SMEP也不能執行user space code
https://mdanilor.github.io/posts/hevd-2/
就是stack canary的概念
https://breaking-bits.gitbook.io/breaking-bits/exploit-development/linux-kernel-exploit-development/stack-cookies
如果process的integrity是medium
通過調用NtQuerySystemInformation可以獲得所有sys或nt address,及handle對應Object位置
https://learn.microsoft.com/zh-tw/windows/win32/api/winternl/nf-winternl-ntquerysysteminformation
核心概念是透過stack overflow,來Return 到userspace上的shellcode上,為何要跳回userspace原因是要在沒有任意讀任意寫情況下在kernel space 放shellcode並執行有難度,所以在userspace給一塊rwx個memory,並把shellcode寫進去跳進去
不過首先會遇到一個問題 SMAP/SMEP
不過在有stack overflow可以堆ROP時繞過他蠻簡單的,透過將cr4的第20bit 21bit蓋成0就可以了
pop rcx; ret
mov cr4, rcx ; ret
接下來透過shellcode來抓出token(高權限),並寫掉自己的token來提權
https://github.com/scwuaptx/CTF/tree/master/2019-writeup/hitcon/breathofshadow
首先他給了一個windows drive,來分析看看
reverse windows driver 我有看到一篇不錯的文章可以參考
https://v1k1ngfr.github.io/winkernel-reverse-ida-ghidra/
這裡他建立了一個Device(IoCreateDevice)
L"\\Device\\BreathofShadow"
是deivce name
然後去設定MajorFunction,處理IRP
第一個createdeleteHandle沒做甚麼就直接回傳IofCompleteRequest
這邊重要的是
有些symbol爛了自己修一下
這邊補充一下IoStackLocation
https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_io_stack_location
詳細IoStackLocation可以參考這個
https://v1k1ngfr.github.io/pimp-my-pid/
如果IoControlCode是0x9C40240B,就Call進去
一樣是symbol全爛,自己修一下
NtDeviceIoControlFile:
我們互動的東西會存在IoStackLocation的inputbuffer(可控)
他會將InputBuffer丟進去Dst(一個有限大小的buffer)
這裡就有一個buffer overflow
另外我們還需要information leak
原因是因為cookie(像是stack canary)、XorKey、KernelBase(ROP need)需要leak出來
這邊其實information leak洞非常明顯
如果inputbuffer塞超小
outputbuffer塞超大
DST值會只有少部分被xor
而DST被copy到inputbuffer(根據output buffer)
這樣就可以任意讀stack上的value(也就有information leak)
IoControl互動方式就跟上述一樣
很顯然ret2usr
條件已經湊齊了
接著來看EoP
https://www.matteomalvica.com/blog/2019/07/06/windows-kernel-shellcode/#token-stealing
目標是取得system integrity 的 process token,並寫進自己的process token
當我們有任意執行shellcode時,透過去traverse整個EPROCESS(linklist),並找到system process,然後換掉自己的token成system process,來達到EoP
要拿token代表要拿到system EPROCESS
代表要找到一個EPORCESS透過ActiveProcessLinks尋找
要如何找到呢?
有一塊struct叫做KPCR(Kernel Process Control Region)
在Windows 64 bits時gs register恆指向該struct
裡面有一塊叫做PRCB(Kernel Process Control Block)
PRCB裡有Current Thread(KTHREAD)
ETHREAD裡面有TCB,TCB有Process指向EPROCESS
所以就可以透過gs拿到EPROCESS了
遍歷EPROCESS時可以查看是否PID是4,因為這支就是system
那以上就是EoP的方法
來整理一下攻擊流程
PS: 偷偷爆個雷,當你get shell後會發現該process砍不掉,原因是因為沒有IoCompleteRequest導致driver一直在等待,所以最後記得回傳,這樣可以讓exploit更穩定
順便補充gs register、KPCR是啥
gs register主要用於存取與當前 CPU 或Thread相關的數據結構
KPCR 的主要目的是為每個CPU維護專屬的處理器控制數據,並在多CPU系統中進行高效管理。KPCR 位於kernel space,提供一些關鍵資訊供內核、驅動程式以及低層次的系統組件使用
KPCR represents the Kernel Processor Control Region. The KPCR contains per-CPU information which is shared by the kernel and the HAL. There are as many KPCR in the system as there are CPUs.
參考上面的如何attach kernel 跟 install kernel
總之就先開啟testing mode
之後掛上去kernel driver
這邊再用windbg時候沒有顯示我們install上去的kernel driver
我就試著加載了一下symbol就看到了
真的得先熟悉windows 32 API怎麼用QQ
寫腳本寫的好卡
先來寫跟 driver 互動的腳本,並觀察他做了啥
下個 breakpoint 在 ioctl handler 內
下在 memcpy 上
先跑這份
理論上第一個 memcpy 時
memcpy(Dst, InputBuffer, (unsigned int)InputBufferLength);
register | Value |
---|---|
r8d | InputBufferLength |
rdx | InputBuffer |
rcx | Dst |
通過 windbg 觀察可以發現卡在第一個斷點
查看 register 值,可以發現 inputbuffer 有 0x81
繼續 g 一下會卡在第二個斷點,也就是第二個 memcpy
memcpy(InputBuffer, Dst, OutputBufferLength);
register | Value |
---|---|
r8d | OutputBufferLength |
rdx | Dst |
rcx | InputBuffer |
看 windbg
現在 rdx 是 Dst,也就是被 xor 過的值
那總之第一步先來 xor key
先上腳本,這樣就可以 leakkey 了,先把一串 0x81 丟進去,他會丟出 xor 結果,xor 回來就可以了
成功畫面
接下來要 leak kernel base 來用上面的 gadget,以及要打 Buffer overflow 蓋 ret address 需要 leak stack cookie,在 overflow 時蓋回去才不會 crash,這邊可以用上述提到的 information leak 洞
先看 kernel base 在哪
從我們印出的位置開始看,可以看到 ffff8104 0d392648
上有 nt kernel base (stack_ptr + 0x108)
扣掉 offset 0xb7b3c9
就是 kernel base 了
接下來來看 stack cookie
stack cookie 會在 return address 附近,不過其實不用理他,因為到時候把整個 stack leak 下來,順便蓋回去就好了
不過還是要先找 return address 位置
這邊可以用 backtrace
https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/calls-window
找到 return address 在 ffff8104 0d6ee668
Dst 開始的位置在 ffff8104 0d6ee540
0xffff8104 0d6ee668 - 0xffff8104 0d6ee540 = 0x128
這樣就可以控制 rip 到 0xaabbccdd 了
接下來來看 KROP 怎麼堆
第一步要做關掉 SMAP/SMEP
一樣觀察 cr4
把他蓋成 0b 00 0101 0000 1110 1111 0000
先找兩個 gadget,可以控 cr4 的跟可以控 mov cr4, reg,pop reg 之類的 gadget
到這找 gadget C:\Windows\System32\ntoskrnl.exe
將 cr4 設為 0x50ef0
如果沒清 cr4 會直接 BSOD
windbg 卡住也可以看到噴錯
下 !analyze -v
可以看到詳細資訊
接下來跳回 userspace 上的 shellcode,開始提權
先 VirtualAlloc 一塊 rwx 的區段,把 shellcode 寫上去
接下來 return address 部分寫 shellcode address
因為 SMAP/SMEP 已經被寫掉了,所以可以跳到 userspace shellcode 上
來寫 shellcode
https://defuse.ca/online-x86-assembler.htm#disassembly
首先先上張圖
Use gs find structure _KPCR -> _ETHREAD -> EPROCESS
Loop traverse EPROCESS linklist and find pid 4's process. It is system integrity process's token
Change yourself process token and return (You must recover register about protection, because kernel will check.)
我們需要找出 system 這支 process 的 EPROCESS 裡面的 token,寫到自己 process 的 token
要從 gs 找 _KPCR struct
觀察 _KPCR
_PRCB 內 +0x180 是 _KPRCB
_KPRCB + 0x8 是 CurrentThread (_KTHREAD)
所以可以從 gs:[0x188] 找到 _KTHREAD
_KTHREAD + 0x98 是 _KAPC_STATE
_KAPC_STATE + 0x20 是 _KPROCESS
所以 _KTHREAD + 0xb8 是 _KPROCESS 也就是 _EPROCESS 內的 PCB
最後是要找 EPROCESS 內的 ActiveProcessLinks 來遍歷整個 EPROCESS,找出 system 的 token
他在 _EPROCESS + 0x1d8
所以目前 shellcode 先這樣寫
已經可以清楚的看到 rcx 內拿到了 Flink 了
接下來來寫循環查找,拿 token 跟寫 token
UniqueProcessId 位於 Flink-0x8
通過 rcx - 0x8 就可以拿到 UID
token 部分則在 _EPORCESS + 0x248
接下來就是循環去找了,直接上,最後用 loop 來去卡著 shell
最終 shellcode 在這
整個操作的結構圖
最終可以在 windows 上通過執行 exploit 拿到 ntos 權限來 EoP