PatchGuard Analysis - Part 4

PatchGuard Analysis - Part 4

Ağustos 6, 2025·0xbekoo
0xbekoo

Verification Routines

Artık PatchGuard’ın kalbine geldik. PatchGuard artık bir timer, bir DPC, system thread ya da diğer mekanizmalar aracılığıyla tetiklendikten sonra kendisi bir takım doğrulama işlemlerine başlıyor. Doğrulama kodları PatchGuard durumunu ve kernel yapılarını decryptleyip ardından incelemeye başlar.

Esasen doğrulama rutini olarak birkaç rutin var ancak bunlardan en sıka kullanılan rutin ise daha öncede bahsedilen FsRtlMdlReadCompleteDevEx fonksiyonu olacaktır. Bu arkadaş ntoskrnl.exe’nin en uzun rutini olabilir. Diğer rutinler ise şunlar:

  • TVCallBack Routine: Bu rutin FsRtlMdlReadCompleteDevEx’e baya benzemekte. Bu fonksiyonu KiFilterFiberContext’ten tekrar hatırlayalım.

  • CcBcbProfiler: Bu fonksiyonun daha önceden görmüştük. Hatırlayalım fonksiyon ntoskrnl.exe’den random olarak seçtiği rıutinleri kontrol ediyordu.

Bu başlıkta ana odağımız FsRtlMdlReadCompleteDevEx fonksiyonu olacaktır. Dediğim gibi bu fonksiyon PatchGuard’ın birçok doğrulamasını üstleniyor ve fonksiyon gerçekten çok uzun, ayrıca analiz etmesi bir o kadar uğraş verici ancak düzenli olması açısından bu bölümü üç başlığa böleceğiz:

  1. Prologue
  2. Check of Structures
  3. Epilogue

Prologue

Verifying PatchGuard Context Integrity (Part 1, 2, 3)

Bu aşamada, PatchGuard Context’i artık decryptlenmiş ve ve bellekte plaintext halde olacaktır. İlk adım ise PatchGuard Context’in üç kısmın checksum’ların hesaplanmasını ve ardından KiInitPatchGuardContext tarafından initialization esnasında oluşturulmuş değerler tarafından karşılaştırılmasını içerir.

Ancak bu doğrulama gerçekleşmeden önce, Context’in checksum veya geçici WorkItem yapıları gibi bazı volatile alanlar, geçici olarak bellek bölgesinden kaldırılır veya yığına taşınır. Bu değerler, checksum doğrulamaları tamamlandıktan hemen sonra geri yüklenecektir (0x140BB308F):

Stack ve r13 register’ı fonksiyonun başında ayarlanır:

FsRtlMdlReadCompleteDevEx:
...
lea rbp, [rsp-898h]
sub rsp, 998h
mov r13, rcx ; Pass the Context to r13

Bu adım, PatchGuard Context’in hiçbir parçasının başlatma işleminden bu yana değiştirilmemesini sağlar ve etkili bir şekilde kendi kendini kontrol eden bir bütünlük mekanizması görevi görecektir.

Re-Encrypting the Primary Context Region (Part 1)

Doğrulama tamamlandıktan sonra PatchGuard artık Context’in ilk kısmını tekrar encryptleyerek devam eder. Bu bölüm meta veriler içerir ve asla plaintext biçiminde kalıcı olması hedeflenmez.

İlginç bir şekilde PatchGuard kritik bilgiler içermesine rağmen Part 2 ve 3 bölümlerini hemen yeniden şifrelemiyor. Bu kısmi yeniden şifreleme stratejisinin arkasındaki kesin nedeni belirsizdir; Bu bir performans optimizasyonu olabilir.

Checksum Verification of Parts 2 and 3

Artık bu kısımda Context’in Part 1’i tekrar şifrelendi. Artık bu noktada PatchGuard Part 2 ve 3’ün doğrulama işlemlerine odaklanacak. Context’in bu kısımların neler tuttuklarını tekrar hatırlayalım:

  • ntoskrnl rutinlerin kopyaları
  • Kritik kernel yapılarını ve bunların önceden hesaplanmış checksum’ları.

Bu kısımlar, sonraki doğrulamalar sırasında doğrudan erişime izin vermek için bu aşamada şifrelenmemiş olarak kalacaktır.

Timed Delay via Sleep or Wait Routine

Sık kontrolleri önlemek ve belirli zamanlama tabanlı saldırıları engellemek için PatchGuard doğrulama döngüleri arasında zorunlu bir bekleme süresi içeriyor. Bu bekleme süreleri önceleri çok defa gördüğümüz 2 dakika ile 2 dakika 10 saniye arasında değişiyor ve üç yöntemden biri kullanılarak uygulanabilir:

  • SelfEncryptWaitAndDecrypt (sub_140510430): Context’i şifreler ve KeDelayExecutionThread’i çağırır, ardından bekleme süresi dolduktan sonra Context’i geri çözer.

SelfEncryptWaitAndDecrypt’in (fonksiyonun literatürdeki adı) bir örneğini 0x140BC274E adresinde görebiliriz:

Ekstra olarak, bu fonksiyon parametre olarak KeDelayExecutionThread adresini de almakta. Bu adresler yani hem SelfEncryptWaitAndDecrypt hem de KeDelayExecutionThread 0x140BC252F adresinde hazırlanıyor. Ayrıca timer da aynı adreste hazırlanır:

loc_140BC252F:
mov rax, [rsi+2C8h] ; Get the address of KeWaitForSingleObject
mov r9, [rsi+170h]  ; Get the address of KeDelayExecutionThread
mov [rbp+8D0h+var_8D8], rax
mov rax, [rsi+340h] ; Get the address of SelfEncryptWaitAndDecrypt
mov [rbp+8D0h+var_8F0], rax
mov [rbp+8D0h+var_950], r9

[...]

mov [rbp+8D0h+var_850], rcx ; Pass the timer

SelfEncryptWaitAndDecrypt’te içine baktığımızda ilk olarak context’i şifreler ve 0x140510567’de direkt olarak KeDelayExecutionThread’i çağırır:

KeDelayExecutionThread’in çalıştırılmasından sonra, 0x1405105AB adresindeki context’i decryptler ve fonksiyondan çıkar:


  1. KeWaitForSingleObject: Bir kernel events veya timer object’i bekler; eğer zaten sinyal gönderilmişse hemen geri döner.

KeWaitForSingleObject’in bir örneği, SelfEncryptWaitAndDecrypt’in çağrıldığı bölümün hemen yakınlarında 0x140BC278A adresinde bulunabilir:


loc_140BC2770:
xor     edx, edx
test    r11, r11
jnz     short loc_140BC278A 

[...]

loc_140BC278A:
lea rax, [rbp+8D0h+var_850] ; Get the address of KeWaitForSingleObject
xor r9d, r9d
mov [rsp+9D0h+BugCheckParameter4], rax
xor r8d, r8d
mov rax, [rbp+8D0h+var_8D8]
mov rcx, r11
call KeGuardDispatchICall ; Call KeWaitForSingleObject

Aynı şekilde KeWaitForSingleObject’in adresi 0x140BC252F‘de hazırlanır.


  1. KeDelayExecutionThread: Süreç boyunca uyku durumunda öylece durur ve yedek olarak kullanılır. 0x140BC2770 adresinden:
loc_140BC2770:
xor edx, edx
test r11, r11
jnz short loc_140BC278A ; Call KeWaitForSingleObject

lea r8, [rbp+8D0h+var_850]
xor ecx, ecx
mov rax, r9
call KeGuardDispatchICall ; Call KeDelayExecutionThread

Burada kullanılacak herhangi bir yöntem, PatchGuard context’indeki değerler, özellikle KiInitPatchGuardContext tarafından ayarlanan flag’ler ve object pointer’ları tarafından belirlenir. Örneğin:


  • 0xAB8 ofsetindeki bir flag SelfEncryptWaitAndDecrypt yöntemini seçmek için kullanır. Bu flag 0x140BC251D‘de hazırlanmaya başlanır ve ardından bu fonksiyonun çağırılıp çağırılmayacağı belirlenir:

Daha sonradan bu flag, yukarıda gördüğümüz yöntemlerin kısımlarında kullanılmaktadır. Eğer bu flag set edilmiş ise SelfEncryptWaitAndDecrypt çağırılacaktır. Kodlara tekrar bakalım:

loc_140BC26F1:
...
mov r10, [rbp+8D0h+arg_8] ; Get the Flag

[...]

loc_140BC274E:
test r10, r10 ; If the flag is 0, then jump
jz short loc_140BC2770

; /* Call SelfEncryptWaitAndDecrypt */ 
mov rax, [rbp+8D0h+var_8F0] ; Get the address of SelfEncryptWaitAndDecrypt
lea r8, [rbp+8D0h+var_850]
mov edx, r14d
mov [rsp+9D0h+BugCheckParameter4], r10
mov rcx, rsi
call KeGuardDispatchICall 
jmp short loc_140BC27A8

  • Offset 0xA40 veya 0x5D0 KeWaitForSingleObject için Object tutabilir. KeWaitForSingleObject çağırılırken r11 register’ın değeri Object olarak geçiliyor:
loc_140BC2770:
xor edx, edx
test r11, r11        
jnz loc_140BC278A ; If the object is initialized, then jump it
[.. Calling KeDelayExecutionThread Function..]


...


loc_140BC278A:
...
mov rax, [rbp+8D0h+var_8D8] ; Get the address of KeWaitForSingleObject
mov rcx, r11 ; Pass the object as a parameter
call KeGuardDispatchICall 

r11’in değeri yine aynı adreste, 0x140BC251D’de ayarlanıyor:


  • Bunlar geçerli değilse, KeDelayExecutionThread aracılığıyla Sleep uygulanır (bknz loc_140BC2770 fonksiyon).
Decrypt Part 1 of the Context

Uyku veya bekleme işlemleri sona erdikten sonra, PatchGuard yürütme işlemlerine devam eder ve context’in ilk segmenti decryptler. Bu segment daha önce koruma için yeniden şifrelenmişti.

Revalidate Checksums for Parts 2 and 3

Bekleme aşamasında herhangi bir kurcalama olmadığından emin olmak için PatchGuard, Bölüm 2 ve 3’ün sağlama toplamlarını yeniden hesaplar ve bunları daha önce depolanan orijinal değerlerle karşılaştırır. Bu checksum değerleri, push/pop komutları kullanılarak geçici olarak yığına kaydedilerek bekleme boyunca korunuyor.

Orijinal değerler bekleme sırasında statik belleğe asla dokunmadığından, bunları yakalamak ve değiştirmek son derece zor olacaktır. Bu adım PatchGuard’ın kendi kendine bütünlük uygulamasının çok önemli bir parçasıdır.

Final Checksum of Part 1

Bu son kontrolünde, PatchGuard context’in ilk 0x618 byte’ın checksum’unu tekrar kontrol eder. Hesaplanan sonuç, context’in initialize edile sırasında oluşturulan orijinal checksum değeri ile karşılaştırılır (0x8b8 ofsetinde saklanır).

Set Thread Affinity to a Target Processor

Doğrulama kısmını tamamlamak için PatchGuard, bütünlük kontrolünün çalışması gereken belirli bir işlemci çekirdeği seçme işleminden devam eder. Bu önemlidir çünkü bazı kernel yapıları işlemciye özeldir - IDT gibi - ve tutarlı bir kernel üzerinde çalıştırılması kontrol ve güvenilirliğin korunmasına yardımcı olur.

PatchGuard, context’i başlatma sırasında saklanan session ID’i alarak başlar. Daha sonra sıfır ile toplam aktif process sayısı arasında bir değer üretir ve sistemden rastgele bir process seçer. Doğrudan bir PID seçmek yerine, sistemin process listesinde numaralandırma yapar ve üretilen numaraya göre entry seçer.

Bir süreç seçildiğinde, PatchGuard ayarlanmış bitlerin sayısını sayarak (yani, bir Hamming ağırlık işlemi gerçekleştirerek) bu yakınlıkta kaç işlemcinin mevcut olduğunu hesaplar. Bu izin verilen işlemciler kümesinden rastgele birini seçer ve KeSetSystemGroupAffinityThread API’sini kullanır.

Kernel Yapısının Bütünlük Kontrolü

IDT Verfication

IDT’yi kontrol etmek için PatchGuard bazı ön adımlardan geçer. İlk olarak, bugcheck göndererek başlar. IDT için tür 0x2’dir, daha sonra dispatcher 0x140BBFC59’a gider:

Interrupt Descriptor Table (IDT) işlemciye özgü olduğundan ve idtr kaydı üzerinden erişildiğinden, PatchGuard’ın belirli işlemciyi kontrol etmek için doğru işlemciyi seçmesi gerekiyor. Ve bu bilgi yapının 3. bölümünde 0x28 ofsetinde saklanır (0x140BEA994 adresinde başlatılır). Daha sonra PatchGuard bu bilgi ile bir KAFFINITY yapısını başlatır ve system thread’i çalıştırmak için KeSetSystemGroupAffinityThread’i çağırır. Son olarak idr ve gdtr değerlerini almak için KiGetGdtIdt’yi çağırır.

Daha sonra PatchGuard kontrolü iki parçaya böler: ilk parça KxUnexpectedInterrupt fonksiyonlarını ve ikincisi IDT’nin kendisine odaklanır. İlk olarak, PatchGuard aslında bir fonksiyon dizisi olan KxUnexpectedInterrupt0’ın adresini alarak başlar.

Her entry için, CR8’i 0xF’ye (0x140BBFCFA) ayarlayarak external interrupt’i devre dışı bırakır:

loc_140BBFCBD:
...
mov r15, cr8        ; Keep Original Value of CR8
mov eax, 0Fh
mov cr8, rax        ; Disable all external interrupt (CR8 = 0xF)

Ardından 0x140BBFD34 adresinde kontrolü başlatır:

mov eax, [rcx+4]
mov rcx, [rsi+628h] ; Get the entry from KxUnexpectedInterrupt0
mov eax, edi
lea rdx, [rcx+rax*8]
cmp rbx, rdx

KxUnexpectedInterrupt0’ın bu entryleri esasen 0x140BD724F adresinde başlatılır:

loc_140BD71C0:
...
lea rax, KxUnexpectedInterrupt0
mov [r14+628h], rax

ilgili KxUnexpectedInterrupt(s) entry ile eşleşirse, 0x140BBFD4E adresindeki KINTERRUPT nesnesini almak için KiGetInterruptObjectAddress’i çağıracaktır:

loc_140BBFD4E:
mov rax, [rsi+470h]
mov ecx, edi
call KeGuardDispatchICall ; Call KiGetInterruptObjectAddress

The address of KiGetInterruptObjectAddress is initialized at 0x140BD6D09:

loc_140BD6CE6:
...
lea rax, KiGetInterruptObjectAddress
mov [r14+470h], rax

Ardından PatchGuard RtlSectionTableFromVirtualAddress aracılığıyla aşağıda listelenmiş üç şeyi kontrol etmeye başlar:

  • adresin discardable bir image’a ait olup olmadığı (IMAGE_SCN_MEM_DISCARDABLE);
  • adresin ntoskrnl.exe’nin eşlemesine ait olup olmadığı;
  • ntoskrnl.exe’nin export listesinden bir rutine ait olup olmadığı (RtlLookupFunctionEntry kullanarak).

0x140BBFDDA adresinde, RtlSectionTableFromVirtualAddress çağırarak başlar:

loc_140BBFDA2:
...
mov r8d, dword ptr [rbp+8D0h+var_8A8]
mov rcx, [rsi+8F8h]
sub r8d, edx
mov  rax, [rsi+220h]
call KeGuardDispatchICall ; Call RtlSectionTableFromVirtualAddress
test rax, rax
jz loc_140BC010A

Ardından 0x140BBFE41 adresinde RtlLookupFunctionEntry rutinini çalıştırır:

loc_140BBFDF5:
...
lea rdx, [rbp+8D0h+var_878]
xor r8d, r8d
mov rcx, rbx
call KeGuardDispatchICall ; Call RtlLookupFunctionEntry

var_878 is initialized at 0x140BBFCBD:

loc_140BBFCBD:
mov rax, [rsi+8E8h]
mov r9, r15
mov [rbp+8D0h+var_878], rax

0x8E8 ofseti 0x140000000 değerini içerir. Bu ofsetin 0x140BFB409 adresinde hazırlandığını görebiliriz:

loc_140BFB402:                          
lea rbx, cs:140000000h
mov [rdi+8E8h], rbx
...

İkinci kısım için PatchGuard, diğer yapıların çoğunu doğruladığı gibi IDT kaydı tarafından başvurulan tablonun checksum’unu hesaplar. Hash hesaplamasını tamamladıktan sonra PatchGuard, KeRevertToUserGroupAffinityThread’i çağırarak önceki işlemci yakınlığını geri yükler, ardından hesaplanan hash’i bellekte saklanan ile karşılaştırarak işlemi tamamlar.

Epilogue

Kontrol rutininin epilogue’i iki bölüme ayrılabilir: bir değişiklik tespit edildiğinde gerçekleşen bölüm ve her şey yolunda gittiğinde gerçekleşen bölüm.

Normal Completion: No Issues Detected

Bir yapının son hash karşılaştırmasından sonra, daha önce belirtildiği gibi, doğrulanan toplam veri miktarı KiInitPatchGuardContext’te tanımlanan maksimum değerden azsa, PatchGuard dizideki bir sonraki yapıya geçer. Aksi takdirde, PatchGuard context’i daha sonra kullanmak üzere yeniden silahlandırır. Bu işlem birden fazla adım içerir, ancak bunlar temelde başlatma adımlarıyla aynıdır.

0, 1, 2, 4 ve 5 methodları için kodlar, uygulanan yönteme göre KiInitPatchGuardContext içinde bulunan uygulama ile neredeyse aynıdır:

  1. KeSetCoalescableTimer doğrudan çağrılır
  2. DPC, KPRCB.AcpiReserved içinde saklanır
  3. DPC KPRCB.HalReserved içinde saklanır
  4. APC, KeInsertQueueApc ile eklenir
  5. DPC zaten global bir değişkende ayarlanmıştı

Bir system thread’i oluşturulmasını içeren üçüncü yöntem, aynı ana fonksiyon içinde değil, yeniden ayarlar. Doğrulama rutini bittiğinde, küçük bir gönderici KeDelayExecutionThread veya KeWaitForSingleObject çağrısı arasında seçim yapar:

  • KeDelayExecutionThread seçilirse, 2 dakika ile 2 dakika 10 saniye arasında olağan bir zaman aşımı ayarlanır.

  • KeWaitForSingleObject seçilirse, bu kez 2 dakikalık sabit bir zaman aşımı uygulanır.

Yedinci yöntem için kesinlikle hiçbir şey yapılmaz: kod hemen kontrol rutininin sonuna atlar. Daha önce de belirtildiği gibi, bu yöntem intilization başladıktan hemen sonra temizlenir, bu nedenle buradaki davranışı belirsiz kalır.

Modification Detection: System Integrity Compromised

Checksum toplama işlemi tamamlandığında, Interrupt Descriptor Table (IDT) durumunda, PatchGuard ilk olarak mevcut thread için önceki işlemci yakınlığını geri yükler. Bunu takiben, yeni hesaplanan hash’i KiInitPatchGuardContext’in initilization sırasında saklanan orijinal hash ile karşılaştırır. Herhangi bir değişiklik tespit edilirse, PatchGuard, sonuçta bir BSOD tetikleyen kontrollü bir dizi adım yürütmeye başlayacaktır.

Checksum, Encryption, and Verifications

İlk aşama PatchGuard Context Yapısını içerir. PatchGuard tüm yapı üzerinde bir checksum kontrolü yapar, ancak bunu yapmadan önce, kontroller arasında dalgalanabilecek uçucu veya dinamik alanları temizleyerek veya sıfırlayarak yapıyı “ortak” bir duruma dönüştürür. Bu, çağrılar arasında tutarlı checksum sonuçları sağlamak için önemlidir.

Checksum hesaplamasından önce gerçekleştirilen işlemler şunları içerir:

  • Tüm context yapısı (1., 2. ve 3. bölümler) içinde checksum alanının kendisini (0x658 offsetinde bulunur) sıfırlama veya temizleme.

  • Kontrol edilen verilerin toplam boyutunu (0x6C8 offsetinde) context’in ilk bölümünün boyutuyla eşleşecek şekilde ayarlayarak işlemi başlatmadan itibaren tekrarlar.

  • Workitem alanını (“0x638” ofsetinde) stack’E kaydetmek

Yapıyı bu değişikliklerle hazırladıktan sonra, checksum hesabı tüm context üzerinden hesaplanır.

Checksum hesaplandıktan sonra, workitem stackten context’e geri yüklenir ve checksum sonucu 0x658 offsetinde saklanır.

Bu yeni hesaplanan checksum bu noktada önceki checksum ile doğrudan karşılaştırılmadığına dikkat etmek önemli olacaktır.

Daha sonra PatchGuard, PatchGuard Context’in en başını, yani CmpAppendDllSection kodunu yeniden şifrelemeye devam eder.

Restore Sensitive Data
Additional Anti-Debug Technique: Overwriting DbgPrint with a RET Instruction

PatchGuard, yürütme boyunca birden fazla Anti-Debug tekniği kullanır. Bu önlemlerden biri, ilk komutun üzerine RET işlem koduna karşılık gelen 0xC3 yazarak DbgPrint rutininin basit ama etkili bir şekilde tahrif edilmesi gibi görünmektedir.

Clearing Specific Context Entries

PatchGuard Context’teki iki özel offseti temizler: KxUnexpectedInterrupt0 ve KiIsrThunkShadow. Bunun kesin nedeni, özellikle checksum zaten hesaplanmış olduğundan, belirsizdir.

KeBugCheckEx or SdpbCheckDll

Doğrulama rutininin sonuna doğru PatchGuard, KeBugCheckEx argümanı ile KeGuardCheckICall çağrısı yapar. Kullanılan zamanlama yöntemi 7 ise, KeGuardCheckICall, KeGuardDispatchICall ile birlikte 0x140BFB3FD adresindeki KiInitPatchGuardContext içine yeniden yazılır:

loc_140BFB3EF:
lea rax, KeGuardCheckICall
sub eax, ecx
test r9d, r9d
jz short loc_140BFB402
mov byte ptr [rax+r8], 0C3h ; Ret Instruction

Bu, kullanılan yöntem 7 değilse KeBugCheckEx yerine SdpbCheckDll’in çağrılacağı anlamına gelir.

SdpbCheckDll aslında KeBugCheckEx’e giden bir stub’tur, ancak atlamadan önce ETHREAD.InitialStack’ten alınan thread’ın yığınını temizler. Eğer mevcut iş parçacığı bir DPC yürütüyorsa (KPCRB.DpcRoutineActive ile gösterilir), PatchGuard mevcut yığının DPC yığınıyla (KPCRB.DpcStack ile gösterilir) eşleşip eşleşmediğini kontrol eder. Bu durumda, ETHREAD.InitialStack’ı temizlemek yerine, DpcStack’ı temizler.

Conclusion

Bu yolculuk gerçekten uzun ve yoğun geçti. Nisan ayında başlayan bu çalışma, Ağustos ayında nihayet tamamlandı. Süreç boyunca yürüttüğüm analizler, yaklaşık 90 sayfalık teknik not ve belgeyle şekillendi. Bugün artık PatchGuard’a dair çok daha derin bir anlayışa sahibiz: kendisini gizlemek için uyguladığı yöntemler, gerçekleştirdiği denetim türleri ve farklı tekniklerle nasıl başlatıldığı gibi birçok yönünü detaylarıyla inceledik.

Bu çalışmada, PatchGuard’ın temel yapı taşlarını, başlatılma sürecini ve genel işleyişini yüksek seviyede ele alarak sistem mimarisine dair bütünsel bir bakış sunmayı amaçladım. Tersine mühendislik araçları ve metodolojileri kullanılarak yürütülen analiz, bu karmaşık güvenlik mekanizmasının işleyiş mantığını anlaşılır kılmayı hedefledi.

Tüm bu emek ve süreç sonucunda ortaya çıkan bu çalışmanın, güvenlik araştırmalarında başvuru niteliğinde bir kaynak olarak kullanılacağına inanıyorum.

References

Bu makale boyunca ana kaynak olarak Tetrane’in “Updated Analysis of PatchGuard on Microsoft Windows 10 RS4” kaynağını kullandım. Bu çalışma, PatchGuard’ın başlatılması, kernel düzeyinde bütünlük kontrolleri ve koruma stratejileri de dahil olmak üzere mekanizmaları hakkında ayrıntılı bilgiler sağladı.

  1. AMD64 Architecture Programmer’s Manual Volume 2, see page 109
  2. Microsoft Docs - KeExpandKernelStackAndCallout
  3. Intel® 64 Architecture x2APIC Specification, see page 2-2
  4. Intel 64 and IA-32 Architectures Software Developer’s Manual Volume 4: Model-Specific Registers, see Page 2-4
  5. Felix Clouiter - CPUID
Last updated on