/
Автор: Yosifovich P. Ionescu A. Russinovich M.E. Solomon D.A.
Теги: windows informatik betriebssysteme
ISBN: 978-3-86490-538-4
Год: 2018
Похожие
Текст
Zu diesem Buch – sowie zu vielen weiteren dpunkt.büchern – können Sie auch das entsprechende E-Book im PDF-Format herunterladen. Werden Sie dazu einfach Mitglied bei dpunkt.plus+: www.dpunkt.plus
Für meine Familie: meine Frau Idit und unsere Kinder Danielle, Amit und Yoav. Danke für eure Geduld und eure Ermutigung während dieser anspruchsvollen Arbeit. – Pavel Yosifovich Für meine Eltern, die mich angeleitet und angeregt haben, meine Träume zu verfolgen, und für meine Familie, die während all dieser zahllosen Abende hinter mir gestanden hat. – Alex Ionescu Für unsere Eltern, die uns angeleitet und angeregt haben, unsere Träume zu verfolgen. – Mark E. Russinovich und David A. Solomon
Windows Internals Band 1: Systemarchitektur, Prozesse, Threads, Speicherverwaltung, Sicherheit und mehr Übersetzung der 7. englischsprachigen Auflage Pavel Yosifovich Alex Ionescu Mark E. Russinovich David A. Solomon
Pavel Yosifovich Alex Ionescu Mark E. Russinovich David A. Solomon Lektorat: Sandra Bollenbacher Übersetzung: G&U Language & Publishing Services GmbH, www.gundu.com Copy-Editing: Petra Heubach-Erdmann Satz: G&U Language & Publishing Services GmbH, www.gundu.com Umschlaggestaltung: Helmut Kraus, exclam.de Druckerei: C.H.Beck ISBN: Print PDF ePub mobi 978-3-86490-538-4 978-3-96088-338-8 978-3-96088-339-5 978-3-96088-340-1 Translation Copyright für die deutschsprachige Ausgabe © 2018 dpunkt.verlag GmbH Wieblinger Weg 17 69123 Heidelberg Authorized translation from the English language edition, entitled WINDOWS INTERNALS, PART 1: SYSTEM ARCHITECTURE, PROCESSES, THREADS, MEMORY MANAGEMENT, AND MORE, 7th Edition by PAVEL YOSIFOVICH, ALEX IONESCU, MARK RUSSINOVICH, DAVID SOLOMON, published by Pearson Education, Inc, publishing as Microsoft Press, Copyright © 2017 by Pavel Yosifovich, Alex Ionescu, Mark E. Russinovich and David A. Solomon All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from Pearson Education, Inc. German language edition published by dpunkt.verlag GmbH, Copyright © 2018 ISBN of the English language edition: 978-0-7356-8418-8 Die vorliegende Publikation ist urheberrechtlich geschützt. Alle Rechte vorbehalten. Die Verwendung der Texte und Abbildungen, auch auszugsweise, ist ohne die schriftliche Zustimmung des Verlags urheberrechtswidrig und daher strafbar. Dies gilt insbesondere für die Vervielfältigung, Übersetzung oder die Verwendung in elektronischen Systemen. Es wird darauf hingewiesen, dass die im Buch verwendeten Soft- und Hardware-Bezeichnungen sowie Markennamen und Produktbezeichnungen der jeweiligen Firmen im Allgemeinen warenzeichen-, marken- oder patentrechtlichem Schutz unterliegen. Alle Angaben und Programme in diesem Buch wurden mit größter Sorgfalt kontrolliert. Weder Autor noch Verlag können jedoch für Schäden haftbar gemacht werden, die in Zusammenhang mit der Verwendung dieses Buchs stehen. 543210
Inhaltsverzeichnis Einleitung xvii Kapitel 1 Begriffe und Werkzeuge 1 Versionen des Betriebssystems Windows ����������������������������������������������������������������������� 1 Windows 10 und zukünftige Windows-Versionen ������������������������������������������������� 3 Windows 10 und OneCore ����������������������������������������������������������������������������������������� 3 Grundprinzipien und -begriffe ����������������������������������������������������������������������������������������� 4 Die Windows-API ��������������������������������������������������������������������������������������������������������� 4 Dienste, Funktionen und Routinen ��������������������������������������������������������������������������� 7 Prozesse ������������������������������������������������������������������������������������������������������������������������ 8 Threads ����������������������������������������������������������������������������������������������������������������������� 20 Jobs ����������������������������������������������������������������������������������������������������������������������������� 23 Virtueller Arbeitsspeicher ����������������������������������������������������������������������������������������� 23 Kernel- und Benutzermodus ����������������������������������������������������������������������������������� 26 Hypervisor ������������������������������������������������������������������������������������������������������������������ 30 Firmware ��������������������������������������������������������������������������������������������������������������������� 32 Terminaldienste und mehrere Sitzungen ��������������������������������������������������������������� 32 Objekte und Handles ����������������������������������������������������������������������������������������������� 33 Sicherheit ������������������������������������������������������������������������������������������������������������������� 34 Die Registrierung ������������������������������������������������������������������������������������������������������� 36 Unicode ����������������������������������������������������������������������������������������������������������������������� 37 Die internen Mechanismen von Windows untersuchen ��������������������������������������������� 39 Leistungsüberwachung und Ressourcenmonitor ������������������������������������������������� 40 Kernel-Debugging ����������������������������������������������������������������������������������������������������� 42 Windows Software Development Kit ��������������������������������������������������������������������� 48 Windows Driver Kit ��������������������������������������������������������������������������������������������������� 49 Sysinternals-Werkzeuge ������������������������������������������������������������������������������������������� 49 Zusammenfassung ����������������������������������������������������������������������������������������������������������� 50 vii
Kapitel 2 Systemarchitektur 51 Anforderungen und Designziele ����������������������������������������������������������������������������������� 51 Das Modell des Betriebssystems ������������������������������������������������������������������������������������� 52 Die Architektur im Überblick ����������������������������������������������������������������������������������������� 53 Portierbarkeit ������������������������������������������������������������������������������������������������������������� 56 Symmetrisches Multiprocessing ����������������������������������������������������������������������������� 57 Skalierbarkeit ������������������������������������������������������������������������������������������������������������� 60 Unterschiede zwischen den Client- und Serverversionen ����������������������������������� 61 Testbuild ��������������������������������������������������������������������������������������������������������������������� 64 Die virtualisierungsgestützte Sicherheitsarchitektur im Überblick ��������������������������� 66 Hauptsystemkomponenten ��������������������������������������������������������������������������������������������� 69 Umgebungsteilsysteme und Teilsystem-DLLs ������������������������������������������������������� 70 Weitere Teilsysteme ��������������������������������������������������������������������������������������������������� 76 Exekutive ��������������������������������������������������������������������������������������������������������������������� 81 Der Kernel ������������������������������������������������������������������������������������������������������������������� 84 Die Hardwareabstraktionsschicht ��������������������������������������������������������������������������� 88 Gerätetreiber ������������������������������������������������������������������������������������������������������������� 92 Systemprozesse ��������������������������������������������������������������������������������������������������������� 98 Zusammenfassung ��������������������������������������������������������������������������������������������������������� 111 Kapitel 3 Prozesse und Jobs 113 Prozesse erstellen ����������������������������������������������������������������������������������������������������������� 113 Argumente der CreateProcess*-Funktionen ������������������������������������������������������� 114 Moderne Windows-Prozesse erstellen ����������������������������������������������������������������� 115 Andere Arten von Prozessen erstellen ����������������������������������������������������������������� 116 Interne Mechanismen von Prozessen ������������������������������������������������������������������������� 117 Geschützte Prozesse ������������������������������������������������������������������������������������������������������� 126 Protected Process Light (PPL) ������������������������������������������������������������������������������� 128 Unterstützung für Drittanbieter-PPLs ������������������������������������������������������������������� 133 Minimale und Pico-Prozesse ����������������������������������������������������������������������������������������� 134 Minimale Prozesse ��������������������������������������������������������������������������������������������������� 134 Pico-Prozesse ����������������������������������������������������������������������������������������������������������� 134 viii Inhaltsverzeichnis
Trustlets (sichere Prozesse) ������������������������������������������������������������������������������������������� 137 Der Aufbau von Trustlets ��������������������������������������������������������������������������������������� 137 Richtlinien-Metadaten für Trustlets ��������������������������������������������������������������������� 138 Attribute von Trustlets �������������������������������������������������������������������������������������������� 139 Integrierte System-Trustlets ����������������������������������������������������������������������������������� 140 Trustlet-Identität ����������������������������������������������������������������������������������������������������� 140 IUM-Dienste ������������������������������������������������������������������������������������������������������������� 141 Für Trustlets zugängliche Systemaufrufe ������������������������������������������������������������� 142 Der Ablauf von CreateProcess ��������������������������������������������������������������������������������������� 144 Phase 1: Parameter und Flags konvertieren und validieren ����������������������������� 145 Phase 2: Das auszuführende Abbild öffnen ��������������������������������������������������������� 151 Phase 3: Das Windows-Exekutivprozessobjekt erstellen ����������������������������������� 154 Phase 4: Den ursprünglichen Thread mit seinem Stack und Kontext erstellen ����������������������������������������������������������������������������������������������������� 160 Phase 5: Spezifische Prozessinitialisierung für das Windows-Teilsystem durchführen ��������������������������������������������������������������������������������������������� 162 Phase 6: Ausführung des ursprünglichen Threads starten ������������������������������� 164 Phase 7: Prozessinitialisierung im Kontext des neuen Prozesses durchführen ��������������������������������������������������������������������������������������������� 165 Einen Prozess beenden ������������������������������������������������������������������������������������������������� 172 Der Abbildlader ��������������������������������������������������������������������������������������������������������������� 173 Frühe Prozessinitialisierung ����������������������������������������������������������������������������������� 176 DLL-Namensauflösung und -Umleitung ������������������������������������������������������������� 179 Die Datenbank der geladenen Module ��������������������������������������������������������������� 183 Importanalyse ��������������������������������������������������������������������������������������������������������� 188 Prozessinitialisierung nach dem Import ��������������������������������������������������������������� 190 SwitchBack ��������������������������������������������������������������������������������������������������������������� 191 API-Sets ��������������������������������������������������������������������������������������������������������������������� 194 Jobs ����������������������������������������������������������������������������������������������������������������������������������� 196 Grenzwerte für Jobs ����������������������������������������������������������������������������������������������� 197 Umgang mit Jobs ��������������������������������������������������������������������������������������������������� 199 Verschachtelte Jobs ������������������������������������������������������������������������������������������������� 199 Windows-Container (Serversilos) ������������������������������������������������������������������������� 204 Zusammenfassung ��������������������������������������������������������������������������������������������������������� 213 Inhaltsverzeichnis ix
Kapitel 4 Threads 215 Threads erstellen ������������������������������������������������������������������������������������������������������������� 215 Interne Strukturen von Threads ����������������������������������������������������������������������������������� 216 Datenstrukturen ������������������������������������������������������������������������������������������������������� 216 Geburt eines Threads ��������������������������������������������������������������������������������������������� 230 Die Threadaktivität untersuchen ��������������������������������������������������������������������������������� 231 Einschränkungen für Threads in geschützten Prozessen ����������������������������������� 237 Threadplanung ��������������������������������������������������������������������������������������������������������������� 238 Überblick über die Threadplanung in Windows ������������������������������������������������� 239 Prioritätsstufen ��������������������������������������������������������������������������������������������������������� 240 Threadstatus ������������������������������������������������������������������������������������������������������������� 248 Die Dispatcherdatenbank ��������������������������������������������������������������������������������������� 255 Das Quantum ����������������������������������������������������������������������������������������������������������� 258 Prioritätserhöhung ������������������������������������������������������������������������������������������������� 266 Kontextwechsel ������������������������������������������������������������������������������������������������������� 286 Mögliche Fälle bei der Threadplanung ��������������������������������������������������������������� 288 Leerlaufthreads ������������������������������������������������������������������������������������������������������� 292 Anhalten von Threads ��������������������������������������������������������������������������������������������� 296 Einfrieren und Tiefgefrieren ����������������������������������������������������������������������������������� 296 Threadauswahl ��������������������������������������������������������������������������������������������������������� 299 Mehrprozessorsysteme ������������������������������������������������������������������������������������������� 301 Threadauswahl auf Mehrprozessorsystemen ����������������������������������������������������� 319 Prozessorauswahl ����������������������������������������������������������������������������������������������������� 320 Heterogene Threadplanung (big.LITTLE) ������������������������������������������������������������� 322 Gruppengestützte Threadplanung ������������������������������������������������������������������������������� 324 Dynamische gleichmäßige Planung (DFSS) ��������������������������������������������������������� 326 Grenzwerte für die CPU-Rate ������������������������������������������������������������������������������� 330 Dynamisches Hinzufügen und Ersetzen von Prozessoren ��������������������������������� 333 Arbeitsfactories (Threadpools) ������������������������������������������������������������������������������������� 335 Erstellen von Arbeitsfactories �������������������������������������������������������������������������������� 337 Zusammenfassung ��������������������������������������������������������������������������������������������������������� 339 x Inhaltsverzeichnis
Kapitel 5 Speicherverwaltung 341 Einführung in den Speicher-Manager ������������������������������������������������������������������������� 341 Komponenten des Speicher-Managers ��������������������������������������������������������������� 342 Große und kleine Seiten ����������������������������������������������������������������������������������������� 343 Die Speichernutzung untersuchen ����������������������������������������������������������������������� 345 Interne Synchronisierung ��������������������������������������������������������������������������������������� 350 Vom Speicher-Manager bereitgestellte Dienste ������������������������������������������������������� 350 Seitenstatus und Speicherzuweisungen ��������������������������������������������������������������� 352 Gesamter zugesicherter Speicher und Zusicherungsgrenzwert ����������������������� 356 Seiten im Arbeitsspeicher festhalten ������������������������������������������������������������������� 356 Granularität der Zuweisung ����������������������������������������������������������������������������������� 357 Gemeinsam genutzter Arbeitsspeicher und zugeordnete Dateien ����������������� 357 Speicherschutz ��������������������������������������������������������������������������������������������������������� 360 Datenausführungsverhinderung ��������������������������������������������������������������������������� 362 Kopieren beim Schreiben ��������������������������������������������������������������������������������������� 366 AWE (Address Windowing Extensions) ����������������������������������������������������������������� 367 Kernelmodusheaps (Systemspeicherpools) ����������������������������������������������������������������� 370 Poolgrößen ��������������������������������������������������������������������������������������������������������������� 370 Die Poolnutzung überwachen ������������������������������������������������������������������������������� 372 Look-Aside-Listen ��������������������������������������������������������������������������������������������������� 377 Der Heap-Manager ��������������������������������������������������������������������������������������������������������� 378 Prozessheaps ����������������������������������������������������������������������������������������������������������� 379 Arten von Heaps ����������������������������������������������������������������������������������������������������� 380 NT-Heaps ����������������������������������������������������������������������������������������������������������������� 380 Heapsynchronisierung ������������������������������������������������������������������������������������������� 381 Der Low-Fragmentation-Heap ����������������������������������������������������������������������������� 381 Segmentheaps ��������������������������������������������������������������������������������������������������������� 383 Sicherheitseinrichtungen von Heaps ������������������������������������������������������������������� 388 Debugging einrichten für Heaps ��������������������������������������������������������������������������� 390 Der Seitenheap ��������������������������������������������������������������������������������������������������������� 391 Der fehlertolerante Heap ��������������������������������������������������������������������������������������� 394 Layouts für virtuelle Adressräume ������������������������������������������������������������������������������� 396 x86-Adressraumlayouts ����������������������������������������������������������������������������������������� 397 Das Layout des x86-Systemadressraums ������������������������������������������������������������� 401 Inhaltsverzeichnis xi
x86-Sitzungsraum ��������������������������������������������������������������������������������������������������� 401 System-Seitentabelleneinträge ����������������������������������������������������������������������������� 404 ARM-Adressraumlayout ����������������������������������������������������������������������������������������� 405 64-Bit-Adressraumlayout ��������������������������������������������������������������������������������������� 406 Einschränkungen bei der virtuellen Adressierung auf x64-Systemen ������������� 408 Dynamische Verwaltung des virtuellen Systemadressraums ��������������������������� 408 Kontingente für den virtuellen Systemadressraum ������������������������������������������� 415 Layout des Benutzeradressraums ������������������������������������������������������������������������� 416 Adressübersetzung ��������������������������������������������������������������������������������������������������������� 422 Übersetzung virtueller Adressen auf x86-Systemen ����������������������������������������� 422 Der Look-Aside-Übersetzungspuffer für die Übersetzung ������������������������������� 429 Übersetzung virtueller Adressen auf x64-Systemen ����������������������������������������� 433 Übersetzung virtueller Adressen auf ARM-Systemen ��������������������������������������� 434 Seitenfehler ��������������������������������������������������������������������������������������������������������������������� 435 Ungültige PTEs ��������������������������������������������������������������������������������������������������������� 436 Prototyp-PTEs ����������������������������������������������������������������������������������������������������������� 438 Einlagerungs-E/A ����������������������������������������������������������������������������������������������������� 440 Seitenfehlerkollisionen ������������������������������������������������������������������������������������������� 440 Seitencluster ������������������������������������������������������������������������������������������������������������� 441 Auslagerungsdateien ��������������������������������������������������������������������������������������������� 442 Gesamter zugesicherter Speicher und systemweiter Zusicherungsgrenzwert ����������������������������������������������������������������������������������������� 448 Der Zusammenhang zwischen dem gesamten zugesicherten Speicher und der Größe der Auslagerungsdatei ����������������������������������������������������������������� 452 Stacks ������������������������������������������������������������������������������������������������������������������������������� 454 Benutzerstacks ��������������������������������������������������������������������������������������������������������� 455 Kernelstacks ������������������������������������������������������������������������������������������������������������� 456 Der DPC-Stack ��������������������������������������������������������������������������������������������������������� 457 VADs ��������������������������������������������������������������������������������������������������������������������������������� 457 Prozess-VADs ����������������������������������������������������������������������������������������������������������� 458 Umlauf-VADs ����������������������������������������������������������������������������������������������������������� 460 NUMA ������������������������������������������������������������������������������������������������������������������������������� 461 Abschnittsobjekte ����������������������������������������������������������������������������������������������������������� 462 Arbeitssätze ��������������������������������������������������������������������������������������������������������������������� 472 Auslagerung bei Bedarf ����������������������������������������������������������������������������������������� 472 Der logische Prefetcher und ReadyBoot ������������������������������������������������������������� 473 xii Inhaltsverzeichnis
Platzierungsrichtlinien ������������������������������������������������������������������������������������������� 477 Verwaltung von Arbeitssätzen ������������������������������������������������������������������������������� 477 Der Balance-Set-Manager und der Swapper ������������������������������������������������������ 482 Systemarbeitssätze ������������������������������������������������������������������������������������������������� 483 Speicherbenachrichtigungsereignisse ����������������������������������������������������������������� 485 Die PFN-Datenbank ������������������������������������������������������������������������������������������������������� 487 Seitenlistendynamik ����������������������������������������������������������������������������������������������� 491 Seitenpriorität ���������������������������������������������������������������������������������������������������������� 499 Die Schreibthreads für geänderte und für zugeordnete Seiten ����������������������� 502 PFN-Datenstrukturen ��������������������������������������������������������������������������������������������� 504 Reservierungen in der Auslagerungsdatei ����������������������������������������������������������� 509 Grenzwerte für den physischen Speicher ������������������������������������������������������������������� 512 Speichergrenzwerte für Windows-Clienteditionen ������������������������������������������� 513 Speicherkomprimierung ����������������������������������������������������������������������������������������������� 515 Ablauf der Komprimierung ����������������������������������������������������������������������������������� 516 Komprimierungsarchitektur ����������������������������������������������������������������������������������� 520 Speicherpartitionen ������������������������������������������������������������������������������������������������������� 523 Speicherzusammenführung ����������������������������������������������������������������������������������������� 526 Die Suchphase ��������������������������������������������������������������������������������������������������������� 528 Die Klassifizierungsphase ��������������������������������������������������������������������������������������� 529 Die Zusammenführungsphase ������������������������������������������������������������������������������� 530 Vom privaten zum gemeinsamen PTE ����������������������������������������������������������������� 530 Freigabe von zusammengeführten Seiten ����������������������������������������������������������� 533 Speicherenklaven ����������������������������������������������������������������������������������������������������������� 536 Programmierschnittstellen ������������������������������������������������������������������������������������� 538 Initialisierung von Speicherenklaven ������������������������������������������������������������������� 538 Aufbau von Enklaven ��������������������������������������������������������������������������������������������� 539 Daten in eine Enklave laden ���������������������������������������������������������������������������������� 541 Eine Enklave initialisieren ��������������������������������������������������������������������������������������� 542 Vorausschauende Speicherverwaltung (SuperFetch) ����������������������������������������������� 542 Komponenten ��������������������������������������������������������������������������������������������������������� 543 Ablaufverfolgung und Protokollierung ��������������������������������������������������������������� 544 Szenarien ������������������������������������������������������������������������������������������������������������������� 545 Seitenpriorität und Rebalancing ��������������������������������������������������������������������������� 546 Leistungsstabilisierung ������������������������������������������������������������������������������������������� 549 Inhaltsverzeichnis xiii
ReadyBoost ��������������������������������������������������������������������������������������������������������������� 550 ReadyDrive ��������������������������������������������������������������������������������������������������������������� 551 Prozessreflexion ������������������������������������������������������������������������������������������������������� 552 Zusammenfassung ��������������������������������������������������������������������������������������������������������� 554 Kapitel 6 Das E/A-System 555 Komponenten des E/A-Systems ����������������������������������������������������������������������������������� 555 Der E/A-Manager ��������������������������������������������������������������������������������������������������� 557 Typische E/A-Verarbeitung ������������������������������������������������������������������������������������ 558 IRQ-Ebenen und verzögerte Prozeduraufrufe ����������������������������������������������������������� 560 IRQ-Ebenen ������������������������������������������������������������������������������������������������������������� 561 Verzögerte Prozeduraufrufe ��������������������������������������������������������������������������������� 563 Gerätetreiber ������������������������������������������������������������������������������������������������������������������� 564 Arten von Gerätetreibern ��������������������������������������������������������������������������������������� 565 Aufbau eines Treibers ��������������������������������������������������������������������������������������������� 572 Treiber- und Geräteobjekte ����������������������������������������������������������������������������������� 574 Geräte öffnen ����������������������������������������������������������������������������������������������������������� 582 E/A-Verarbeitung ����������������������������������������������������������������������������������������������������������� 586 Verschiedene Arten der E/A ����������������������������������������������������������������������������������� 586 E/A-Anforderungspakete ��������������������������������������������������������������������������������������� 590 E/A-Anforderungen an einen einschichtigen Hardwaretreiber ����������������������� 603 E/A-Anforderungen an geschichtete Treiber ����������������������������������������������������� 613 Threadagnostische E/A ������������������������������������������������������������������������������������������� 616 Abbrechen der E/A ������������������������������������������������������������������������������������������������� 617 E/A-Vervollständigungsports ��������������������������������������������������������������������������������� 622 E/A-Priorisierung ����������������������������������������������������������������������������������������������������� 627 Containerbenachrichtigungen ������������������������������������������������������������������������������� 634 Treiberüberprüfung ������������������������������������������������������������������������������������������������������� 635 E/A-Überprüfungsoptionen ����������������������������������������������������������������������������������� 638 Speicherüberprüfungsoptionen ��������������������������������������������������������������������������� 638 Der PnP-Manager ����������������������������������������������������������������������������������������������������������� 643 Der Grad der Unterstützung für Plug & Play ����������������������������������������������������� 644 Geräteauflistung ����������������������������������������������������������������������������������������������������� 645 Gerätestacks ������������������������������������������������������������������������������������������������������������� 649 xiv Inhaltsverzeichnis
Treiberunterstützung für Plug & Play ������������������������������������������������������������������� 655 Installation von Plug-&-Play-Treibern ����������������������������������������������������������������� 656 Laden und Installieren von Treibern ��������������������������������������������������������������������������� 661 Treiber laden ������������������������������������������������������������������������������������������������������������� 661 Treiberinstallation ��������������������������������������������������������������������������������������������������� 663 Windows Driver Foundation ����������������������������������������������������������������������������������������� 664 Kernel-Mode Driver Framework ��������������������������������������������������������������������������� 665 Das E/A-Modell von KMDF ����������������������������������������������������������������������������������� 672 User-Mode Driver Framework ������������������������������������������������������������������������������� 675 Energieverwaltung ��������������������������������������������������������������������������������������������������������� 679 Verbundener und moderner Standbymodus ����������������������������������������������������� 683 Funktionsweise der Energieverwaltung ��������������������������������������������������������������� 684 Energieverwaltung durch die Treiber ������������������������������������������������������������������� 686 Steuerung der Geräteenergiezustände durch den Treiber und die Anwendung ������������������������������������������������������������������������������������������������������������� 689 Das Framework für die Energieverwaltung ��������������������������������������������������������� 690 Energieverfügbarkeitsanforderungen ����������������������������������������������������������������� 692 Zusammenfassung ��������������������������������������������������������������������������������������������������������� 694 Kapitel 7 Sicherheit 697 Sicherheitseinstufungen ����������������������������������������������������������������������������������������������� 697 Trusted Computer System Evaluation Criteria ����������������������������������������������������� 697 Common Criteria ����������������������������������������������������������������������������������������������������� 699 Systemkomponenten für die Sicherheit ��������������������������������������������������������������������� 700 Virtualisierungsgestützte Sicherheit ��������������������������������������������������������������������������� 704 Credential Guard ����������������������������������������������������������������������������������������������������� 705 Device Guard ����������������������������������������������������������������������������������������������������������� 712 Objekte schützen ����������������������������������������������������������������������������������������������������������� 714 Zugriffsprüfungen ��������������������������������������������������������������������������������������������������� 716 Sicherheitskennungen ��������������������������������������������������������������������������������������������� 720 Virtuelle Dienstkonten ������������������������������������������������������������������������������������������� 745 Sicherheitsdeskriptoren und Zugriffssteuerung ������������������������������������������������� 751 Dynamische Zugriffssteuerung ����������������������������������������������������������������������������� 770 Inhaltsverzeichnis xv
Die AuthZ-API ����������������������������������������������������������������������������������������������������������������� 771 Bedingte Zugriffssteuerungseinträge ������������������������������������������������������������������� 772 Privilegien und Kontorechte ����������������������������������������������������������������������������������������� 773 Kontorechte ������������������������������������������������������������������������������������������������������������� 775 Privilegien ����������������������������������������������������������������������������������������������������������������� 776 Superprivilegien ������������������������������������������������������������������������������������������������������� 783 Zugriffstokens von Prozessen und Threads ��������������������������������������������������������������� 784 Sicherheitsüberwachung ����������������������������������������������������������������������������������������������� 785 Überwachung des Objektzugriffs ������������������������������������������������������������������������� 787 Globale Überwachungsrichtlinie ��������������������������������������������������������������������������� 790 Erweiterte Überwachungsrichtlinienkonfiguration ������������������������������������������� 792 Anwendungscontainer ��������������������������������������������������������������������������������������������������� 793 UWP-Apps im Überblick ����������������������������������������������������������������������������������������� 794 Anwendungscontainer ������������������������������������������������������������������������������������������� 796 Anmeldung ��������������������������������������������������������������������������������������������������������������������� 824 Initialisierung durch Winlogon ����������������������������������������������������������������������������� 826 Die einzelnen Schritte der Benutzeranmeldung ������������������������������������������������� 827 Sichere Authentifizierung ��������������������������������������������������������������������������������������� 833 Das Windows-Biometrieframework ��������������������������������������������������������������������� 835 Windows Hello ��������������������������������������������������������������������������������������������������������� 837 Benutzerkontensteuerung und Virtualisierung ��������������������������������������������������������� 838 Virtualisierung des Dateisystems und der Registrierung ����������������������������������� 839 Rechteerhöhung ����������������������������������������������������������������������������������������������������� 847 Schutz gegen Exploits ��������������������������������������������������������������������������������������������������� 855 Abwehrmaßnahmen auf Prozessebene ��������������������������������������������������������������� 856 Control Flow Integrity ��������������������������������������������������������������������������������������������� 861 Zusicherungen ��������������������������������������������������������������������������������������������������������� 875 Anwendungsidentifizierung ����������������������������������������������������������������������������������������� 880 AppLocker ����������������������������������������������������������������������������������������������������������������������� 882 Richtlinien für Softwareeinschränkung ����������������������������������������������������������������������� 887 Kernelpatchschutz ����������������������������������������������������������������������������������������������������������� 889 PatchGuard ��������������������������������������������������������������������������������������������������������������� 890 HyperGuard ������������������������������������������������������������������������������������������������������������� 894 Zusammenfassung ��������������������������������������������������������������������������������������������������������� 896 Index 897 xvi Inhaltsverzeichnis
Einleitung Dieses Buch ist für Computerexperten (Entwickler, Sicherheitsforscher und Systemadministratoren) gedacht, die die Funktionsweise der Kernkomponenten von Microsoft Windows 10 und Windows Server 2016 kennenlernen möchten. Mit diesen Kenntnissen können Entwickler beim Schreiben von Anwendungen für die Windows-Plattform die Gründe für bestimmte Designentscheidungen besser verstehen. Sie helfen ihnen auch beim Debugging komplizierter Probleme. Auch Systemadministratoren profitieren von diesen Informationen, da das Wissen, wie das System in seinem Inneren »tickt«, sein Verhalten verständlicher macht und die Störungssuche erleichtert. Sicherheitsforscher erfahren hier, welche Fehlverhalten Softwareanwendungen und das Betriebssystem an den Tag legen können, wie sie missbraucht werden können und welche Schutzmaßnahmen und Sicherheitsfunktionen moderne Windows-Versionen dagegen bieten. Nach der Lektüre dieses Buches haben Sie ein besseres Verständnis darüber, wie Windows funktioniert und welche Gründe hinter seinem Verhalten stehen. Die Geschichte dieses Buches Dies ist die siebente Ausgabe eines Buches, das ursprünglich Inside Windows NT hieß (Microsoft Press, 1992) und noch vor der Erstveröffentlichung von Windows NT 3.1 von Helen Custer geschrieben wurde. Es war das erste Buch, das es je über Windows NT gab, und es bot wichtige Einsichten in die Architektur und das Design des Systems. Inside Windows NT, Second Edition (Microsoft Press, 1998) wurde von David Solomon geschrieben. Es deckte auch Windows NT 4.0 ab und ging weit tiefer in die technischen Details. Inside Windows 2000, Third Edition (Microsoft Press, 2000) wurde von David Solomon und Mark Russinovich geschrieben. Hier kamen viele neue Themen wie Starten und Herunterfahren, interne Mechanismen von Diensten und der Registrierung, Dateisystemtreiber und Netzwerkanbindung hinzu. Auch die Änderungen am Kernel in Windows 2000 wurden erläutert, darunter das Windows Driver Model (WDM), Plug & Play, Energieverwaltung, Windows-Verwaltungsinstrumentation (Windows Management Instrumentation, WMI), Verschlüsselung, Jobobjekte und Terminaldienste. Windows Internals, Fourth Edition (Microsoft Press, 2004) war die neue Ausgabe für Windows XP und Windows Server 2003 mit zusätzlichen Inhalten, um IT-Fachleuten zu helfen, ihre Kenntnisse über die internen Mechanismen von Windows praktisch anzuwenden, z. B. durch die Werkzeuge von Sysinternals und die Analyse von Absturzabbildern. Mit Windows Internals, Fifth Edition (Microsoft Press, 2009) wurde das Buch auf Windows Vista und Windows Server 2008 aktualisiert. Da Mark Russinovich eine Vollzeitstelle bei Microsoft annahm (wo er jetzt als CTO für Azure arbeitet), kam der neue Co-Autor Alex Ionescu hinzu. Zu den neuen Inhalten gehörten der Abbildlader, Einrichtungen für das Debugging im Benutzermodus, ALPC (Advanced Local Procedure Call) und Hyper-V. Die nächste Ausgabe, xvii
Windows Internals, Sixth Edition (Microsoft Press, 2012) war komplett aktualisiert, um viele Änderungen am Kernel in Windows 7 und Windows Server 2008 R2 abzudecken. Außerdem kamen viele neue praktische Experimente hinzu, um die Änderungen an den Werkzeugen deutlich zu machen. Änderungen in dieser Auflage Seit der letzten Aktualisierung dieses Buches hat Windows mehrere neue Releases durchlaufen und ist jetzt bei Windows 10 und Windows Server 2016 angekommen. Dabei ist Windows 10 jetzt praktisch der neue Name für Windows. Seit der Produktionsfreigabe hat es wiederum mehrere Releases gegeben. Bezeichnet werden sie jeweils mit einer vierstelligen Zahl, die sich aus dem Jahr und dem Monat der Veröffentlichung zusammensetzt, also z. B. Windows 10 Version 1703 für die Version vom März 2017. Zum Zeitpunkt der Abfassung dieses Buches hat Windows also seit Windows 7 mindestens sechs Versionen durchlaufen. Beginnend mit Windows 8 hat Microsoft mit einer Zusammenführung seiner Betriebssysteme begonnen, was für die Entwicklung und für das Windows-Konstruktionsteam von Vorteil ist. Windows 8 und Windows Phone 8 verwendeten bereits einen gemeinsamen Kernel, und mit Windows 8.1 und Windows Phone 8.1 kam auch eine Vereinheitlichung bei modernen Apps hinzu. Mit Windows 10 war die Zusammenführung abgeschlossen: Dieses Betriebssystem läuft auf Desktops, Laptops, Servern, der Xbox One, Smartphones (Windows Mobile 10), der HoloLens und verschiedenen IoT-Geräten (Internet of Things). Angesichts dieser großen Vereinheitlichung war die Zeit reif für eine neue Ausgabe dieses Buches, um die Änderungen von fast einem halben Jahrzehnt aufzuarbeiten, die zu einer stabileren Kernelarchitektur geführt haben. Daher deckt dieses Buch verschiedene Aspekte des Betriebssystems von Windows 8 bis Windows 10 Version 1703 ab. Des Weiteren heißen wir Pavel Yosifovich als neuen Co-Autor willkommen. Praktische Experimente Auch ohne Zugriff auf den Windows-Quellcode können Sie mithilfe des Kerneldebuggers, der Sysinternals-Tools und der eigens für dieses Buch entwickelten Werkzeuge viel über die internen Mechanismen von Windows herausfinden. Immer wenn ein Aspekt des internen Verhaltens von Windows mit einem der Tools sichtbar gemacht oder veranschaulicht werden kann, erfahren Sie in den mit »Experiment« betitelten Abschnitten, wie Sie dieses Tool selbst ausprobieren können. Diese Abschnitte sind über das ganze Buch verteilt, und wir raten Ihnen dazu, diese Experimente bei der Lektüre auszuführen. Es ist viel eindrucksvoller, die sichtbaren Auswirkungen der internen Mechanismen von Windows zu beobachten, als nur darüber zu lesen. Nicht behandelte Themen Windows ist ein umfangreiches und vielschichtiges Betriebssystem. In diesem Buch können wir nicht sämtliche internen Mechanismen von Windows abdecken, sondern konzentrieren uns auf xviii Einleitung
die grundlegenden Systemkomponenten. Beispielsweise werden COM+, die verteilte objekt orientierte Programmierinfrastruktur von Windows, und Microsoft .NET Framework, die Grundlage für Anwendungen mit verwaltetem Code, in diesem Buch nicht behandelt. Da es ein Buch über die »Interna« von Windows ist und kein Leitfaden für die Benutzung, die Programmierung oder die Systemadministration, erfahren Sie hier auch nicht, wie Sie Windows verwenden, programmieren oder konfigurieren. Warnung Dieses Buch beschreibt nicht dokumentierte Aspekte der internen Architektur und Funktionsweise von Windows (wie interne Kernelstrukturen und -funktionen), die sich zwischen den einzelnen Releases ändern können. Das soll nicht heißen, dass sie sich zwangsläufig ändern, sondern nur, dass Sie sich nicht auf ihre Unveränderbarkeit verlassen können. Software, die sich auf solche nicht dokumentierten Schnittstellen oder auf Insiderkenntnisse über das Betriebssystem verlässt, kann in zukünftigen Releases von Windows unter Umständen nicht mehr funktionieren. Software, die im Kernelmodus läuft (z. B. Gerätetreiber) und diese nicht dokumentierten Schnittstellen nutzt, kann bei der Ausführung in einer neueren Windows-Version sogar einen Systemabsturz und damit möglicherweise einen Datenverlust für den Benutzer hervorrufen. Kurz: Bei der Entwicklung von Software für Endbenutzersysteme und allgemein für jegliche Zwecke außer Forschung und Dokumentation sollten Sie niemals auf die in diesem Buch erwähnten internen Windows-Einrichtungen, Registrierungsschlüssel, Verhaltensweisen, APIs oder sonstigen nicht dokumentierten Elemente zurückgreifen. Suchen Sie immer erst auf MSDN (Microsoft Software Developer Network) nach einer offiziellen Dokumentation. Zielgruppe In diesem Buch wird vorausgesetzt, dass die Leser mit Windows auf dem Niveau von »Power- Usern« vertraut sind und Grundkenntnisse über Betriebssystem- und Hardwareelemente wie CPU-Register, Arbeitsspeicher, Prozesse und Threads mitbringen. In einigen Abschnitten sind auch Grundkenntnisse über Funktionen, Zeiger und ähnliche Konstrukte der Programmiersprache C von Nutzen. Der Aufbau dieses Buches Ebenso wie die sechste Ausgabe ist auch diese in zwei Bände aufgeteilt, von denen Sie den ersten in Händen halten. QQ Kapitel 1, »Begriffe und Werkzeuge«, gibt eine allgemeine Einführung in die Grundbegriffe von Windows und in die wichtigsten Werkzeuge, die wir in diesem Buch einsetzen werden. Es ist sehr wichtig, dieses Kapitel als erstes zu lesen, da es die Hintergrundinformationen für das Verständnis des Restes gibt. Einleitung xix
QQ Kapitel 2, »Systemarchitektur«, zeigt die Architektur und die Hauptkomponenten auf, aus denen Windows besteht, und erläutert sie. Mehrere dieser Komponenten werden in den folgenden Kapiteln noch ausführlicher beschrieben. QQ Kapitel 3, »Prozesse und Jobs«, erklärt, wie Prozesse in Windows implementiert sind, und zeigt die verschiedenen Vorgehensweisen zu ihrer Bearbeitung auf. Des Weiteren werden Jobs beschrieben, die eine Möglichkeit bieten, um eine Gruppe von Prozessen zu steuern und die Containerunterstützung von Windows zu aktivieren. QQ Kapitel 4, »Threads«, beschreibt ausführlich, wie Threads verwaltet, geplant und auf sonstige Weise in Windows bearbeitet werden. QQ Kapitel 5, »Speicherverwaltung«, zeigt, wie der Speicher-Manager physischen und virtuellen Arbeitsspeicher verwendet, und erklärt die verschiedenen Möglichkeiten, mit denen Prozesse und Treiber den Arbeitsspeicher bearbeiten und nutzen können. QQ Kapitel 6, »Das E/A-System«, beschreibt, wie das E/A-System von Windows funktioniert und wie es mit den Gerätetreibern zusammenwirkt, um die Mechanismen für E/A-Peripheriegeräte bereitzustellen. QQ Kapitel 7, »Sicherheit«, beschreibt ausführlich die verschiedenen Sicherheitsmechanismen von Windows einschließlich systemeigener Schutzmaßnahmen gegen Exploits. Schreibweisen In diesem Buch gelten die folgenden Schreibweisen: QQ Fettschrift wird für Text verwendet, den Sie in einer Schnittstelle eingeben müssen. QQ Kursivschrift kennzeichnet neue Begriffe sowie die Bezeichnungen von GUI-Elementen, Dateinamen und Webadressen. QQ Codeelemente stehen in nichtproportionaler Schrift. QQ Bei Tastaturkürzeln steht ein Pluszeichen zwischen den Bezeichnungen der einzelnen Tasten. Beispielsweise bedeutet (Strg) + (Alt) + (Entf), dass Sie gleichzeitig die Tasten (Strg), (Alt) und (Entf) drücken müssen. Begleitmaterial Damit Sie aus diesem Buch noch größeren Nutzen ziehen können, haben wir Begleitmaterial zusammengestellt, das Sie von der folgenden Seite herunterladen können: https://dpunkt.de/windowsinternals1 Den Quellcode der Tools, die wir eigens für dieses Buch geschrieben haben, finden Sie auf https://github.com/zodiacon/windowsinternals. xx Einleitung
Danksagungen Als Erstes möchten wir Pavel Yosifovich dafür danken, dass er sich uns für dieses Projekt angeschlossen hat. Seine Beteiligung war für die Veröffentlichung entscheidend. Die vielen Nächte, die er damit zugebracht hat, Einzelheiten von Windows zu studieren und über Änderungen während der Zeit von sechs Releases zu schreiben, haben dieses Buch erst möglich gemacht. Ohne die Rückmeldungen und die Unterstützung von wichtigen Mitgliedern des Windows-Entwicklungsteams und anderer Fachleute von Microsoft wäre dieses Buch auch technisch nicht so tief schürfend und nicht so präzise geworden, wie es ist. Daher möchten wir den folgenden Personen danken, die es auf technische Korrektheit durchgesehen, Informa tionen gegeben oder die Autoren auf andere Weise unterstützt haben: Akila Srinivasan, Alessandro Pilotti, Andrea Allievi, Andy Luhrs, Arun Kishan, Ben Hillis, Bill Messmer, Chris Kleynhans, Deepu Thomas, Eugene Bak, Jason Shirk, Jeremiah Cox, Joe Bialek, John Lambert, John Lento, Jon Berry, Kai Hsu, Ken Johnson, Landy Wang, Logan Gabriel, Luke Kim, Matt Miller, Matthew Woolman, Mehmet Iyigun, Michelle Bergeron, Minsang Kim, Mohamed Mansour, Nate Warfield, Neeraj Singh, Nick Judge, Pavel Lebedynskiy, Rich Turner, Saruhan Karademir, Simon Pope, Stephen Finnigan und Stephen Hufnagel. Wir möchten auch Ilfak Guilfanov von Hex-Rays (http://www.hex-rays.com) für die IDA Pro Advanced- und Hey-Rays-Lizenzen danken, die er Alex Ionescu vor mehr als einem Jahrzehnt zur Verfügung gestellt hat, um das Reverse-Engineering des Windows-Kernels zu beschleunigen, und für die fortlaufende Unterstützung und Entwicklung der Decompilerfeatures, die es überhaupt erst möglich gemacht haben, ein solches Buch ohne Zugriff auf den Quellcode zu schreiben. Zu guter Letzt möchten die Autoren den großartigen Mitarbeitern bei Microsoft Press denken, die dieses Buch verwirklicht haben. Devon Musgrave hat zum letzten Mal als unser Akquiseleiter gearbeitet, während Kate Shoup den Titel als Projektleiterin beaufsichtigt hat. Auch Shawn Morningstar, Kelly Talbot und Corina Lebegioara haben zu der Qualität dieses Buches beigetragen. Errata Wir haben uns gewissenhaft darum bemüht, dieses Buch und das Begleitmaterial so korrekt wie möglich zu gestalten. Jegliche Fehler in der englischen Originalausgabe, die seit der Veröffentlichung des Buches gemeldet wurden, sind auf der Website von Microsoft Press unter folgender Adresse aufgeführt: https://aka.ms/winint7ed/errata Mit Anmerkungen, Fragen oder Verbesserungsvorschlägen zu diesem Buch können Sie sich aber auch an den deutschen dpunkt.verlag wenden: hallo@dpunkt.de Bitte beachten Sie, dass über unsere E-Mail-Adressen kein Support für Software und Hardware angeboten wird. Für Supportinformationen bezüglich der Soft- und Hardwareprodukte besuchen Sie die Microsoft-Website http://support.microsoft.com. Einleitung xxi
Kontakt Bei Microsoft Press steht Ihre Zufriedenheit an oberster Stelle. Daher ist Ihr Feedback für uns sehr wichtig. Lassen Sie uns auf dieser englischsprachigen Website wissen, wie Sie dieses Buch finden: https://aka.ms/tellpress Wir wissen, dass Sie viel zu tun haben. Darum finden Sie auf der Webseite nur wenige Fragen. Ihre Antworten gehen direkt an das Team von Microsoft Press. (Es werden keine persönlichen Informationen abgefragt.) Im Voraus vielen Dank für Ihre Unterstützung. Über Ihr Feedback per E-Mail freut sich außerdem der dpunkt.verlag über: hallo@dpunkt.de Falls Sie News, Updates usw. zu Microsoft Press-Büchern erhalten möchten, können Sie uns auf Twitter folgen: http://twitter.com/MicrosoftPress (Englisch) https://twitter.com/dpunkt_verlag (Deutsch) xxii Einleitung
K apite l 1 Begriffe und Werkzeuge In diesem Kapitel führen wir Kernbegriffe des Betriebssystems Microsoft Windows ein, die wir in dem gesamten Buch verwenden werden, z. B. Windows-API, Prozesse, Threads, virtueller Speicher, Kernel- und Benutzermodus, Objekte, Handles, Sicherheit und Registrierung. Außerdem stellen wir die Werkzeuge vor, mit denen Sie die inneren Mechanismen von Windows erkunden können, darunter den Kernel-Debugger, die Leistungsüberwachung und die wichtigsten Werkzeuge aus Windows Sysinternals (http://www.microsoft.com/technet/sysinternals). Des Weiteren erklären wir, wie Sie das Windows Driver Kit (WDK) und das Windows Software Developer Kit (SDK) verwenden können, um mehr über Windows-Interna zu erfahren. In dem Rest dieses Buches wird vorausgesetzt, dass Sie die in diesem Kapitel eingeführten Begriffe kennen. Versionen des Betriebssystems Windows In diesem Buch geht es um die neueste Version der Client- und Serverbetriebssysteme von Microsoft, nämlich Windows 10 (eine 32-Bit-Version für x86 und ARM sowie eine 64-Bit-Version für x64) und Windows Server 2016 (wovon es nur eine 64-Bit-Version gibt). Sofern nicht ausdrücklich anders angegeben, bezieht sich der Text auf alle diese Versionen. Als Hintergrundinformation finden Sie in Tabelle 1–1 die Produktnamen, internen Versionsnummern und Veröffentlichungsdaten der bisherigen Windows-Versionen. Produktname Interne Versionsnummer Veröffentlichungsdatum Windows NT 3.1 3.1 Juli 1993 Windows NT 3.5 3.5 September 1994 Windows NT 3.51 3.51 Mai 1995 Windows NT 4.0 4.0 Juli 1996 Windows 2000 5.0 Dezember 1999 Windows XP 5.1 August 2001 Windows Server 2003 5.2 März 2003 Windows Server 2003 R2 5.2 Dezember 2005 Windows Vista 6.0 Januar 2007 Windows Server 2008 6.0 (Service Pack 1) März 2008 Windows 7 6.1 Oktober 2009 Windows Server 2008 R2 6.1 Oktober 2009 → 1
Produktname Interne Versionsnummer Veröffentlichungsdatum Windows 8 6.2 Oktober 2012 Windows Server 2012 6.2 Oktober 2012 Windows 8.1 6.3 Oktober 2013 Windows Server 2012 R2 6.3 Oktober 2013 Windows 10 10.0 (Build 10240) Juli 2015 Windows 10 Version 1511 10.0 (Build 10586) November 2015 Windows 10 Version 1607 (Anniversary Update) 10.0 (Build 14393) Juli 2016 Windows Server 2016 10.0 (Build 14393) Oktober 2016 Tabelle 1–1 Windows-Betriebssysteme Beginnend mit Windows 7 scheint bei der Vergabe der Versionsnummern von einem wohldefinierten Muster abgewichen worden zu sein. Die Versionsnummer dieses Betriebssystems lautete nicht 7, sondern 6.1. Als mit Windows Vista die Versionsnummer auf 6.0 erhöht wurde, konnten manche Anwendungen das richtige Betriebssystem nicht erkennen. Das lag daran, dass die Entwickler aufgrund der Beliebtheit von Windows XP nur auf eine Überprüfung auf Hauptversionsnummern größer oder gleich 5 und Nebenversionsnummern größer oder gleich 1 durchführten. Diese Bedingung aber war bei Windows Vista nicht gegeben. Microsoft hatte die Lektion daraus gelernt und im Folgenden die Hauptversion bei 6 belassen und die Nebenversionsnummer auf mindestens 2 (größer 1) festgelegt, um solche Inkompatibilitäten zu minimieren. Mit Windows 10 wurde die Versionsnummer jedoch auf 10.0 heraufgesetzt. Beginnend mit Windows 8 gibt die Funktion GetVersionEx der Windows-API als Betriebssystemnummer unabhängig vom tatsächlich vorhandenen Betriebssystem standardmäßig 6.2 (Windows 8) zurück. (Diese Funktion ist außerdem als unerwünscht eingestuft.) Das dient zur Minimierung von Kompatibilitätsproblemen, zeigt aber auch, dass die Überprüfung der Betriebssystemversion in den meisten Fällen nicht die beste Vorgehensweise ist, weil manche Komponenten unabhängig von einem bestimmten offiziellen Windows-Release installiert werden können. Wenn Sie dennoch die tatsächliche Betriebssystemversion benötigen, können Sie sie indirekt bestimmen, indem Sie die Funktion VerifyVersionInfo oder die neuen Hilfs-APIs für die Versionsbestimmung wie IsWindows80rGreater, IsWindows8Point 10rGreater, IsWindows100rGreater, IsWindowsServer u. Ä. verwenden. Außerdem lässt sich die Betriebssystemkompatibilität im Manifest der ausführbaren Datei angeben, was die Ergebnisse dieser Funktion ändert. (Einzelheiten entnehmen Sie bitte Kapitel 8, »Systemmechanismen«, in Band 2.) Informationen über die Windows-Version können Sie mit dem Befehlszeilenwerkzeug ver oder grafisch durch Ausführen von winver einsehen. Der folgende Screenshot zeigt das Ergebnis von winver in Windows 10 Enterprise Version 1511: 2 Kapitel 1 Begriffe und Werkzeuge
Das Dialogfeld zeigt auch die Buildnummer (hier 10586.218), die für Windows-Insider hilfreich sein mag (also für diejenigen, die sich registriert haben, um Vorabversionen von Windows zu erhalten). Sie ist auch praktisch für die Verwaltung von Sicherheitsaktualisierungen, da sie zeigt, welche Patches installiert sind. Windows 10 und zukünftige Windows-Versionen Mit dem Erscheinen von Windows 10 hat Microsoft angekündigt, Windows jetzt in schnellerer Folge zu aktualisieren als zuvor. Es wird kein offizielles Windows 11 geben. Stattdessen wird Windows Update (oder ein anderes Bereitstellungsmodell) das vorhandene Windows 10 auf eine neue Version aktualisieren. Zurzeit hat es schon zwei solcher Aktualisierungen gegeben, nämlich im November 2015 (Version 1511, wobei sich die Nummer auf Jahr und Monat der Bereitstellung bezieht) und im Juli 2016 (Version 1607, die auch unter der Bezeichnung Anniversary Update vermarktet wurde). Intern wird Windows nach wie vor phasenweise weiterentwickelt. Beispielsweise trug das ursprüngliche Release von Windows 10 den Codenamen Threshold 1 und die Aktualisierung von November 2015 die Bezeichnung Threshold 2. Die nächsten drei Phasen sind Redstone 1 (Version 1607), auf die Redstone 2 und Redstone 3 folgen werden. Windows 10 und OneCore Mit den Jahren wurden verschiedene Geschmacksrichtungen von Windows entwickelt. Neben dem Standard-Windows für PCs gibt es auch das Betriebssystem für die Spielkonsole Xbox 360, bei dem es sich um eine Fork von Windows 2000 handelt. Die Variante Windows Phone 7 basierte auf Windows CE (dem Echtzeit-Betriebssystem von Microsoft). Die Pflege und Erweiterung all dieses Codes ist naturgemäß nicht einfach. Daher hat man bei Microsoft entschieden, die Kernels und die Binärdateien für die grundlegende Plattform in einer einzigen zusammenfassen. Das begann bei Windows 8 und Windows Phone 8, die bereits über einen gemeinsamen Kernel verfügten (wobei Window 8.1. und Windows Phone 8.1 auch eine gemeinsame Versionen des Betriebssystems Windows Kapitel 1 3
Windows-Laufzeit-API hatten). Mit Windows 10 ist diese Zusammenlegung abgeschlossen. Die gemeinsame Plattform heißt OneCore und läuft auf PCs, Smartphones, der Spielkonsole Xbox One, der HoloLens und auf IoT-Geräten (für das »Internet der Dinge«) wie dem Raspberry Pi 2. Diese Geräte unterscheiden sich in ihrer physischen Gestalt sehr stark voneinander, weshalb einige Merkmale nicht überall zur Verfügung stehen. So ist es beispielsweise nicht sinnvoll, für HoloLens-Geräte eine Unterstützung für Maus und Tastatur anzubieten, weshalb diese Teile in der Windows 10-Version dafür nicht vorhanden sind. Aber der Kernel, die Treiber und die Binärdateien der grundlegenden Plattform sind im Prinzip identisch (mit registrierungs- oder richtliniengestützten Einstellungen, wo dies für die Leistung oder aus anderen Gründen sinnvoll ist). Ein Beispiel für solche Richtlinien finden Sie im Abschnitt »API-Sets« in Kapitel 3, »Prozesse und Jobs«. In diesem Buch geht es um die inneren Mechanismen des OneCore-Kernels unabhängig von dem Gerät, auf dem er läuft. Die Experimente in diesem Buch sind jedoch für Desktop-Computer mit Maus und Tastatur ausgelegt, allerdings hauptsächlich aus praktischen Gründen, da es nicht einfach (und manchmal sogar unmöglich) ist, die gleichen Versuche auf Geräten wie Smartphones oder Xbox One durchzuführen. Grundprinzipien und -begriffe In den folgenden Abschnitten geben wir eine Einführung in die Grundprinzipien und -begriffe von Windows, deren Kenntnis für das Verständnis dieses Buches unverzichtbar ist. Viele der hier genannten Elemente wie Prozesse, Threads und virtueller Speicher werden in den nachfolgenden Kapiteln noch ausführlicher dargestellt. Die Windows-API Die Windows-API ist die Systemprogrammierschnittstelle der Windows-Betriebssystemfamilie für den Benutzermodus. Vor der Einführung der 64-Bit-Versionen von Windows wurde die 32-Bit-APi als Win32API bezeichnet, um sie von der ursprünglichen 16-Bit-API zu unterscheiden. Die Bezeichnung Windows-API in diesem Buch bezieht sich sowohl auf die 32- als auch die 64-Bit-Programmierschnittstelle von Windows. Manchmal verwenden wir auch den Begriff Win32API statt Windows-API. Aber auch dann ist sowohl die 32- als auch die 64-Bit-Variante gemeint. Die Windows-API wird in der Dokumentation des Windows-SDK beschrieben (siehe den Abschnitt »Windows Software Development Kit« weiter hinten in diesem Kapitel), die kostenlos auf https://developer.microsoft.com/en-us/windows/ desktop/develop zur Verfügung steht und in allen Abonnements von MSDN, dem Microsoft-Unterstützungsprogramm für Entwickler, eingeschlossen ist. Eine hervorragende Beschreibung der Programmierung für die grundlegende WindowsAPI finden Sie in dem Buch Windows via C/C++, Fifth Edition von Jeffrey Richter und Christophe Nasarre (Microsoft Press, 2007). 4 Kapitel 1 Begriffe und Werkzeuge
Varianten der Windows-API Die Windows-API bestand ursprünglich nur aus C-artigen Funktionen. Heutzutage können Entwickler auf Tausende solcher Funktionen zurückgreifen. Bei der Erfindung von Windows bot sich die Sprache C an, da sie den kleinsten gemeinsamen Nenner darstellte (weil sie auch von anderen Sprachen aus zugänglich war) und maschinennah genug, um Betriebssystemdienste bereitzustellen. Der Nachteil bestand in einer ungeheuren Menge von Funktionen ohne konsistente Benennung und logische Gruppierung (etwa durch einen Mechanismus wie die Namespaces von C++). Aufgrund dieser Schwierigkeiten wurden schließlich neuere APIs verwendet, die einen anderen API-Mechanismus einsetzten, nämlich das Component Object Model (COM). COM wurde ursprünglich entwickelt, um Microsoft Office-Anwendungen die Kommunikation und den Datenaustausch zwischen Dokumenten zu ermöglichen (z. B. die Einbettung eines Excel-Diagramms in ein Word-Dokument oder eine PowerPoint-Präsentation). Diese Fähigkeit wird als OLE (Object Linking and Embedding) bezeichnet und wurde ursprünglich mit dem alten Windows-Messagingmechanismus DDE (Dynamic Data Exchange) implementiert. Aufgrund der Einschränkungen, die DDE zu eigen waren, wurde allerdings eine neue Kommunikationsmöglichkeit entwickelt, und das war COM. Als COM um 1993 veröffentlicht wurde, hieß es sogar ursprünglich OLE 2. COM basiert auf zwei Grundprinzipien: Erstens kommunizieren Clients mit Objekten (manchmal COM-Serverobjekte genannt) über Schnittstellen. Dies sind wohldefinierte Kontrakte mit einem Satz logisch verwandter Methoden, die durch einen Zuteilungsmechanismus für virtuelle Tabellen gruppiert werden. Dies ist eine übliche Vorgehensweise von C++-Compilern zur Implementierung einer Zuteilung für virtuelle Funktionen. Sie resultiert in Binärkompatibilität und verhindert Probleme durch Namensverstümmelung im Compiler. Dadurch wird es möglich, die Methoden von vielen Sprachen (und Compilern) aufzurufen, z. B. C, C++, Visual Basic, .NET-Sprachen, Delphi usw. Das zweite Prinzip lautet, dass die Implementierung von Komponenten dynamisch geladen und nicht statisch mit dem Client verlinkt wird. Der Begriff COM-Server bezieht sich gewöhnlich auf eine DLL (Dynamic Link Library) oder eine ausführbare Datei (EXE), in der die COM-Klassen implementiert sind. COM weist noch weitere wichtige Merkmale zu Sicherheit, prozessübergreifendem Marshalling, Threading usw. auf. Eine umfassende Darstellung von COM können wir in diesem Buch nicht leisten. Eine hervorragende Abhandlung über COM finden Sie in dem Buch Essential COM von Don Box (Addison-Wesley, 1998). Über COM zugängliche APIs sind beispielsweise DirectShow, Windows Media Foundation, DirectX, DirectComposition, WIC (Windows Imaging Component) und BITS (Background Intelligent Transfer Service). Die Windows-Laufzeitumgebung Mit Windows 8 wurden eine neue API und eine neue Laufzeitumgebung eingeführt, die Windows Runtime (manchmal auch als WinRT abgekürzt; nicht zu verwechseln mit dem inzwischen eingestellten Betriebssystem Windows RT für ARM-Prozessoren). Die Windows-Laufzeitumgebung Grundprinzipien und -begriffe Kapitel 1 5
besteht aus Plattformdiensten für die Entwickler von sogenannten Windows-Apps (die früher auch als Metro Apps, Modern Apps, Immersive Apps und Windows Store Apps bezeichnet wurden). Windows-Apps können auf unterschiedlichen physischen Geräten ausgeführt werden, von kleinen IoT-Geräten über Telefone und Tablets bis zu Laptop- und Desktop-Computern und sogar auf Geräten wie die Xbox One und die Microsoft HoloLens. Aus der Sicht der API setzt WinRT auf COM auf und erweitert die grundlegende COM-In frastruktur. Beispielsweise sind in WinRT vollständige Typmetadaten verfügbar (die im .NET-Metadatenformat in WINMDF-Dateien gespeichert werden), was die COM-Typbibliotheken erweitert. Mit Namespace-Hierarchien, konsistenter Benennung und Programmiermustern ist WinRT auch viel zusammenhängender gestaltet als klassische Windows-API-Funktionen. Anders als normale Windows-Anwendungen (die jetzt als Windows-Desktop-Anwendungen oder klassische Windows-Anwendungen bezeichnet werden) unterliegen Windows-Apps neuen Regeln. Diese Regeln werden in Kapitel 9, »Verwaltungsmechanismen«, von Band 2 beschrieben. Die Beziehungen zwischen den verschiedenen APIs und Anwendungen sind nicht offensichtlich. Desktop-Anwendungen können einen Teil der WinRT-APIs verwenden, WindowsApps dagegen eine der Win32- und COM-APIs. Um jeweils genau zu erfahren, welche APIs für welche Anwendungsplattformen zur Verfügung stehen, schlagen Sie bitte in der MSDN-Dokumentation nach. Beachten Sie jedoch, dass die WinRT-API auf der grundlegenden binären Ebene immer noch auf den älteren Binärdateien und APIs von Windows basiert, auch wenn bestimmte APIs nicht unterstützt werden und ihre Verfügbarkeit nicht dokumentiert ist. Es gibt keine neue »native« API für das System. Das ist vergleichbar mit .NET, das nach wie vor auf die traditionelle Windows-API zurückgreift. Anwendungen, die in C++, C#, anderen .NET-Sprachen oder JavaScript geschrieben sind, können WinRT-APIs dank der für diese Plattformen entwickelten Sprachprojektionen leicht nutzen. Für C++ hat Microsoft eine nicht standardmäßige Erweiterung namens C++/CX erstellt, die die Verwendung von WinRT-Typen vereinfacht. Die normale COM-Interop-Schicht für .NET (zusammen mit einigen unterstützenden Laufzeiterweiterungen) erlaubt allen .NET-Sprachen die natürliche und einfache Nutzung von WinRT-APIs wie in reinem .NET. Für JavaScript-Entwickler gibt es die Erweiterung WinJS für den Zugriff auf WinRT. Für die Gestaltung der Benutzeroberfläche müssen JavaScript-Entwickler allerdings nach wie vor HTML einsetzen. HTML kann zwar in Windows-Apps eingesetzt werden, allerdings handelt es sich dabei immer noch um eine lokale Client-App und nicht um eine von einem Webserver abgerufene Webanwendung. .NET Framework .NET Framework gehört zu Windows. Tabelle 1–2 gibt an, welche Version davon jeweils in den einzelnen Windows-Versionen installiert ist. Es ist jedoch immer möglich, auch eine neuere Version zu installieren. 6 Kapitel 1 Begriffe und Werkzeuge
Windows-Version .NET Framework-Version Windows 8 4.5 Windows 8.1 4.5.1 Windows 10 4.6 Windows 10 Version 1511 4.6.1 Windows 10 Version 1607 4.6.2 Tabelle 1–2 .NET Framework-Standardinstallation in Windows .NET Framework besteht aus den beiden folgenden Hauptkomponenten: QQ CLR (Common Language Runtime) Dies ist die Laufzeit-Engine für .NET. Sie umfasst einen JIT-Compiler (Just In Time), der CIL-Anweisungen (Common Intermediate Language) in die Maschinensprache der CPU übersetzt, einen Garbage Collector, Typverifizierung, Codezugriffssicherheit usw. Sie ist als In-Process-COM-Server (DLL) implementiert und nutzt verschiedene Einrichtungen, die von der Windows-API bereitgestellt werden. QQ .NET Framework-Klassenbibliothek (FCL) Dies ist eine umfangreiche Sammlung von Typen für die Funktionalität, die Client- und Serveranwendungen gewöhnlich benötigen, z. B. Benutzerschnittstellendienste, Netzwerkzugriff, Datenbankzugriff usw. Durch diese und weitere Merkmale, darunter neue High-Level-Programmiersprachen (C#, Visual Basic, F#) und Unterstützungswerkzeuge, steigert .NET Framework die Produktivität von Entwicklern und erhöht Sicherheit und Zuverlässigkeit der Anwendungen. Abb. 1–1 zeigt die Beziehung zwischen .NET Framework und dem Betriebssystem. Benutzermodus (verwalteter Code) .NET-App (EXE) FCL (.NET-Assemblies) (DLLs) Benutzermodus (nativer Code) CLR (COM-DLL-Server) Windows-API-DLLs Kernelmodus Abbildung 1–1 Windows-Kernel Die Beziehung zwischen .NET und Windows Dienste, Funktionen und Routinen Mehrere Begriffe in der Windows-Dokumentation für Benutzer und für Programmierer haben je nach Kontext eine unterschiedliche Bedeutung. Beispielsweise kann es sich bei einem Dienst um eine aufrufbare Routine im Betriebssystem, um einen Gerätetreiber oder um einen Server- Grundprinzipien und -begriffe Kapitel 1 7
prozess handeln. Die folgende Aufstellung gibt an, was in diesem Buch mit den genannten Begriffen gemeint ist: QQ Windows-API-Funktionen Dies sind dokumentierte, aufrufbare Subroutinen in der Windows-API, beispielsweise CreateProcess, CreateFile und GetMessage. QQ Native Systemdienste (Systemaufrufe) Dies sind nicht dokumentierte, zugrunde liegende Dienste im Betriebssystem, die vom Benutzermodus aus aufgerufen werden können. Beispielsweise ist NtCreateUserProcess der interne Systemdienst, den die Windows-Funk tion CreateProcess aufruft, um einen neuen Prozess zu erstellen. QQ Kernelunterstützungsfunktionen (Routinen) Dies sind Subroutinen in Windows, die ausschließlich im Kernelmodus aufgerufen werden können (der weiter hinten in diesem Kapitel erklärt wird). Beispielsweise ist ExAllocatePoolWithTag die Routine, die Gerätetreiber aufrufen, um Speicher von den Windows-Systemheaps (den sogenannten Pools) zuzuweisen. QQ Windows-Dienste Dies sind Prozesse, die vom Dienststeuerungs-Manager von Windows gestartet werden. Beispielsweise wird der Taskplaner-Dienst in einem Benutzermodusprozess ausgeführt, der den Befehl schtasks unterstützt (der den UNIX-Befehlen at und cron ähnelt). (In der Registrierung werden Windows-Gerätetreiber zwar als »Dienste« bezeichnet, nicht aber in diesem Buch.) QQ DLLs (Dynamic Link Libraries) Eine DLL ist eine Binärdatei, zu der aufrufbare Subroutinen verknüpft worden sind und die von Anwendungen, die diese Subroutinen nutzen, dynamisch geladen werden kann, beispielsweise Msvcrt.dll (die C-Laufzeitbibliothek) und Kernel32.dll (eine der Teilsystem-Bibliotheken der Windows-API). Komponenten des Windows-Benutzermodus und Anwendungen nutzen DLLs ausgiebig. Der Vorteil von DLLs gegenüber statischen Bibliotheken besteht darin, dass Anwendungen sie gemeinsam nutzen, wobei Windows dafür sorgt, dass es im Arbeitsspeicher nur ein einziges Exemplar des DLL-Codes gibt, auf die alle Anwendungen zugreifen. Bibliotheks-Assemblies von .NET werden als DLLs kompiliert, aber ohne nicht verwaltete, exportierte Subroutinen. Stattdessen parst die CLR die kompilierten Metadaten, um auf die entsprechenden Typen und Elemente zuzugreifen. Prozesse Programme und Prozesse scheinen zwar oberflächlich gesehen ähnlich zu sein, unterscheiden sich aber grundlegend voneinander. Ein Programm ist eine statische Folge von Anweisungen, wohingegen es sich bei einem Prozess um einen Container für einen Satz von Ressourcen handelt, die bei der Ausführung einer Instanz des Programms genutzt werden. Grob gesehen, besteht ein Windows-Prozess aus folgenden Elementen: QQ Privater virtueller Adressraum Dies ist ein Satz von virtuellen Speicheradressen, die der Prozess nutzen kann. QQ Ausführbares Programm Das Programm definiert den ursprünglichen Code und die Daten und wird auf den virtuellen Adressraum des Prozesses abgebildet. 8 Kapitel 1 Begriffe und Werkzeuge
QQ Ein Satz offener Handles Diese Handles werden verschiedenen Systemressourcen zugeordnet, z. B. Semaphoren, Synchronisierungsobjekten und Dateien, und sind von allen Threads im Prozess aus zugänglich. QQ Sicherheitskontext Hierbei handelt es sich um ein Zugriffstoken, das den Benutzer, die Sicherheitsgruppen, die Rechte, Attribute, Claims und Fähigkeiten, den Virtualisierungsstatus der Benutzerkontensteuerung, die Sitzung und den Zustand des eingeschränkten Benutzerzugriffs angibt, die mit dem Prozess verbunden sind. Des Weiteren legt der Kontext den Bezeichner des Anwendungscontainers fest und enthält Angaben über die zugehörige Sandbox. QQ Prozesskennung Dieser eindeutige Bezeichner ist Teil eines weiteren Bezeichners, nämlich der Clientkennung. QQ Mindestens ein Ausführungsthread tens) nicht sinnvoll. Ein »leerer« Prozess ist zwar möglich, aber (meis- Es gibt eine Reihe von Werkzeugen, um Prozesse und Prozessinformationen einzusehen und zu bearbeiten. Die folgenden Experimente zeigen die verschiedenen Ansichten von Prozessinformationen, die Sie mit einigen dieser Tools gewinnen können. Manche dieser Werkzeuge sind in Windows selbst, im Debugging Tool for Windows oder im Windows SDK enthalten, während es sich bei anderen um eigenständige Programme von Sysinternals handelt. Viele dieser Werkzeuge zeigen die gleichen Kerninformationen über Prozesse und Threads an, manchmal jedoch unter unterschiedlichen Bezeichnungen. Das wahrscheinlich am häufigsten verwendete Werkzeug zur Untersuchung von Prozessaktivitäten ist der Task-Manager. (Der Name ist allerdings etwas eigenartig, da es im Windows-Kernel keine »Tasks« gibt.) Das folgende Experiment führt einige der Grundfunktionen dieses Programms vor. Experiment: Prozessinformationen im Task-Manager einsehen Der im Lieferumfang von Windows enthaltene Task-Manager gibt eine Übersicht über die Prozesse im System. Sie können ihn auf vier verschiedene Weisen starten: QQ Drücken Sie (Strg) + (Umschalt) + (Esc). QQ Rechtsklicken Sie auf die Taskleiste und wählen Sie Task-Manager starten. QQ Drücken Sie (Strg) + (Alt) + (Entf) und klicken Sie auf die Schaltfläche Task-Manager starten. QQ Starten Sie die ausführbare Datei Taskmgr.exe. → Grundprinzipien und -begriffe Kapitel 1 9
Wenn Sie den Task-Manager zum ersten Mal starten, befindet er sich im Übersichtsmodus, in dem nur die Prozesse angezeigt werden, für die es ein sichtbares Fenster gibt: In diesem Fenster können Sie allerdings nicht viel sehen. Klicken Sie daher auf die Erweiterungsschaltfläche Details, um in die Vollansicht zu wechseln. Dabei liegt standardmäßig die Registerkarte Prozesse oben: Die Registerkarte Prozesse zeigt eine Liste der Prozesse mit vier Spalten: CPU, Arbeitsspeicher, Datenträger und Netzwerk. Wenn Sie auf die Kopfzeile rechtsklicken, können Sie noch weitere Spalten einblenden lassen. Zur Verfügung stehen Prozessname (Abbild), Prozess-ID, Typ, Status, Herausgeber und Befehlszeile. Einige Prozesse können erweitert werden, sodass Sie erkennen können, welche sichtbaren Fenster von ihnen erstellt werden. Um noch mehr Einzelheiten zu den Prozessen einzusehen, klicken Sie auf die Registerkarte Details. Sie können auch auf einen Prozess rechtsklicken und Zu Details wechseln auswählen. Dadurch gelangen Sie auf die Registerkarte Details, wobei der betreffende Prozess schon ausgewählt ist. → 10 Kapitel 1 Begriffe und Werkzeuge
Die Registerkarte Prozesse im Task-Manager von Windows 7 entspricht ungefähr der Registerkarte Details im Task-Manager von Windows 8 und höher. Im Task-Manager von Windows 7 zeigt die Registerkarte Anwendungen nur Prozesse mit sichtbaren Fenstern, und nicht sämtliche Prozesse. Diese Angaben sind jetzt auf der Registerkarte Prozesse des neuen Task-Managers von Windows 8 und höher enthalten. Die Registerkarte Details zeigt ebenfalls Prozesse, aber auf kompaktere Art und Weise. Sie gibt die von den Prozessen erstellten Fenster nicht an, enthält aber eine breitere Auswahl von Informationsspalten. Prozesse werden anhand ihres Namens bezeichnet und mit einem Symbol dargestellt, das angibt, wovon sie eine Instanz darstellen. Anders als viele andere Objekte in Windows können Prozesse keine globalen Namen bekommen. Um weitere Einzelheiten einzublenden, rechtsklicken Sie auf die Kopfzeile und wählen Spalten auswählen. Dadurch wird eine Liste von Spalten angezeigt: → Grundprinzipien und -begriffe Kapitel 1 11
Unter anderem gibt es folgende wichtige Spalten: QQ Threads Die Spalte Threads zeigt die Anzahl der Threads in jedem Prozess. Normalerweise sollte sie mindestens eins betragen, da es keine unmittelbare Möglichkeit gibt, um einen Prozess ohne Thread zu erstellen (und ein solcher Prozess auch nicht sehr sinnvoll wäre). Werden für einen Prozess null Threads angezeigt, so bedeutet das gewöhnlich, dass der Prozess aus irgendeinem Grund nicht gelöscht werden kann, wahrscheinlich, weil der Treibercode fehlerhaft ist. QQ Handles Die Spalte Handles zeigt die Anzahl der Handles von Kernelobjekten, die die Threads in dem Prozess geöffnet haben. (Dieser Vorgang wird weiter hinten in diesem Kapitel sowie ausführlich in Kapitel 8 von Band 2 beschrieben.) QQ Status Die Spalte Status ist etwas knifflig. Bei Prozessen ohne Benutzerschnittstelle sollte hier normalerweise Wird ausgeführt angezeigt werden, wobei es jedoch sein kann, dass alle Threads auf irgendetwas warten, etwa auf ein Signal an ein Kernelobjekt oder den Abschluss einer E/A-Operation. Die andere Möglichkeit für einen solchen Prozess ist Angehalten, was angezeigt wird, wenn alle Threads in dem Prozess angehalten sind. Es ist sehr unwahrscheinlich, dass dies durch den Prozess selbst verursacht wird, lässt sich aber programmgesteuert durch den Aufruf der nicht dokumentierten nativen API NtSuspendProcess für den Prozess erreichen, gewöhnlich mithilfe eines Tools (beispielsweise gibt es in dem weiter hinten beschriebenen Process Explorer eine solche Option). Für Prozesse mit Benutzerschnittstelle bedeutet der Statuswert Wird ausgeführt, dass die Benutzerschnittstelle reagiert. Mit anderen Worten, der Thread, der die Fenster erstellt hat, wartet auf Benutzereingaben (technisch gesehen auf die mit ihm verbundene Nachrichtenwarteschlange). Der Zustand Angehalten ist ebenso möglich wie bei Prozessen ohne Benutzerschnittstelle, allerdings tritt er bei Windows-Apps (also denen mit Windows Runtime) gewöhnlich auf, wenn die Anwendung vom Benutzer minimiert wird und dadurch ihren Vordergrundstatus einbüßt. Solche Prozesse werden nach fünf Sekunden angehalten, damit sie keine CPU- und Netzwerkressourcen verbrauchen. Dadurch kann die neue Vordergrundanwendung alle Computerressourcen erhalten. Das ist insbesondere auf akkubetriebenen Geräten wie Tablets und Smartphones wichtig. Dieser und ähnliche Mechanismen werden ausführlich in Kapitel 9 von Band 2 beschrieben. Der dritte mögliche Wert für Status lautet Reagiert nicht. Das kann geschehen, wenn ein Thread in dem Prozess für die Benutzerschnittstelle seine Nachrichtenwarteschlange für Aktivitäten im Zusammenhang mit der Schnittstelle mindestens fünf Sekunden lang nicht überprüft hat. Der Prozesse (eigentlich der Thread, dem das Fenster gehört) kann mit irgendwelchen CPU-intensiven Arbeiten beschäftigt sein oder auf etwas völlig anderes warten (z. B. auf den Abschluss einer E/A-Operation). In jedem Fall friert die Benutzeroberfläche ein, was Windows dadurch kennzeichnet, dass die fraglichen Fenster blasser dargestellt und ihren Titeln der Zusatz (reagiert nicht) hinzugefügt wird. 12 Kapitel 1 Begriffe und Werkzeuge
Jeder Prozess verweist außerdem auf seinen Elternprozess (bei dem es sich um seinen Erstellerprozess handeln kann, aber nicht muss). Existiert der Elternprozess nicht mehr, so wird diese Angabe auch nicht mehr aktualisiert. Daher kann es sein, dass ein Prozess auf einen Elternprozess verweist, den es nicht mehr gibt. Das ist jedoch kein Problem, da es nichts gibt, was eine aktuelle Angabe darüber benötigt. Dieses Verhalten wird durch das folgende Experiment veranschaulicht. (Im Tool Process Explorer wird der Startzeitpunkt des Elternprozesses berücksichtigt, um zu verhindern, dass ein Kindprozess auf der Grundlage einer wiederverwendeten Prozesskennung angefügt wird.) Wann ist der Elternprozess nicht mit dem Erstellerprozess identisch? Bei einigen Prozessen, die von bestimmten Benutzeranwendungen erstellt werden, kann die Hilfe eines Makler- oder Hilfsprozesses erforderlich sein, der die API zur Prozesserstellung aufruft. In einem solchen Fall wäre es verwirrend (oder sogar falsch, wenn eine Vererbung von Handles oder Adressräumen erforderlich ist), den Maklerprozess als Ersteller anzuzeigen, weshalb eine Umdefinition des Elternprozesses erfolgt. Mehr darüber erfahren Sie in Kapitel 7, »Sicherheit«. Experiment: Die Prozessstruktur einsehen Die meisten Werkzeuge zeigen für einen Prozess nicht die ID des Eltern- oder des Erstellerprozesses an. Diesen Wert können Sie jedoch in der Leistungsüberwachung (oder programmgesteuert) gewinnen, indem Sie Prozesskennung erstellen abrufen (was eigentlich »Kennung des erstellenden Prozesses« heißen müsste). In Tlist.exe aus den Debugging Tools for Windows können Sie die Prozessstruktur mithilfe des Optionsschalters /t anzeigen lassen. Im Folgenden sehen Sie eine Beispielausgabe von tlist /t: System Process (0) System (4) smss.exe (360) csrss.exe (460) wininit.exe (524) services.exe (648) svchost.exe (736) unsecapp.exe (2516) WmiPrvSE.exe (2860) WmiPrvSE.exe (2512) RuntimeBroker.exe (3104) SkypeHost.exe (2776) ShellExperienceHost.exe (3760) Windows Shell Experience Host ApplicationFrameHost.exe (2848) OleMainThreadWndName SearchUI.exe (3504) Cortana WmiPrvSE.exe (1576) → Grundprinzipien und -begriffe Kapitel 1 13
TiWorker.exe (6032) wuapihost.exe (5088) svchost.exe (788) svchost.exe (932) svchost.exe (960) svchost.exe (976) svchost.exe (68) svchost.exe (380) VSSVC.exe (1124) svchost.exe (1176) sihost.exe (3664) taskhostw.exe (3032) Task Host Window svchost.exe (1212) svchost.exe (1636) spoolsv.exe (1644) svchost.exe (1936) OfficeClickToRun.exe (1324) MSOIDSVC.EXE (1256) MSOIDSVCM.EXE (2264) MBAMAgent.exe (2072) MsMpEng.exe (2116) SearchIndexer.exe (1000) SearchProtocolHost.exe (824) svchost.exe (3328) svchost.exe (3428) svchost.exe (4400) svchost.exe (4360) svchost.exe (3720) TrustedInstaller.exe (6052) lsass.exe (664) csrss.exe (536) winlogon.exe (600) dwm.exe (1100) DWM Notification Window explorer.exe (3148) Program Manager OneDrive.exe (4448) cmd.exe (5992) C:\windows\system32\cmd.exe - tlist /t conhost.exe (3120) CicMarshalWnd tlist.exe (5888) SystemSettingsAdminFlows.exe (4608) → 14 Kapitel 1 Begriffe und Werkzeuge
Die Einrückungen in der Liste zeigen die Beziehungen zwischen Eltern- und Kindprozessen. Prozesse, deren Elternprozesse nicht mehr existieren, sind nicht eingerückt (wie explorer.exe im vorstehenden Beispiel), denn selbst wenn der Großelternprozess noch vorhanden wäre, gäbe es keine Möglichkeit, diese Beziehung herauszufinden. Windows führt nur die Kennungen der Erstellerprozesse, aber keine Verbindungen zurück zu den Erstellern der Ersteller usw. Die Zahl in Klammern ist die Prozesskennung, der bei einigen Prozessen folgende Test der Titel des Fensters, das von dem Prozess angelegt wurde. Um zu beweisen, dass Windows sich nicht mehr merkt als die Kennung des Elternprozesses, führen Sie die folgenden Schritte aus: 1. Drücken Sie (Windows) + (R), geben Sie cmd ein und drücken Sie (Eingabe), um eine Eingabeaufforderung zu öffnen. 2. Geben Sie titel Parent ein, um den Titel des Fensters in Parent zu ändern. 3. Geben Sie start cmd ein, um eine zweite Eingabeaufforderung zu öffnen. 4. Geben Sie titel Child an der zweiten Eingabeaufforderung ein. 5. Geben Sie an der zweiten Eingabeaufforderung mspaint ein, um Microsoft Paint zu starten. 6. Kehren Sie wieder zu der zweiten Eingabeaufforderung zurück und geben Sie exit ein. Paint wird weiterhin ausgeführt. 7. Drücken Sie (Strg) + (Umschalt) +(Esc), um den Task-Manager zu öffnen. 8. Wenn sich der Task-Manager im Übersichtsmodus befindet, klicken Sie auf Details. 9. Klicken Sie auf die Registerkarte Prozesse. 10. Suchen Sie die Anwendung Windows Command Processor und erweitern Sie ihren Knoten. Wie im folgenden Screenshot sollten Sie jetzt den Titel Parent sehen. 11. Rechtsklicken Sie auf Windows Command Processor und wählen Sie Zu Details wechseln. → Grundprinzipien und -begriffe Kapitel 1 15
12. Rechtsklicken Sie auf diesen cmd.exe-Prozess und wählen Sie Prozessstruktur beenden. 13. Klicken Sie im Bestätigungsdialogfeld des Task-Managers auf Prozessstruktur beenden. Das Fenster mit der ersten Eingabeaufforderung verschwindet, aber das Paint-Fenster wird nach wie vor angezeigt, da es nur der Enkel des beendeten Eingabeaufforderungsprozesses ist. Da der Zwischenprozess (der Elternprozess von Paint) beendet wurde, gibt es keine Verbindung mehr zwischen dem Elternprozess und seinem Enkel. Der Process Explorer aus Sysinternals zeigt mehr Einzelheiten über Prozesse und Threads an als jedes andere verfügbare Werkzeug, weshalb wir ihn in vielen Experimenten in diesem Buch verwenden. Unter anderem gibt Ihnen Process Explorer die folgenden einzigartigen Angaben und Möglichkeiten: QQ Ein Prozesssicherheitstoken, z. B. Listen von Gruppen und Rechten und des Virtualisierungszustands QQ Hervorhebungen von Veränderungen in der Liste der Prozesse, Threads, DLLs und Handles QQ Eine Liste der Dienste in Diensthostingprozessen einschließlich Anzeigename und Beschreibung QQ Eine Liste zusätzlicher Prozessattribute wie Abschwächungsrichtlinien und deren Prozessschutzebene QQ Prozesse, die zu einem Job gehören, und Angaben zu dem Job QQ Prozesse für .NET-Anwendungen mit .NET-spezifischen Angaben wie die Aufzählung der Anwendungsdomänen, der geladenen Assemblies sowie CLR-Leistungsindikatoren QQ Prozesse für Windows Runtime (immersive Prozesse) QQ Startzeitpunkt von Prozessen und Threads QQ Vollständige Liste aller Dateien, die im Speicher zugeordnet wurden (nicht nur der DLLs) QQ Die Möglichkeit, einen Prozess oder Thread anzuhalten QQ Die Möglichkeit, einen einzelnen Thread zwangsweise zu beenden QQ Leichte Identifizierung der Prozesse, die im Laufe der Zeit die meisten CPU-Ressourcen verbrauchen Die Leistungsüberwachung kann die CPU-Nutzung eines Satzes von Prozessen darstellen, zeigt dabei aber nicht automatisch auch Prozesse an, die nach dem Start dieser Überwachungssitzung erstellt wurden. Das können Sie nur mit einer manuellen Ablaufverfolgung im binären Ausgabeformat erreichen. Des Weiteren bietet Process Explorer leichten, zentralen Zugriff auf Informationen wie die folgenden: QQ Die Prozessstruktur, wobei es möglich ist, einzelne Teile davon reduziert darzustellen QQ Offene Handles in einem Prozess, einschließlich nicht benannter Handles 16 Kapitel 1 Begriffe und Werkzeuge
QQ Liste der DLLs (und im Arbeitsspeicher zugeordneten Dateien) in einem Prozess QQ Threadaktivität in einem Prozess QQ Thread-Stacks im Benutzer- und im Kernelmodus, einschließlich der Zuordnung von Adressen zu Namen mithilfe der Dbghelp.dll, die in den Debugging Tools for Windows enthalten ist • Genauere Angabe des prozentualen CPU-Anteils mithilfe des Threadzykluszählers – eine noch bessere Darstellung der genauen CPU-Aktivität (siehe Kapitel 4, »Threads«) • Integritätsebene QQ Einzelheiten des Speicher-Managers wie Spitzenwert des festgelegten virtuellen Speichers und Grenzwerte für ausgelagerte und nicht ausgelagerte Pools des Kernelspeichers (während andere Werkzeuge nur die aktuelle Größe zeigen) Das folgende Experiment dient zur Einführung in Process Explorer. Experiment: Einzelheiten über Prozesse mit Process Explorer einsehen Laden Sie die neueste Version von Process Explorer von Sysinternals herunter und führen Sie sie aus. Das können Sie mit Standardbenutzerrechten tun, aber alternativ auch auf die ausführbare Datei rechtsklicken und Als Administrator ausführen wählen. Wenn Sie Process Explorer mit Administratorrechten ausführen, wird ein Treiber installiert, der mehr Funktionen bietet. Die folgende Beschreibung funktioniert jedoch unabhängig davon, wie Sie Process Explorer starten. Bei der ersten Ausführung von Process Explorer sollten Sie die Symbole konfigurieren, da Sie anderenfalls eine entsprechende Meldung bekommen, wenn Sie auf einen Prozess doppelklicken und die Registerkarte Threads wählen. Bei ordnungsgemäßer Konfiguration kann Process Explorer auf Symbolinformationen zugreifen, um die Symbolnamen für die Startfunktion des Threads sowie die Funktion auf seinem Aufrufstack anzuzeigen. Das ist praktisch, um zu ermitteln, was die Threads innerhalb eines Prozesses tun. Für den Zugriff auf Symbole müssen die Debugging Tools for Windows installiert sein (siehe die Beschreibung weiter hinten in diesem Kapitel). Klicken Sie auf Options, wählen Sie Configure Symbols und geben Sie den Pfad zu Dbghelp.dll im Ordner von Debugging Tools sowie einen gültigen Symbolpfad an. Wenn die Debugging Tools for Windows auf einem 64-Bit-System im Rahmen des WDK im Standardspeicherort installiert sind, können Sie beispielsweise folgende Konfiguration verwenden: → Grundprinzipien und -begriffe Kapitel 1 17
Hier wird der Symbolserver verwendet, um auf Symbole zuzugreifen, und die Symboldateien werden auf dem lokalen Computer im Ordner C:\symbols gespeichert. (Wenn Sie Speicherplatz sparen müssen, können Sie diesen Ordner auch durch einen anderen Ordner ersetzen, auch durch einen auf einem anderen Laufwerk.) Weitere Informationen darüber, wie Sie die Verwendung des Symbolservers einrichten, erhalten Sie auf https:// msdn.microsoft.com/en-us/library/windows/desktop/ee416588.aspx. Sie können den Microsoft-Symbolserver einrichten, indem Sie die Umgebungsvariable _NT_SYMBOL_PATH auf den Wert aus der vorstehenden Abbildung setzen. Mehrere Werkzeuge suchen automatisch nach dieser Variablen, darunter Process Explorer, die Debugger aus den Debugging Tools for Windows, Visual Studio u. a. Dadurch können Sie es sich ersparen, jedes Tool einzeln zu konfigurieren. Beim Start zeigt Process Explorer standardmäßig die Prozessstruktur an. Sie können den unteren Bereich erweitern, um die offenen Handles oder die im Arbeitsspeicher zugeordneten DLLs und anderen Dateien anzuzeigen (siehe auch Kapitel 5, »Speicherverwaltung«, und Kapitel 8 von Band 2). Wenn Sie den Mauszeiger über den Namen eines Prozesses halten, werden auch QuickInfos über die Prozessbefehlszeile und den Pfad eingeblendet. Bei einigen Arten von Prozessen enthält die QuickInfo auch zusätzliche Angaben, darunter die folgenden: QQ Die Dienste innerhalb eines Diensthostingprozesses (z. B. Svchost.exe) QQ Die Tasks innerhalb eines Taskhostingprozesses (z. B. TaskHostw.exe) QQ Das Ziel eines Runddl32.exe-Prozesses für Elemente der Systemsteuerung und andere Merkmale QQ Angaben zur COM-Klasse, wenn sich der Prozess in einem Dllhost.exe-Prozess befindet (auch Standard-COM+-Ersatz genannt) QQ Anbieterinformationen für WMI-Hostprozesse (Windows Management Instrumenta tion) wie WMIPrvSE.exe (siehe Kapitel 8 in Band 2) QQ Paketinformationen für Windows-Apps-Prozesse (Prozesse mit Windows Runtime, die weiter vorn in diesem Kapitel im Abschnitt »Die Windows-Laufzeitumgebung« vorgestellt wurden) → 18 Kapitel 1 Begriffe und Werkzeuge
Führen Sie die folgenden Schritte aus, um einige Grundfunktionen von Process Explorer kennenzulernen: 1. Machen Sie sich mit den farbigen Hervorhebungen vertraut: Prozesse für Dienste werden standardmäßig rosa hinterlegt, Ihre eigenen Prozesse dagegen blau. Diese Farben können Sie ändern, indem Sie das Dropdownmenü öffnen und Options > Configure Colors auswählen. 2. Halten Sie den Mauszeiger über den Imagenamen eines Prozesses. In einer QuickInfo wird der vollständige Pfad angezeigt. Wie zuvor erwähnt, enthält die QuickInfo bei bestimmten Arten von Prozessen weitere Angaben. 3. Klicken Sie auf der Registerkarte Process Image auf View und wählen Sie Select Columns, um den Imagepfad hinzuzufügen. 4. Klicken Sie auf den Kopf der Spalte Process, um die Prozesse zu sortieren. Dabei verschwindet die Strukturansicht. (Sie können nur entweder die Struktur anzeigen lassen oder eine Sortierung nach einer der Spalten durchführen.) Klicken Sie erneut auf den Kopf von Process, um eine Sortierung von Z zu A zu erreichen. Klicken Sie ein drittes Mal darauf, um wieder die Strukturansicht anzuzeigen. 5. Öffnen Sie das Menü View und deaktivieren Sie Show Processes from All Users, damit nur Ihre Prozesse angezeigt werden. → Grundprinzipien und -begriffe Kapitel 1 19
6. Klicken Sie auf das Menü Options, wählen Sie Difference Highlight Duration und ändern Sie den Wert auf drei Sekunden. Starten Sie dann einen (beliebigen) neuen Prozess. Dabei wird der neue Prozess drei Sekunden lang grün hervorgehoben. Beenden Sie den neuen Prozess. Dabei wird er drei Sekunden lang rot hervorgehoben, bevor er aus der Anzeige verschwindet. Diese Funktion ist nützlich, um sich einen Überblick über die Prozesse zu verschaffen, die auf dem System erstellt und beendet werden. 7. Doppelklicken Sie auf einen Prozess und schauen Sie sich die verschiedenen Registerkarten an, die in der Anzeige der Prozesseigenschaften zur Verfügung stehen. (In verschiedenen Experimenten in diesem Buch werden wir auf diese Registerkarten hinweisen und die darauf befindlichen Angaben erklären.) Threads Ein Thread ist eine Entität in einem Prozess, die Windows zur Ausführung einplant. Ohne einen Thread kann das Programm des Prozesses nicht ausgeführt werden. Ein Thread schließt die folgenden grundlegenden Komponenten ein: QQ Den Inhalt eines Satzes von CPU-Registern, die den Zustand des Prozessors darstellen QQ Zwei Stacks: einen für Kernel- und einen für den Benutzermodus QQ Einen privaten Speicherbereich, der als lokaler Threadspeicher (Thread-Local Storage, TLS) bezeichnet und von Teilsystemen, Laufzeitbibliotheken und DLLs verwendet wird QQ Einen eindeutigen Bezeichner, der als Threadkennung oder Thread-ID bezeichnet wird (und der zu einer internen Struktur namens Client-ID gehört; Prozess- und Thread-IDs werden aus demselben Namespace generiert, weshalb sie niemals überlappen) Darüber hinaus haben Threads manchmal auch einen eigenen Sicherheitskontext oder ein Token, das häufig von Multithread-Serveranwendungen verwendet wird, um den Sicherheitskontext der Clients anzunehmen, deren Anforderungen sie verarbeiten. Die flüchtigen Register, die Stacks und der private Speicherbereich bilden den sogenannten Kontext des Threads. Da sie sich zwischen den einzelnen Maschinenarchitekturen unterscheiden, auf denen Windows läuft, ist diese Struktur notgedrungen architekturspezifisch. Die Windows-Funktion GetThreadContext bietet Zugriff auf diese architekturspezifischen Informa tionen (den sogenannten CONTEXT-Block). Da an der Umschaltung der Ausführung von einem Thread zu einem anderen der Kernelscheduler beteiligt ist, kann dieser Vorgang sehr kostspielig sein, insbesondere wenn dies häufig zwischen immer denselben Threads geschieht. In Windows gibt es zwei Mechanismen, um die Kosten dafür zu senken, nämlich Fibers und UMS (User-Mode Scheduling). 20 Kapitel 1 Begriffe und Werkzeuge
Die Threads einer 32-Bit-Anwendung, die auf einer 64-Bit-Version von Windows läuft, enthalten sowohl 32- als auch 64-Bit-Kontexte, was Wow64 (Windows on Windows) dazu verwendet, die Anwendung bei Bedarf vom 32- in den 64-Bit-Modus umzuschalten. Diese Threads haben zwei Benutzerstacks und zwei CONTEXT-Blöcke. Die üblichen Windows-API-Funktionen geben stattdessen aber den 64-Bit-Kontext zurück, die Funktion Wow64GetThreadContext dagegen den 32-Bit-Kontext. Mehr Informationen über Wow64 erhalten Sie in Kapitel 8 in Band 2. Fibers Fibers ermöglichen es einer Anwendung, ihre eigenen Ausführungsthreads zu planen, anstatt sich auf den prioritätsgestützten Mechanismus von Windows zu verlassen. Sie werden oft auch als schlanke Threads (»lightweight threads«) bezeichnet. Da sie in Kernel32.dll im Benutzermodus implementiert sind, sind sie für den Kernel nicht sichtbar. Um Fibers zu nutzen, müssen Sie zunächst die Windows-Funktion ConvertThreadToFiber aufrufen, die den Thread in eine laufende Fiber umwandelt. Anschließend können Sie die neue Fiber nutzen, um mit der Funktion CreateFiber weitere Fibers zu erstellen. (Jede Fiber kann ihren eigenen Satz von Fibers haben.) Anderes als bei einem Thread jedoch beginnt die Ausführung einer Fiber erst dann, wenn sie über einen Aufruf der Funktion SwitchToFiber manuell ausgewählt wird. Die neue Fiber wird ausgeführt, bis sie mit einem Aufruf von SwitchToFiber beendet wird, der eine andere Fiber zur Ausführung auswählt. Weitere Informationen entnehmen Sie bitte der Dokumentation des Windows SDK über Fiberfunktionen. Es ist gewöhnlich nicht gut, Fibers zu verwenden, da sie für den Kernel nicht sichtbar sind. Außerdem gibt es bei ihnen Probleme durch die gemeinsame Nutzung des lokalen Threadspeichers (TLS), da in einem Thread mehrere Fibers laufen können. Es gibt zwar auch lokalen Fiberspeicher (FLS), der allerdings nicht alle Probleme der gemeinsamen Verwendung löst. An die E/A gebundene Fibers zeigen auch bei der Verwendung von FLS eine schlechte Leistung. Des Weiteren können Fiber nicht gleichzeitig auf mehreren Prozessoren laufen und sind auf kooperatives Multitasking beschränkt. In den meisten Situationen ist es besser, dem Windows-Kernel die Planung zu überlassen, indem Sie die geeigneten Threads für die vorliegende Aufgabe auswählen. UMS-Threads UMS-Threads (User-Mode Scheduling) stehen nur in den 64-Bit-Versionen von Windows zur Verfügung. Sie bieten die gleichen grundlegenden Vorteile wie Fibers, aber nur wenige von deren Nachteilen. UMS-Threads haben ihren eigenen Kernelthreadstatus und sind daher für den Kernel sichtbar, was es ermöglicht, dass mehrere UMS-Threads Ressourcen gemeinsam nutzen, darum konkurrieren und blockierende Systemaufrufe ausgeben. Wenn zwei oder mehr UMS-Threads Arbeit im Benutzermodus verrichten müssen, können sie regelmäßig den Ausführungskontext im Benutzermodus wechseln (durch Zurücktreten eines Threads zugunsten Grundprinzipien und -begriffe Kapitel 1 21
eines anderen), anstatt den Scheduler zu bemühen. Aus der Sicht des Kernels läuft weiterhin derselbe Kernelthread, weshalb sich für ihn nichts geändert hat. Führt ein UMS-Thread eine Operation durch, die einen Eintritt in den Kernel erfordert (z. B. einen Systemaufruf), dann schaltet er zu seinem eigenen Kernelmodusthread um (was als direkter Kontextwechsel bezeichnet wird). Es ist zwar immer noch nicht möglich, UMS-Threads gleichzeitig auf mehreren Prozessoren auszuführen, doch zumindest folgen sie einem präemptiven und nicht vollständig kooperativen Modell. Threads haben zwar ihren eigenen Ausführungskontext, doch alle Threads in einem Prozess teilen sich seinen virtuellen Adressraum (neben den anderen Ressourcen des Prozesses) und haben vollständigen Lese- und Schreibzugriff darauf. Allerdings ist es nicht möglich, dass ein Thread versehentlich auf den Adressraum eines anderen Prozesses verweist, sofern nicht dieser andere Prozess Teile seines privaten Adressraums als freigegebenen Speicherbereich verfügbar macht (was in der Windows-API als Dateizuordnungsobjekt [File Mapping Object] bezeichnet wird) oder ein Prozess das Recht hat, andere Prozesse zur Verwendung von prozessübergreifenden Speicherfunktionen zu öffnen, z. B. ReadProcessMemory und WriteProcessMemory. (Dieses Recht kann ein Prozess, der in demselben Benutzerkonto und nicht innerhalb eines Anwendungscontainers oder einer anderen Art von Sandbox läuft, standardmäßig erwerben, wenn der Zielprozess keine entsprechenden Schutzvorkehrungen aufweist.) Neben dem privaten Adressraum und einem oder mehr Threads verfügt jeder Prozess auch über einen Sicherheitskontext und einen Satz von offenen Handles für Kernelobjekte wie Dateien, freigegebene Speicherbereiche oder Synchronisierungsobjekte wie Mutexe, Ereignisse oder Semaphoren (siehe Abb. 1–2). Zugriffstoken Prozess VAD VAD VAD Handletabelle Objekt Objekt Thread Thread Thread Zugriffstoken Abbildung 1–2 Ein Prozess und seine Ressourcen Der Sicherheitskontext eines Prozesses wird jeweils in einem Objekt namens Zugriffstoken gespeichert. Es enthält die Sicherheitsidentifizierung und die Anmeldeinformationen für den Prozess. Standardmäßig haben Threads keine eigenen Zugriffstokens. Sie können allerdings eines erwerben, sodass einzelne Threads die Identität des Sicherheitskontextes eines anderes Prozesses annehmen können – auch von Prozessen auf einem anderen Windows-System –, ohne andere Threads in dem Prozess zu beeinträchtigen. (Weitere Einzelheiten zur Prozess- und Threadsicherheit erhalten Sie in Kapitel 7.) 22 Kapitel 1 Begriffe und Werkzeuge
Die VADs (Virtual Address Descriptors) sind Datenstrukturen, die der Speicher-Manager nutzt, um sich über die vom Prozess verwendeten virtuellen Adressen auf dem neuesten Stand zu halten. Diese Datenstrukturen werden in Kapitel 5 ausführlicher beschrieben. Jobs In Windows gibt es Jobs als Erweiterung des Prozessmodells. Der Hauptzweck von Jobs besteht darin, die Verwaltung und Bearbeitung von Prozessgruppen als eine Einheit zu ermöglichen. Diese Objekte erlauben es, bestimmte Attribute zu steuern, und bieten Grenzwerte für die mit ihnen verknüpften Prozesse. Sie zeichnen auch grundlegende Buchführungsinformationen für alle verknüpften Prozesse auf, sowohl die aktuellen als auch diejenigen, die bereits beendet wurden. In gewisser Hinsicht bilden Jobs einen Ersatz für den fehlenden strukturierten Prozessbaum in Windows. In manchen Aspekten sind sie jedoch leistungsfähiger als ein Prozessbaum nach Art von UNIX. Der Process Explorer kann die von einem Job verwalteten Prozesse anzeigen (standardmäßig in Braun), aber diese Funktion müssen Sie erst aktivieren. Dazu öffnen Sie das Menü Options und wählen Configure Colors. Auf den Eigenschaftenseite solcher Prozesse gibt es außerdem die zusätzliche Registerkarte Jobs, auf der Sie Informationen über das zugehörige Jobobjekt finden. Weitere Informationen über die interne Struktur von Prozessen und Jobs erhalten Sie in Kapitel 3. Threads und Algorithmen zu ihrer Planung sind Thema von Kapitel 4. Virtueller Arbeitsspeicher Windows richtet ein virtuelles Speichersystem auf der Grundlage eines linearen Adressraums ein, das jedem Prozess die Illusion gibt, über einen riesigen, privaten Adressraum für sich selbst zu verfügen. Der virtuelle Speicher bietet eine logische Sicht des Arbeitsspeichers, die nicht unbedingt mit dem physischen Aufbau übereinstimmen muss. Zur Laufzeit bildet der Speicher-Manager, unterstützt von der Hardware, die virtuellen Adressen auf die physischen Adressen ab (oder ordnet sie ihnen zu), an denen die Daten tatsächlich gespeichert werden. Durch Schutzvorkehrungen und durch Steuern dieser Zuordnung sorgt das Betriebssystem dafür, dass einzelne Prozesse nicht kollidieren und gegenseitig Betriebssystemdaten überschreiben. Da der physische Arbeitsspeicher auf den meisten Systemen viel kleiner ist als der gesamte virtuelle Arbeitsspeicher, der von den laufenden Prozessen genutzt wird, lagert der Speicher-Manager einige Speicherinhalte auf die Festplatte aus. Wenn ein Thread auf eine virtuelle Adresse zugreift, die ausgelagert wurde, lädt der Speicher-Manager die Informationen von der Festplatte zurück in den Arbeitsspeicher. Um die Auslagerung nutzen zu können, ist keinerlei Anpassung der Anwendungen erforderlichen, da die Hardware dem Speicher-Manager die Auslagerung ermöglicht, ohne dass die Prozesse oder Threads das wissen oder unterstützen müssen. Abb. 1–3 zeigt zwei Prozesse, die virtuellen Arbeitsspeicher nutzen. Teilweise ist er auf den physischen Speicher (RAM) abgebildet und teilweise ausgelagert. Beachten Sie, dass zusammenhängende virtuelle Spei- Grundprinzipien und -begriffe Kapitel 1 23
cherbereiche auf nicht zusammenhängende Bereiche im physischen Speicher abgebildet werden können. Diese Bereiche werden als Seiten bezeichnet und haben eine Standardgröße von 4 KB. Virtueller Arbeitsspeicher Physischer Arbeitsspeicher Prozess A Virtueller Arbeitsspeicher Prozess B Festplatte Abbildung 1–3 Zuweisung von virtuellem zu physischem Speicher mit Auslagerung Die Größe des virtuellen Adressraums hängt von der Hardwareplattform ab. Auf 32-Bit-x86- Systemen hat der gesamte virtuelle Adressraum eine theoretische Maximalgröße von 4 GB. Standardmäßig weist Windows die untere Hälfte dieses Adressraums (Adressen 0x00000000 bis 0x7FFFFFFF) für den privaten Speicher der Prozesse zu und die obere Hälfte (0x80000000 bis 0xFFFFFFFF) für seinen eigenen geschützten Betriebssystemspeicher. Die Zuordnungen der unteren Hälfte ändern sich mit dem jeweils ausgeführten Prozess, aber die Zuordnungen der oberen Hälfte (zumindest der Großteil davon) besteht immer aus dem virtuellen Speicher für das Betriebssystem. In Windows können auch Bootoptionen eingesetzt werden, z. B. der Qualifizierer increaseuserva aus der Bootkonfigurationsdatenbank (siehe Kapitel 5), der Prozessen für besonders gekennzeichnete Programme die Möglichkeit einräumt, bis zu 3 GB privaten Adressraum zu verwenden, wobei für das Betriebssystem 1 GB übrig bleibt. (»Besonders gekennzeichnet« bedeutet, dass im Header des ausführbaren Images das Flag für großen Adressraum gesetzt ist.) Diese Option ermöglicht Anwendungen wie Datenbankservern, größere Teile der Datenbank im Prozessadressraum zu belassen, was die Notwendigkeit der Auslagerung von Teilsichten verringert und damit die Leistung erhöht (wobei jedoch der Verlust von 1 GB für das System in manchen Fällen zu deutlichen systemweiten Leistungsverlusten führen kann). Abb. 1–4 zeigt die beiden typischen Adressraumanordnungen in 32-Bit-Versionen von Windows. (Die Option increaseuserva ermöglicht ausführbaren Images mit dem Flag für großen Adressraum, einen Bereich mit einer beliebigen Größe zwischen 2 bis 3 GB zu nutzen.) 24 Kapitel 1 Begriffe und Werkzeuge
Hohe Adressen 32 Bit (Standard) 2 GB Systemraum 2 GB Benutzerprozessraum 32 Bit (optional) 1 GB Systemraum 3 GB Benutzerprozessraum Niedrige Adressen Abbildung 1–4 Typische Adressraumaufteilungen für 32-Bit-Windows 3 GB ist zwar schon besser als 2 GB, aber das ist immer noch nicht genügend virtueller Adressraum zur Zuordnung von sehr großen Datenbanken (von mehreren Gigabytes). Um dieses Problem auf 32-Bit-Systemen anzugehen, verwendet Windows den sogenannten AWE-Mechanismus (Address Windowing Extension), der es einer 32-Bit-Anwendung möglich macht, bis zu 64 GB physischen Arbeitsspeicher zuzuweisen und dann Sichten oder Fenster auf seinen 2 GB großen virtuellen Adressenraum abzubilden. Diese Vorgehensweise bürdet die Abbildung des virtuellen Adressraums auf den physischen zwar dem Entwickler auf, erfüllt aber das Bedürfnis, mehr physischen Arbeitsspeicher direkt zuzuweisen, als in einem 32-Bit-Prozessraum abgebildet werden kann. In 64-Bit-Windows steht den Prozessen ein viel größerer Adressraum zur Verfügung, nämlich 128 TB auf Windows 8.1, Server 2012 R2 und höher. Abb. 1–5 zeigt eine vereinfachte Darstellung des 64-Bit-Systemadressraums. (Eine ausführliche Beschreibung finden Sie in Kapitel 5.) Beachten Sie, dass die angegebenen Größen nicht die von der Architektur vorgegebenen Grenzwerte darstellen. 64 Bit Adressraum entsprechen 264 oder 16 EB (wobei 1 EB gleich 1024 PB oder 1.048.576 TB ist), doch die aktuelle 64-Bit-Hardware beschränkt dies auf kleinere Werte. Der als »nicht zugeordnet« gekennzeichnete Bereich in Abb. 1–5 ist viel größer als der Bereich, der zugeordnet sein kann (auf Windows 8 ungefähr eine Million Mal größer). Die Darstellung ist also bei Weitem nicht maßstabsgerecht. 64 Bit (Windows 8) 64 Bit (Windows 8.1+) Hohe Adressen 8 TB Systemraum Nicht zugeordnet Niedrige Adressen Abbildung 1–5 8 TB Benutzerprozessraum 128 TB Systemraum Nicht zugeordnet 128 TB Benutzerprozessraum Adressraumaufteilungen für 64-Bit-Windows Die Implementierung des Speicher-Managers, die Adressabbildung und die Verwaltung des physischen Arbeitsspeichers durch Windows werden in Kapitel 5 ausführlicher beschrieben. Grundprinzipien und -begriffe Kapitel 1 25
Kernel- und Benutzermodus Um zu verhindern, dass Benutzeranwendungen auf kritische Betriebssystemdaten zugreifen und sie vielleicht sogar ändern, verwendet Windows zwei verschiedene Prozessorzugriffsmodi (auch wenn der Prozessor, auf denen Windows läuft, mehr als zwei unterstützen kann), nämlich den Benutzermodus und den Kernelmodus. Benutzeranwendungen laufen im Benutzermodus, Betriebssystemcode (wie Systemdienste und Gerätetreiber) dagegen im Kernelmodus, der Zugriff auf den gesamten Systemspeicher und alle CPU-Anweisungen gewährt. Zur Unterscheidung dieser Modi werden bei manchen Prozessoren die Begriffe Code Privilege Level und Ring Level und bei anderen Supervisor Mode und Application Mode verwendet. Unabhängig von der Bezeichnung gestatten sie dem Betriebssystemkernel höhere Rechte als Benutzeranwendungen, wodurch Betriebssystementwickler die erforderliche Grundlage bekommen, um sicherzustellen, dass eine nicht ordnungsgemäß funktionierende Anwendung die Stabilität des Gesamtsystems nicht beeinträchtigen kann. Die Architekturen von x86- und x64-Prozessoren definieren vier Rechteebenen (Ringe), um Systemcode und -daten gegen versehentliches oder böswilliges Überschreiben durch Code mit geringeren Rechten zu schützen. Windows verwendet Rechteebene 0 (Ring 0) für den Kernel- und Rechteebene 3 (Ring 3) für den Benutzermodus. Der Grund dafür, dass Windows nur zwei Ebenen nutzt, besteht darin, dass einige Hardwarearchitekturen, wie ARM oder früher MIPS/Alpha, nur zwei implementiert hatten. Die Entscheidung für diesen kleinsten gemeinsamen Nenner ermöglichte eine effizientere und portierbare Architektur, insbesondere da die anderen Ringebenen von x86/x64 nicht die gleichen Garantien bieten wie die Unterscheidung zwischen Ring 0 und Ring 3. Alle Windows-Prozesse haben zwar jeweils ihren eigenen privaten Arbeitsspeicher, doch der Kernelmoduscode des Betriebssystems und der Gerätetreiber teilen sich denselben virtuellen Adressraum. Jede Seite im virtuellen Speicher wird mit dem Zugriffsmodus gekennzeichnet, den der Prozessor haben muss, um sie zu lesen bzw. zu schreiben. Seiten im Systemadressraum sind nur vom Kernelmodus aus zugänglich, Seiten im Benutzeradressraum dagegen vom Benutzer- und vom Kernelmodus aus. Schreibgeschützte Seiten (z. B. solche, die statische Daten enthalten) sind in keinem Modus schreibbar. Auf Prozessoren mit Ausführungsschutz für den Arbeitsspeicher kann Windows außerdem Seiten mit Daten als nicht ausführbar kennzeichnen und dadurch die unabsichtliche oder böswillige Codeausführung in Datenbereichen verhindern (wenn die Funktion der Datenausführungsverhinderung eingeschaltet ist). Windows bietet keinen Schutz für privaten les- und schreibbaren Systemspeicher, der von Komponenten im Kernelmodus genutzt wird. Anders ausgedrückt, im Kernelmodus hat der Betriebssystem- und Gerätetreibercode vollständigen Zugriff auf den Systemarbeitsspeicher und kann die Windows-Sicherheit umgehen, um auf Objekte zuzugreifen. Da der Großteil des Windows-Betriebssystemcodes im Kernelmodus läuft, ist es unverzichtbar, die im Kernelmodus ausgeführten Komponenten sorgfältig zu entwerfen und zu testen, damit sie die Systemsicherheit nicht gefährden und keine Instabilitäten des Systems hervorrufen. 26 Kapitel 1 Begriffe und Werkzeuge
Aufgrund des mangelnden Schutzes ist es auch wichtig, beim Laden von Drittanbieter-Gerätetreibern besonders achtsam zu sein, insbesondere wenn diese nicht signiert sind, denn wenn sich der Treiber erst einmal im Kernelmodus befindet, hat er kompletten Zugriff auf sämtliche Betriebssystemdaten. Dieses Risiko war einer der Gründe für die Einführung eines Mechanismus zur Treibersignierung in Windows 2000. Dabei wird bei dem Versuch, einen nicht signierten Plug-&-Play-Treiber hinzuzufügen, der Benutzer gewarnt und bei entsprechender Einstellung der Vorgang blockiert. (Weitere Informationen über Treibersignierung erhalten Sie in Kapitel 6, »Das E/A-System«.) Andere Arten von Treibern sind davon jedoch nicht betroffen. Die Treiberüberprüfung hilft Treiberautoren außerdem, Bugs zu finden, die die Sicherheit oder Zuverlässigkeit gefährden, z. B. Pufferüberläufe und Speicherlecks. (Die Treiberüberprüfung wird ebenfalls in Kapitel 6 erläutert.) In der 64-Bit- und der ARM-Version von Windows 8.1 schreibt die Richtlinie zur Kernelmodus-Codesignierung (KMCS) vor, dass alle Gerätetreiber (nicht nur Plug-&-Play-Treiber) mit einem kryptografischen Schlüssel von einer der namhaften Zertifizierungsstellen signiert sein müssen. Dadurch können die Benutzer die Installation eines nicht signierten Treibers nicht erzwingen, nicht einmal mit Administratorrechten. Die Einschränkung kann einmalig manuell deaktiviert werden, um Treiber selbst zu signieren und zu testen. Dabei wird auf dem Desktophintergrund jedoch das Wasserzeichen »Test Mode« angezeigt, und es werden bestimmte Funktionen der digitalen Rechteverwaltung (DRM) ausgeschaltet. In Windows 10 hat Microsoft eine noch stärkere Änderung umgesetzt, die ein Jahr nach der Veröffentlichung im Rahmen des Anniversary Updates im Juli durchgesetzt wurde (Version 1607). Seit diesem Zeitpunkt müssen alle neuen Windows 10-Treiber von einer der beiden akzeptierten Zertifizierungsstellen mit einem SHA-2-EV-Hardwarezertifikat (Extended Validation) signiert werden. Das reguläre dateigestützte SHA-1-Zertifikat, das von 20 Stellen ausgegeben wird, reicht also nicht mehr aus. Nach der EV-Signierung muss der Hardwaretreiber außerdem durch das Systemgeräteportal (SysDev) zur Bescheinigung eingereicht werden, bei der der Treiber eine Microsoft-Signatur erhält. Der Kernel signiert nun nur noch von Microsoft signierte Windows 10-Treiber. Außer dem zuvor erwähnten Testmodus gibt es keine Ausnahmen. Treiber, die vor dem Veröffentlichungsdatum von Windows 10 (Juli 2015) signiert wurden, können vorläufig jedoch weiterhin mit der regulären Signatur geladen werden. Die strengsten Anforderungen stellt zurzeit Windows Server 2016. Die bereits genannte EV-Signierung ist nach wie vor erforderlich, aber hier reicht die anschließende reine Bescheinigungssignierung nicht mehr aus. Damit ein Windows 10-Treiber auf einem Serversystem geladen werden kann, muss er die strenge WHQL-Zertifizierung (Windows Hardware Quality Labs) im Rahmen des Hardware Compatibility Kit (HCK) durchlaufen und zur formalen Evaluierung eingereicht werden. Nur Treiber mit WHQL-Signierung – die Systemadministratoren gewisse Kompatibilitäts-, Sicherheits-, Leistungs- und Stabilitätsgarantien gibt – dürfen auf solchen Systemen geladen werden. Unter dem Strich sollte die Beschränkung der Drittanbietertreiber, die in den Kernelmodusspeicher geladen werden dürfen, für erhebliche Verbesserungen bei der Stabilität und Sicherheit sorgen. Bei einzelnen Hersteller-, Plattform- und sogar Unternehmenskonfigurationen von Win dows können diese Signierungsrichtlinien angepasst werden, z. B. durch die DeviceGuard- Technologie, die wir weiter hinten in diesem Kapitel im Abschnitt »Hypervisor« sowie in Kapitel 7 noch kurz erläutern werden. Damit kann ein Unternehmen WHQL-Signaturen auch Grundprinzipien und -begriffe Kapitel 1 27
auf Windows 10-Clientsystemen verlangen oder diese Anforderung auf Windows Server 2016-Systemen aussetzen. Wie Sie in Kapitel 2, »Systemarchitektur«, sehen werden, wechseln Benutzeranwendungen vom Benutzer- in den Kernelmodus, wenn sie einen Systemdienst aufrufen. So muss beispielsweise die Windows-Funktion ReadFile irgendwann die interne Windows-Routine aufrufen, die sich eigentlich um das Lesen der Daten in einer Datei kümmert. Da diese Routine auf interne Systemdatenstrukturen zugreift, muss sie im Kernelmodus laufen. Durch eine besondere Prozessoranweisung wird der Wechsel vom Benutzer- in den Kernelmodus ausgelöst, wobei der Prozessor in den Zuteilungscode für Systemdienste im Kernel eintritt. Dieser Code wiederum ruft die entsprechende interne Funktion in Ntoskrnl.exe oder Win32k.sys auf. Vor der Rückgabe der Steuerung an den Benutzerthread wird der Prozessormodus wieder zurück in den Benutzermodus geschaltet. Dadurch schützt das Betriebssystem sich selbst und seine Daten vor der Verwendung und Bearbeitung durch Benutzerprozesse. Der Wechsel vom Benutzer- in den Kernelmodus (und umgekehrt) wirkt sich nicht per se auf die Threadplanung aus. Ein Moduswechsel ist kein Kontextwechsel. Nähere Einzelheiten zur Systemdienstzuteilung finden Sie in Kapitel 2. Es ist daher normal, wenn ein Benutzerthread manchmal im Benutzer- und manchmal im Kernelmodus ausgeführt wird. Da der Großteil des Grafik- und Fenstersystems auch im Kernelmodus läuft, verbringen grafikintensive Anwendungen sogar mehr Zeit im Kernel- als im Benutzermodus. Das können Sie leicht beobachten, indem Sie eine grafikintensive Anwendung wie Microsoft Paint ausführen und die zeitliche Aufteilung zwischen Benutzer- und Kernelmodus anhand der in Tabelle 1–3 aufgeführten Leistungsindikatoren messen. Anspruchsvollere Anwendungen können auch modernere Technologien wie Direct2D und DirectComposition nutzen, die Massenberechnungen im Benutzermodus durchführen und nur die Oberflächenrohdaten an den Kernel senden, was die erforderliche Zeit für den Wechsel zwischen Benutzer- und Kernelmodus verringert. Objekt: Indikator Bedeutung Prozessor: Privilegierte Zeit (%) Prozentualer Anteil der Zeit, in der eine einzelne CPU (oder alle CPUs) während eines gegebenen Zeitraums im Kernelmodus lief Prozessor: Benutzerzeit (%) Prozentualer Anteil der Zeit, in der eine einzelne CPU (oder alle CPUs) während eines gegebenen Zeitraums im Benutzermodus lief Prozess: Privilegierte Zeit (%) Prozentualer Anteil der Zeit, in der die Threads in einem Prozess während eines gegebenen Zeitraums im Kernelmodus ausgeführt wurden Prozess: Benutzerzeit (%) Prozentualer Anteil der Zeit, in der die Threads in einem Prozess während eines gegebenen Zeitraums im Benutzermodus ausgeführt wurden Thread: Privilegierte Zeit (%) Prozentualer Anteil der Zeit, in der ein Thread während eines gegebenen Zeitraums im Kernelmodus ausgeführt wurde Thread: Benutzerzeit (%) Prozentualer Anteil der Zeit, in der ein Thread während eines gegebenen Zeitraums im Benutzermodus ausgeführt wurde Tabelle 1–3 28 Leistungsindikatoren im Zusammenhang mit Benutzer- und Kernelmodus Kapitel 1 Begriffe und Werkzeuge
Experiment: Kernel- und Benutzermodus Mit der Leistungsüberwachung können Sie sich ansehen, wie viel Zeit Ihr System im Kernelund im Benutzermodus jeweils verbringt. Gehen Sie dazu wie folgt vor: 1. Öffnen Sie das Startmenü und geben Sie Run Performance Monitor ein, um die Leistungsüberwachung auszuführen. (Der vollständige Befehl sollte schon angezeigt werden, bevor Sie die Eingabe abgeschlossen haben.) 2. Wählen Sie in der Struktur auf der linken Seite die Leistungsüberwachung unter Leistungs-/Überwachungstools aus. 3. Um den Standardindikator für die CPU-Gesamtzeit zu entfernen, klicken Sie auf die Schaltfläche Löschen in der Systemleiste oder drücken die Taste (Entf). 4. Klicken Sie in der Symbolleiste auf die Schaltfläche Hinzufügen (die mit dem großen Pluszeichen). 5. Erweitern Sie den Indikatorbereich Prozessor, klicken Sie auf Privilegierte Zeit (%) und bei gedrückter (Strg)-Taste auf Benutzerzeit (%). 6. Klicken Sie auf Hinzufügen und dann auf OK. 7. Öffnen Sie eine Eingabeaufforderung und geben Sie dir\\%computername\c$ /s ein, um einen Verzeichnisscan von Laufwerk C zu starten. 8. Schließen Sie das Tool, wenn Sie fertig sind. → Grundprinzipien und -begriffe Kapitel 1 29
Sie können sich diesen Überblick auch mit dem Task-Manager verschaffen. Klicken Sie dort einfach auf die Registerkarte Leistung und dann auf das CPU-Diagramm und wählen Sie Kernel-Zeiten anzeigen. Die CPU-Leistungsanzeige zeigt jetzt in einem dunkleren Hellblau die CPU-Nutzung im Kernelmodus. Um zu sehen, wie die Verteilung zwischen Benutzer- und Kernelmodus bei einzelnen Prozessen aussieht, führen Sie das Tool erneut aus, fügen aber die Prozessindikatoren Benutzerzeit (%) und Privilegierte Zeit (%) für die gewünschten Prozesse im System hinzu: 1. Starten Sie die Leistungsüberwachung ggf. neu. (Falls sie noch läuft, rechtsklicken Sie auf den Grafikbereich und wählen Sie Alle Leistungsindikatoren entfernen, um mit einer leeren Anzeige beginnen zu können.) 2. Klicken Sie in der Symbolleiste auf die Schaltfläche mit dem Pluszeichen. 3. Erweitern Sie im Bereich der verfügbaren Indikatoren den Abschnitt Prozess. 4. Wählen Sie die Indikatoren Benutzerzeit (%) und Privilegierte Zeit (%) aus. 5. Wählen Sie im Feld Instanz einige Prozesse aus (z. B. mmc, csrss und Leerlauf). 6. Klicken Sie auf Hinzufügen und dann auf OK. 7. Bewegen Sie den Mauszeiger rasch hin und her. 8. Drücken Sie (Strg) + (H), um den Hervorhebungsmodus einzuschalten. Dadurch wird der zurzeit ausgewählte Indikator schwarz hervorgehoben. 9. Scrollen Sie durch die Liste am unteren Rand der Anzeige, um die Prozesse zu erkennen, die ausgeführt wurden, als Sie die Maus bewegt haben. Achten Sie darauf, ob sie im Benutzer- oder im Kernelmodus ausgeführt wurden. Bei der Bewegung der Maus sollten Sie in der Spalte Instanz für den Prozess mmc eine Zunahme der Zeit sowohl im Kernel- als auch im Benutzermodus sehen, da der Prozess Anwendungscode im Benutzermodus ausführt und Windows-Funktionen aufruft, die im Kernelmodus laufen. Bei der Bewegung der Maus werden Sie auch eine Threadaktivität im Kernelmodus für den Prozess csrss feststellen. Ursache dafür ist der Kernelmodusthread des Windows-Teilsystems für Roheingaben von der Tastatur und die Maus, der mit diesem Prozess verknüpft ist. (Mehr über Systemthreads und Teilsysteme erfahren Sie in Kapitel 2.) Der Leerlaufprozess, der fast 100 % seiner Zeit im Kernelmodus verbringt, ist kein echter Prozess, sondern stellt nur eine Möglichkeit dar, die Leerlaufzyklen der CPU zu berücksichtigen. Wenn Windows nichts zu tun hat, so tut es das dieser Anzeige zufolge im Kernelmodus. Hypervisor Jüngste Entwicklungen bei den Anwendungs- und Softwaremodellen wie die Einführung von Clouddiensten und die weite Verbreitung von IoT-Geräten haben dazu geführt, dass Betriebssystem- und Hardwarehersteller effizientere Möglichkeiten finden müssen, um virtuelle Gastbetriebssysteme auf der Hosthardware eines Computers auszuführen, entweder um mehrere Systeme auf einer Serverfarm unterzubringen und 100 isolierte Websites auf einem einzigen Server auszuführen oder um Entwicklern zu erlauben, Dutzende von verschiedenen Betriebs- 30 Kapitel 1 Begriffe und Werkzeuge
systemvarianten zu testen, ohne spezialisierte Hardware dafür kaufen zu müssen. Der Bedarf für schnelle, effiziente und sichere Virtualisierung hat neue Modelle des Computerwesens und neue Ansichten über Software gefördert. Heute wird besondere Software – beispielsweise Docker, das in Windows 10 und Windows Server 2016 unterstützt wird – in Containern ausgeführt, um komplett isolierte virtuelle Maschinen zu bilden, die ausschließlich dafür da sind, einen einzelnen Anwendungsstack oder ein Framework auszuführen, was die Grenzen zwischen Gast und Host mehr und mehr verschwimmen lässt. Um solche Virtualisierungsdienste anzubieten, nutzen fast alle modernen Lösungen einen Hypervisor. Dabei handelt es sich um eine spezialisierte und mit hohen Rechten ausgestattete Komponente, die die Virtualisierung und Isolierung aller Ressourcen des Computers erlaubt – vom virtuellen und physischen Arbeitsspeicher über Geräteinterrupts bis zu PCI- und USB-Geräten. Ein Beispiel für einen Hypervisor ist Hyper-V, das die Hyper-V-Clientfunktionen von Windows 8.1 und höher ermöglicht. Konkurrenzprodukte wie Xen, KVM, VMware und VirtualBox haben ihre eigenen Hypervisors mit ihren jeweiligen Stärken und Schwächen. Da ein Hypervisor hohe Rechte und stärkeren Zugriff als der Kernel selbst hat, bietet er einen erheblichen Vorteil gegenüber der reinen Möglichkeit, mehrere Gastinstanzen anderer Betriebssysteme auszuführen: Er kann einzelne Hostinstanzen schützen und überwachen, um mehr Garantien zu geben, als es der Kernel vermag. In Windows 10 nutzt Microsoft jetzt den Hypervisor Hyper-V, um die neuen VBS-Dienste (Virtualization-Based Security) anzubieten: QQ DeviceGuard Bietet Hypervisor-Codeintegrität (HVCI) für eine stärkere Codesignierungsgarantie als nur mit KMCS und ermöglicht die Anpassung der Signaturrichtlinie von Windows sowohl für Benutzer- als auch für Kernelmoduscode. QQ HyperGuard visors. Schützt wichtige Datenstrukturen und Code des Kernels und des Hyper QQ CredentialGuard Verhindert den nicht autorisierten Zugriff auf Anmeldeinformationen und Geheimnisse für Domänenkonten in Kombination mit biometrischer Sicherheit. QQ ApplicationGuard Stellt eine noch stärkere Sandbox für den Browser Microsoft Edge zur Verfügung. QQ HostGuardian und ShieldedFabric Sie nutzen virtuelles TPM (v-TPM), um eine virtuelle Maschine vor der Infrastruktur zu schützen, auf der sie ausgeführt wird. Außerdem ermöglicht Hyper-V einige wichtige Vorsorgemaßnahmen des Kernels gegen Exploits und ähnliche Angriffe. Der Hauptvorteil all dieser Technologien besteht darin, dass sie anders als vorherige kernelgestützte Sicherheitsvorkehrungen nicht anfällig für bösartige oder schlecht geschriebene Treiber sind, ob signiert oder nicht. Das gibt ihnen eine hohe Widerstandsfähigkeit gegen die anspruchsvollen modernen Angriffe. Möglich wird dies durch die Implementierung von virtuellen Vertrauensebenen (Virtual Trust Levels, VTL) durch den Hypervisor. Da das normale Betriebssystem und seine Komponenten in einem weniger privilegierten Modus laufen (VTL 0), die VBS-Technologien aber auf VTL 1 (mit höheren Rechten), können sie durch Kernelmoduscode nicht beeinträchtigt werden. Der Code verbleibt daher im Rechteraum von VTL 0. In dieser Hinsicht können Sie sich VTLs als orthogonal zu den Rechteebenen des Prozessors vorstellen: Kernel- und Benutzermodus existieren innerhalb der einzelnen VTLs und der Hypervisor verwaltete die Rechte über die VTLs hinweg. Kapitel 2 behandelt weitere Grundprinzipien und -begriffe Kapitel 1 31
Einzelheiten über hypervisorunterstützte Architektur. In Kapitel 7 werden die VBS-Sicherheitsmechanismen ausführlich erläutert. Firmware Windows-Komponenten verlassen sich zunehmend auf die Sicherheit des Betriebssystems und seines Kernels, wobei sich Letzterer wiederum auf den Schutz des Hypervisors verlässt. Es stellt sich jedoch die Frage, was dafür sorgt, dass diese Komponenten sicher geladen und ihre Inhalt authentifiziert werden können. Das ist gewöhnlich die Aufgabe des Bootloaders, aber auch er benötigt irgendeine Form von Authentifizierungsprüfung. Auf diese Weise entsteht eine immer kompliziertere Vertrauenshierarchie. Was also bildet den Anfang dieser Vertrauenskette, um einen ungestörten Startvorgang zu garantieren? In modernen Systemen mit Windows 8 und höher fällt dies in die Zuständigkeit der Systemfirmware, die auf zertifizierten Systemen auf UEFI basieren muss. Als Teil des von Windows vorgeschriebenen UEFI-Standards (UEFI 2.3.1b; siehe http://www.uefi.org) muss eine sichere Bootimplementierung mit starken Garantien und Anforderungen zur Signierung von bootrelevanter Software vorhanden sein. Aufgrund dieser Verifizierung besteht die Garantie, dass Windows-Komponenten vom Anfang des Vorgangs an sicher geladen werden. Außerdem können Technologien wie TPM (Trusted Platform Module) den Prozess messen, um eine Bescheinigung (sowohl lokal als auch im Netzwerk) zu bieten. Durch Branchenpartnerschaften unterhält Microsoft eine Whitelist und eine Blacklist von UEFI-sicheren Bootkomponenten für den Fall von Bootsoftwarefehlern oder -angriffen. In Windows-Updates sind jetzt auch Firm wareaktualisierungen enthalten. Wir werden zwar erst in Kapitel 11, »Starten und Herunterfahren«, von Band 2 wieder auf Firmware kommen, doch es ist wichtig, schon hier auf die Bedeutung hinzuweisen, die sie durch ihre Garantien für die moderne Windows-Architektur bietet. Terminaldienste und mehrere Sitzungen Terminaldienste bezieht sich auf die Unterstützung, die Windows für mehrere interaktive Benutzersitzungen auf einem einzelnen System bietet. Mit den Windows-Terminaldiensten kann ein Remotebenutzer eine Sitzung auf einem anderen Computer einrichten, sich anmelden und Anwendungen auf dem Server ausführen. Der Server überträgt die grafische Benutzeroberfläche (sowie andere auswählbare Ressourcen wie Audio und die Zwischenablage) an den Client, und der Client überträgt die Benutzereingabe zurück an den Server. (Ähnlich wie das X-Window-System erlaubt Windows die Ausführung einzelner Anwendungen auf einem Serversystem so, dass nur deren Anzeige und nicht der gesamte Desktop an den Client übertragen wird.) Die erste Sitzung wird als Dienstesitzung oder Sitzung 0 angesehen und enthält Prozesse, die Systemdienste beherbergen (mehr darüber in Kapitel 9 von Band 2). Die erste Anmeldesitzung an der physischen Konsole des Computers ist Sitzung 1. Weitere Sitzungen können durch die Verwendung eines Programms für Remotedesktopverbindungen (Mstsc.exe) oder den schnellen Benutzerwechsel erstellt werden. Die Clientausgaben von Windows erlauben einem einzelnen Remotebenutzer, Verbindung mit dem Computer aufzunehmen, aber wenn schon jemand an der Konsole angemeldet ist, so 32 Kapitel 1 Begriffe und Werkzeuge
ist die Arbeitsstation gesperrt. Das bedeutet, dass System kann entweder lokal oder über das Netzwerk genutzt werden, aber nicht beides zugleich. Windows-Ausgaben mit Windows Media Center gestatten eine interaktive und bis zu vier Windows Media Center Extender-Sitzungen. Windows-Serversysteme ermöglichen zwei Remoteverbindungen zur selben Zeit. Das dient zur Erleichterung der Remoteverwaltung, also etwa die Verbindung von Verwaltungswerkzeugen, für deren Nutzung sie an dem zu verwaltenden Computer angemeldet sein müssen. Außerdem unterstützen diese Systeme bis zu zwei Remotesitzungen, falls sie entsprechend lizenziert und als Terminalserver eingerichtet sind. Alle Clientausgaben von Windows ermöglichen mehrere Sitzungen, die lokal durch den schnellen Benutzerwechsel erstellt werden. Von diesen Sitzungen kann jedoch immer nur eine auf einmal ausgeführt werden. Anstatt sich abzumelden, kann ein Benutzer auch nur seine Sitzung trennen. Dazu klickt er auf die Startschaltfläche und dann auf den aktuellen Benutzer und wählt in dem daraufhin erscheinenden Untermenü Benutzer wechseln. Er kann auch (Windows) + (L) drücken und dann unten links auf einen anderen Benutzer klicken. In jedem Fall bleibt die aktuelle Sitzung – d. h., die Prozesse, die darin laufen, und alle sitzungsweiten Datenstrukturen, die sie beschreiben – im System aktiv, während das System zum Hauptanmeldebildschirm zurückkehrt (falls es sich nicht bereits dort befindet). Wenn sich ein neuer Benutzer anmeldet, wird eine neue Sitzung erstellt. Für Anwendungen, die wissen sollen, dass sie in einer Terminalserversitzung laufen, gibt es eine Reihe von Windows-APIs, um diesen Umstand programmgesteuert zu ermitteln und um verschiedene Aspekte der Terminaldienste zu steuern. (Einzelheiten entnehmen Sie bitte dem Windows SDK und der Remote Desktop Service API.) Kapitel 2 beschreibt kurz, wie Sitzungen erstellt werden, und enthält einige Experimente, die zeigen, wie Sie sich Sitzungsinformationen mithilfe verschiedener Werkzeuge ansehen können, u. a. mit dem Kerneldebugger. Der Abschnitt »Objekt-Manager« in Kapitel 8 von Band 2 beschreibt, wie der Systemnamensraum für Objekte sitzungsweise instanziiert wird und wie Anwendungen, die über andere Instanzen auf demselben System Bescheid wissen müssen, diese Kenntnisse gewinnen können. In Kapitel 5 schließlich erfahren Sie, wie der Speicher-Manager sitzungsweite Daten einrichtet und verwaltet. Objekte und Handles Ein Kernelobjekt in Windows ist eine einzelne Laufzeitinstanz eines statisch definierten Objekttyps. Ein Objekttyp wiederum besteht aus einem im System definierten Datentyp, Funktionen für Instanzen dieses Datentyps sowie Objektattribute. Wenn Sie Windows-Anwendungen schreiben, werden Sie Prozess-, Thread-, Datei-, Ereignis- und sonstigen Objekten begegnen. All diese Objekte gehen auf maschinennähere Objekte zurück, die von Windows erstellt und verwaltet werden. Ein Prozess ist in Windows eine Instanz des Objekttyps für Prozesse, eine Datei eine Instanz des Objekttyps für Dateien usw. Ein Objektattribut ist ein Datenfeld in einem Objekt, das einen Teil des Objektzustands definiert. Beispielsweise hat ein Objekt vom Typ »Prozess« Attribute wie die Prozesskennung, eine grundlegende Planungspriorität und einen Zeiger auf ein Zugriffstokenobjekt. Objektmethoden sind die Vorkehrungen zur Bearbeitung von Objekten und lesen oder ändern gewöhnlich die Grundprinzipien und -begriffe Kapitel 1 33
Objektattribute. Beispielsweise nimmt die Methode open für einen Prozess eine Prozesskennung als Eingabe entgegen und gibt einen Zeitpunkt auf das Objekt als Ausgabe zurück. Es gibt auch den Parameter ObjectAttributes, den ein Aufrufer angibt, wenn er mit den Objekt-Manager-APIs des Kernels ein Objekt erstellt. Dieser Parameter darf nicht mit der breiteren Bedeutung des Wortes »Objektattribute« verwechselt werden. Der grundlegendste Unterschied zwischen einem Objekt und einer gewöhnlichen Datenstruktur besteht darin, dass die interne Struktur eines Objekts undurchsichtig ist. Um Daten aus einem Objekt zu gewinnen oder darin abzulegen, müssen Sie einen Objektdienst aufrufen. Es ist nicht möglich, die Daten in einem Objekt direkt zu lesen oder zu ändern. Dadurch wird die zugrunde liegende Implementierung des Objekts von dem Code, der es lediglich benutzt, getrennt, was es ermöglicht, Objektimplementierungen im Lauf der Zeit leicht zu ändern. Mithilfe des Objekt-Managers, einer Kernelkomponente, bieten Objekte eine bequeme Möglichkeit, um die folgenden wichtigen Betriebssystemaufgaben zu erfüllen: QQ Systemressourcen mit Namen versehen, die für Menschen lesbar sind QQ Ressourcen und Daten von Prozessen gemeinsam nutzen lasen QQ Ressourcen vor nicht autorisiertem Zugriff schützen QQ Verfolgen von Verweisen, wodurch das System es erkennt, wenn ein Objekt nicht mehr gebraucht wird, sodass seine Zuweisung automatisch aufgehoben werden kann Nicht alle Datenstrukturen in Windows sind Objekte. Nur Daten, die gemeinsam genutzt, geschützt, benannt oder (mithilfe von Systemdiensten) für Benutzermodusprogramme sichtbar gemacht werden müssen, werden in Objekte gestellt. Strukturen, die nur von einer Betriebssystemkomponente verwendet werden, um interne Funktionen zu implementieren, sind keine Objekte. Objekte und Handles (Verweise auf Objektinstanzen) werden ausführlicher in Kapitel 8 von Band 2 beschrieben. Sicherheit Windows wurde von Anfang an auf Sicherheit und auf die Erfüllung verschiedener formaler staatlicher und Branchensicherheitseinstufungen ausgelegt, darunter der CCITSE-Spezifikation (Common Criteria for Information Technology Security Evaluation). Eine staatlich anerkannte Sicherheitseinstufung macht ein Betriebssystem konkurrenzfähig. Natürlich sind viele der geforderten Eigenschaften für jedes Mehrbenutzersystem von Vorteil. Zu den wichtigsten Sicherheitseigenschaften von Windows gehören: QQ Bedingter (Kenntnis nur, wenn nötig) und verbindlicher Schutz für alle gemeinsam nutzbaren Systemobjekte wie Dateien, Verzeichnisse, Prozesse, Threads usw. QQ Sicherheitsüberwachung für die Verantwortlichkeit von Subjekten oder Benutzern und den von ihnen ausgelösten Aktionen 34 Kapitel 1 Begriffe und Werkzeuge
QQ Benutzerauthentifizierung bei der Anmeldung QQ Verhindern des Zugriffs eines Benutzers auf nicht initialisierte Ressourcen wie freien Arbeitsspeicher oder Festplattenplatz, deren Zuweisung ein anderer Benutzer aufgehoben hat Windows bietet drei Arten der Zugriffssteuerung für Objekte: QQ Bedingte Zugriffssteuerung Dies ist der Schutzmechanismus, an den die meisten Menschen beim Thema Betriebssystemsicherheit denken. Mit dieser Methode gewähren oder verweigern die Besitzer von Objekten (wie Dateien oder Druckern) den Zugriff für andere. Wenn sich ein Benutzer anmeldet, erhalt er einen Satz von Sicherheitsanmeldeinforma tionen oder einen Sicherheitskontext. Beim Versuch, auf ein Objekt zuzugreifen, wird dieser Sicherheitszugriff mit der Zugriffssteuerungsliste für das Objekt verglichen, um zu bestimmen, ob er über die erforderlichen Berechtigungen für die verlangte Operation verfügt. In Windows Server 2012 und Windows 8 wurde diese Art der bedingten Zugriffssteuerung durch die Einführung einer attributgestützten Zugriffssteuerung (oder »dynamischen Zugriffssteuerung«) noch verbessert. Dabei muss die Zugriffssteuerungsliste einer Ressource nicht mehr unbedingt einzelne Benutzer und Gruppen nennen, sondern kann auch die Attribute oder Claims angeben, die für den Zugriff auf die Ressource erforderlich sind, z. B. »Zugangsstufe: Top Secret« oder »Dienstalter: 10 Jahre«. Durch die Möglichkeit, SQL-Datenbanken und Schemata mit Aktive Directory durchsuchen und dadurch solche Attribute automatisch ausfüllen zu lassen, bietet diese Vorgehensweise ein erheblich eleganteres und flexibleres Sicherheitsmodell, bei dem sich Organisationen nicht mehr mühevoll um die manuelle Verwaltung von Gruppen und die Gruppenhierarchien kümmern müssen. QQ Privilegierte Zugriffssteuerung Sie ist dann erforderlich, wenn die bedingte Zugriffssteuerung nicht ausreicht. Mit dieser Methode wird dafür gesorgt, dass jemand an geschützte Objekte gelangen kann, wenn der Besitzer nicht verfügbar ist. Nehmen wir an, ein Angestellter scheidet aus einem Unternehmen aus. Der Administrator benötigt dann eine Möglichkeit, um Zugriff auf die Dateien zu bekommen, die nur für diesen Mitarbeiter zugänglich waren. In einem solchen Fall kann der Administrator unter Windows den Besitz der Datei übernehmen, um deren Rechte nach Bedarf einzurichten. QQ Verbindliche Zugriffssteuerung Diese Methode wird angewendet, wenn zusätzliche Sicherheitsvorkehrungen erforderlich sind, um Objekte zu schützen, auf die aus demselben Benutzerkonto heraus zugegriffen wird. Sie wird für verschiedene Zwecke eingesetzt, von einzelnen Aspekten der Sandboxtechnologie für Windows-Apps (siehe die nachfolgende Erörterung) über die Isolierung von Internet Explorer im geschützten Modus (und anderen Browsern) von der Benutzerkonfiguration bis zum Schutz von Objekten, die von einem Administratorkonto mit erhöhten Rechten erstellt wurden, vor dem Zugriff von Nicht- Administratorkonten ohne erhöhte Rechte. (Weitere Informationen über die Benutzerkontensteuerung erhalten Sie in Kapitel 7.) Ab Windows 8 wird eine als Anwendungscontainer (AppContainer) bezeichnete Sandbox für Windows-Apps verwendet. Sie bietet eine Trennung von anderen Anwendungscontainern und nicht zu Windows-Apps gehörenden Prozessen. Über wohldefinierte, von der WindowsLaufzeitumgebung bereitgestellte Kontrakte kann Code in Anwendungscontainern mit Maklern (nicht isolierten Prozessen, die mit den Anmeldeinformationen des Benutzers ausgeführt wer- Grundprinzipien und -begriffe Kapitel 1 35
den) und manchmal auch mit anderen Anwendungscontainern oder Prozessen kommunizieren. Ein kanonisches Beispiel dafür ist der Browser Microsoft Edge; er läuft in einem Anwendungscontainer und bietet dadurch einen besseren Schutz gegen Schadcode, der in seinen Grenzen ausgeführt wird. Des Weiteren können Drittentwickler Anwendungscontainer auf ähnliche Weise zur Isolierung eigener Anwendungen nutzen, die keine Windows-Apps sind. Das Modell der Anwendungscontainer erzwingt einen erheblichen Wandel der herkömmlichen Programmierparadigmen, weg von der traditionellen Implementierung von Einzelprozess-Anwendungen mit mehreren Threads hin zu Anwendungen mit mehreren Prozessen. Sicherheit ist in der Windows-API allgegenwärtig. Das Windows-Teilsystem implementiert die objektgestützte Sicherheit auf die gleiche Weise wie das Betriebssystem: Es schützt gemeinsam genutzte Windows-Objekte vor nicht autorisiertem Zugriff, indem es sie mit Windows-Sicherheitsdeskriptoren versieht. Wenn eine Anwendung zum ersten Mal auf ein gemeinsam genutztes Objekt zugreift, überprüft das Windows-Teilsystem, ob sie das Recht dazu hat. Bei einem positiven Ausgang dieser Sicherheitsprüfung erlaubt das Windows-Teilsystem der Anwendung, fortzufahren. Eine ausführliche Beschreibung der Sicherheit in Windows erhalten Sie in Kapitel 7. Die Registrierung Bei der Arbeit mit Windows-Betriebssystemen haben Sie vielleicht schon einmal einen Blick in die Registrierung geworfen. Es ist schlecht möglich, die internen Mechanismen von Windows zu erläutern, ohne die Registrierung zu erwähnen, denn diese Systemdatenbank enthält Informa tionen über den Start und die Konfiguration des Systems, systemweite Softwareeinstellungen, die den Betrieb von Windows steuern, die Sicherheitsdatenbank und Konfigurationseinstellungen für die einzelnen Benutzer, beispielsweise den zu verwendenden Bildschirmschoner. Außerdem gewährt die Registrierung Einblicke in flüchtige Daten im Arbeitsspeicher, darunter den aktuellen Hardwarezustand des Systems (welche Gerätetreiber geladen sind, welche Ressourcen sie nutzen usw.) und in die Windows-Leistungsindikatoren. Diese Indikatoren befinden sich zwar nicht in der Registrierung, sind aber über Registrierungsfunktionen zugänglich (wobei es jedoch für den Zugriff auf sie eine neue, bessere API gibt). Wie Sie Informationen über Leistungsindikatoren über die Registrierung gewinnen, erfahren Sie in Kapitel 9 von Band 2. Windows-Benutzer und -Administratoren müssen nur selten einen Blick in die Registrierung werfen (da sich die meisten Konfigurationseinstellungen mit Standardverwaltungsprogrammen einsehen und ändern lassen), aber dennoch bildet sie eine nützliche Informationsquelle für Windows-Interna, da sie viele Einstellungen einschließt, die sich auf die Leistung und das Verhalten des Systems auswirken. Bei der Beschreibung einzelner Komponenten finden Sie in diesem Buch auch jeweils Hinweise auf die zugehörigen Registrierungsschlüssel. Die meisten der erwähnten Schlüssel befinden sich im Hive für die systemweite Konfigura tion – HKEY_LOCAL_MACHINE oder kurz HKLM. Wenn Sie Registrierungseinstellungen ändern, müssen Sie außerordentlich umsichtig vorgehen. Unsachgemäße Änderungen können die Systemleistung beeinträchtigen oder sogar dazu führen, dass das System nicht mehr hochgefahren werden kann. 36 Kapitel 1 Begriffe und Werkzeuge
Weitere Informationen über die Registrierung und ihren inneren Aufbau erhalten Sie in Kapitel 9 von Band 2. Unicode Windows unterscheidet sich von der Mehrzahl der anderen Betriebssysteme dadurch, dass seine meisten internen Textstrings als 16 Bit breite Unicode-Zeichen gespeichert und verarbeitet werden (genauer, im Format UTF-16LE; wenn in diesem Buch Unicode erwähnt wird, ist damit, sofern nicht anders angegeben, UTF-16LE gemeint). Unicode ist ein internationaler Zeichensatzstandard, der eindeutige Werte für die meisten Zeichensätze der Welt definiert und 8-, 16- und 32-Bit-Codierungen für jedes Zeichen bietet. Da viele Anwendungen ANSI-Zeichenstrings von 8 Bit (1 Byte) verarbeiten, nehmen viele Windows-Funktionen Stringparameter mit zwei Eintrittspunkten entgegen, einer Unicode-Version (breit, 16 Bit) und einer ANSI-Version (schmal, 8 Bit). Wenn Sie die schmale Version einer Windows-Funktion aufrufen, gibt es eine kleine Leistungseinbuße, da die Eingabestringparameter vor der Verarbeitung durch das System erst in Unicode und die Ausgabeparameter vor der Rückgabe an die Anwendung erst in ANSI umgewandelt werden. Bei älterem Code, der auf Windows laufen muss, aber noch mit ANSI-Zeichenstrings geschrieben ist, konvertiert Windows die ANSI-Zeichen für den eigenen Gebrauch in Unicode, aber niemals die Daten innerhalb von Dateien. Ob solche Daten als Unicode oder ANSI gespeichert werden, entscheidet die Anwendung. Unabhängig von der Sprache enthalten alle Versionen von Windows dieselben Funktionen. Anstelle von eigenständigen Sprachversionen hat Windows nur eine einzige, weltweit gültige Binärdatei, sodass eine einzelne Installation mehrere Sprachen unterstützen kann (durch Hinzufügen von Sprachpaketen). Es ist auch möglich, Windows-Funktionen zu nutzen, um eine Anwendung in Form einer einzigen, weltweiten Binärdatei bereitzustellen, die mehrere Sprachen unterstützt. Die alten Windows 9x-Betriebssysteme boten keine native Unterstützung für Unicode. Dies war ein weiterer Grund dafür, immer zwei Versionen einer Funktion jeweils für ANSI und für Unicode zu erstellen. So ist beispielsweise die Windows-API-Funktion CreateFile im Grunde genommen gar keine Funktion, sondern ein Makro, das entweder zu der Funktion CreateFileA (ANSI) oder CreateFileW (Unicode, wobei das W für »wide«, also »breit« steht) erweitert wird. Diese Erweiterung erfolgt aufgrund der Kompilierungskonstanten UNICODE, die in Visual Studio C++-Projekten standardmäßig gesetzt ist, da es vorteilhafter ist, mit Unicode-Funktionen zu arbeiten. Es ist jedoch möglich, statt des Makros auch ausdrücklich den Funktionsnamen zu verwenden. Das folgende Experiment führt diese Funktionspaare vor. Grundprinzipien und -begriffe Kapitel 1 37
Experiment: Exportierte Funktionen einsehen In diesem Experiment sehen wir uns die aus einer Windows-Teilsystem-DLL exportierten Funktionen mit dem Tool Dependency Walker an. 1. Laden Sie Dependency Walker von http://dependencywalker.com herunter, je nach Ihrem System die 32- oder die 64-Bit-Version. Entpacken Sie die heruntergeladene ZIP-Datei in einem Ordner Ihrer Wahl. 2. Führen Sie das Tool aus (depends.exe). Öffnen Sie das Menü File, wählen Sie Open, wechseln Sie zum Ordner C:\Windows\System32 (sofern Windows bei Ihnen auf C in stalliert ist), markieren Sie die Datei kernel32.dll und klicken Sie auf Öffnen. 3. Möglicherweise zeigt Dependency Walker eine Warnung an. Sie können sie getrost ignorieren und schließen. 4. Es werden jetzt mehrere Ansichten mit vertikalen und horizontalen Trennleisten angezeigt. Stellen Sie sicher, dass in der Baumansicht oben links kernel32.dll markiert ist. 5. Schauen Sie sich die zweite Ansicht von oben auf der rechten Seite an. Dort sind die exportierten Funktionen aus kernel32.dll aufgeführt. Klicken Sie auf den Spaltenkopf Function, um die Liste nach Namen zu sortieren. Suchen Sie die Funktion CreateFileA. Nicht weit darunter finden Sie auch CreateFileW: 6. Wie Sie sehen, handelt es sich bei den meisten Funktionen, die mindestens ein Stringtype-Argument haben, in Wirklichkeit um Funktionspaare. In der vorstehenden Abbildung sehen Sie CreateFileMappingA/W, CreateFileTransactedA/W und CreateFileMappingNumaA/W. 7. Scrollen Sie durch die Liste, um weitere Funktionspaare zu finden. Sie können auch andere Systemdateien öffnen, z. B. user32.dll und advapi32.dll. 38 Kapitel 1 Begriffe und Werkzeuge
Die COM-gestützten APIs in Windows verwenden gewöhnlich Unicode-Strings, manchmal vom Typ BSTR. Dabei handelt es sich im Grunde genommen um ein mit NULL abgeschlossenes Array aus Unicode-Zeichen, bei dem die Stringlänge in vier Bytes vor dem Beginn des Arrays im Arbeitsspeicher gespeichert ist. Windows Runtime-APIs nutzen nur Unicode-Strings vom Typ HSTRING, bei denen es sich um unveränderliche Unicode-Zeichenarrays handelt. Weitere Informationen über Unicode erhalten Sie auf http://www.unicode.org und in der Programmierdokumentation der MSDN Library. Die internen Mechanismen von Windows untersuchen Viele der Informationen, die wir in diesem Buch geben, gehen auf die Lektüre von Windows-Quellcode und Gespräche mit Entwicklern zurück. Sie müssen uns aber nicht alles ungeprüft glauben. Viele Einzelheiten der internen Mechanismen von Windows lassen sich durch eine Reihe von Werkzeugen sichtbar machen und vorführen, unter anderem auch mit den Tools, die im Lieferumfang von Windows und den Windows-Debuggingtools enthalten sind. Diese Werkzeugpakete werden wir weiter hinten in diesem Abschnitt noch kurz behandeln. Um Sie dazu zu ermutigen, die internen Mechanismen von Windows selbst zu erkunden, geben wir überall in diesem Buch Anleitungen zu »Experimenten«, mit denen Sie sich jeweils einen bestimmten Aspekt des internen Verhaltens von Windows ansehen können. Einige dieser Experimente haben Sie in diesem Kapitel schon gesehen. Führen Sie diese Experimente aus, um die in diesem Buch beschriebenen Vorgänge in Aktion zu erleben. Tabelle 1–4 nennt die wichtigsten in diesem Buch verwendeten Werkzeuge und ihren Ursprung. Werkzeug Image Herkunft Startup Programs Viewer AUTORUNS Sysinternals Access Check ACCESSCHK Sysinternals Dependency Walker DEPENDS www.dependencywalker.com Global Flags GFLAGS Debugging Tools Handle Viewer HANDLE Sysinternals Kernel Debuggers WINDBG, KD WDK, Windows SDK Object Viewer WINOBJ Sysinternals Leistungsüberwachung PERFMON.MSC In Windows enthalten Pool Monitor POOLMON WDK Process Explorer PROCEXP Sysinternals Process Monitor PROCMON Sysinternals Task (Process) List TLIST Debugging tools Task-Manager TASKMGR In Windows enthalten Tabelle 1–4 Werkzeuge zur Untersuchung der internen Mechanismen von Windows Die internen Mechanismen von Windows untersuchen Kapitel 1 39
Leistungsüberwachung und Ressourcenmonitor In diesem Buch werden wir häufig die Leistungsüberwachung verwenden, die Sie über den Ordner Verwaltung in der Systemsteuerung oder durch die Eingabe von perfmon in das Dia logfeld Ausführen erreichen. Insbesondere konzentrieren wir uns dabei auf die Werkzeuge Leistungsüberwachung und Ressourcenmonitor. Die Leistungsüberwachung hat drei Funktionen: Systemüberwachung, Anzeige von Leistungsindikatorenprotokollen und Einrichten von Warnungen (mithilfe von Datensammlersätzen, die wiederum Leistungsindikatorenprotokolle sowie Ablaufverfolgungs- sowie Konfigurationsdaten nutzen). Der Einfachheit halber schreiben wir Leistungsüberwachung, wenn wir die darin enthaltene Funktion zur Systemüberwachung meinen. Die Leistungsüberwachung bietet mehr Informationen über das Funktionieren des Systems als jedes andere einzelne Werkzeug. Sie finden Hunderte von grundlegenden und erweiterbaren Indikatoren für verschiedene Objekte. Bei jedem Thema, das wir in diesem Buch beschreiben, geben wir auch jeweils eine Tabelle der entsprechenden Windows-Leistungsindikatoren an. In der Leistungsüberwachung finden Sie eine kurze Beschreibung der einzelnen Indikatoren. Um sie einzusehen, markieren Sie im Fenster Leistungsindikatoren hinzufügen einen Indikator und aktivieren das Kontrollkästchen Beschreibung anzeigen. Obwohl sich die gesamte maschinennahe Überwachung, die wir in diesem Buch durchführen, mit der Leistungsüberwachung erledigen lässt, bietet Windows noch ein weiteres Instrument, nämlich den Ressourcenmonitor (den Sie über das Startmenü oder die Registerkarte Leistung des Task-Managers erreichen). Er zeigt die vier wichtigsten Systemressourcen an, nämlich CPU, Festplatte, Netzwerk und Arbeitsspeicher. In der Grundansicht werden dazu die gleichen Informationen angezeigt wie im Task-Manager. Die einzelnen Abschnitte können jedoch auch erweitert werden, um zusätzliche Angaben einzusehen. Die folgende Abbildung zeigt eine typische Ansicht des Ressourcenmonitors: 40 Kapitel 1 Begriffe und Werkzeuge
Im erweiterten Zustand zeigt die Registerkarte CPU Informationen über die CPU-Nutzung durch die einzelnen Prozesse an. Das können Sie auch im Task-Manager sehen, aber hier gibt es noch eine zusätzliche Spalte für die durchschnittliche CPU-Nutzung, die Ihnen eine bessere Vorstellung davon vermitteln kann, welche Prozesse am aktivsten sind. Die Registerkarte CPU enthält auch eine separate Anzeige der Dienste, ihrer CPU-Nutzung und ihrer durchschnittlichen Nutzung. Bei jedem Diensthostingprozess ist die zugehörige Dienstgruppe angegeben. Wie in Process Explorer können Sie einen Prozess auswählen (indem Sie auf das zugehörige Kontrollkästchen klicken), wodurch eine Liste der benannten Handles angezeigt wird, die der Prozess geöffnet hat, sowie eine Liste der Module (z. B. DLLs), die in dem Adressraum des Prozesses geladen wurden. Im Feld Handles durchsuchen können Sie auch nach den Prozessen suchen, die für eine gegebene benannte Ressource ein Handle geöffnet haben. Die Registerkarte Arbeitsspeicher zeigt fast die gleichen Informationen an wie die entsprechende Registerkarte im Task-Manager, ist aber für das gesamte System ausgelegt. Ein Balkendiagramm zeigt die aktuelle Aufteilung des physischen Arbeitsspeichers in die Abschnitte Für Hardware reserviert, In Verwendung, Geändert, Standby und Frei. Was diese Begriffe bedeuten, erfahren Sie in Kapitel 5. Auf der Registerkarte Datenträger sehen Sie Angaben zur E/A der einzelnen Dateien in einer Darstellung, in der Sie leicht die Dateien auf dem System erkennen, auf die am meisten zugriffen, in die am meisten geschrieben oder in denen am meisten gelesen wird. Die Ergebnisse lassen sich darüber hinaus noch nach Prozessen filtern. Die Registerkarte Netzwerk gibt die aktiven Netzwerkverbindungen, ihre Besitzerprozesse und die Menge der über sie übertragenen Daten an. Dadurch können Sie Netzwerkaktivitäten erkennen, die im Hintergrund ablaufen und sich sonst nur schwer aufspüren lassen. Außerdem Die internen Mechanismen von Windows untersuchen Kapitel 1 41
werden die aktiven TCP-Verbindungen angezeigt, geordnet nach Prozessen und mit Daten wie Remoteport, lokaler Port, Remoteadresse, lokale Adresse, Paketverluste und Latenz. Des Weiteren befindet sich hier eine nach Prozessen geordnete Liste der Ports, die es Administratoren ermöglicht, zu erkennen, welche Dienste oder Anwendungen gerade an einem gegebenen Port auf eine Verbindung warten. Für die einzelnen Ports und Prozesse sind auch jeweils das Protokoll und die Firewallrichtlinie angegeben. Alle Windows-Leistungsindikatoren sind in Programmen zugänglich. Um weitere Informationen darüber zu erhalten, suchen Sie in der MSDN-Dokumentation nach »performance counters«. Kernel-Debugging Kernel-Debugging bedeutet, die internen Datenstrukturen des Kernels zu untersuchen und die Funktionen im Kernel schrittweise zu durchlaufen. Das ist eine praktische Vorgehensweise, um die internen Mechanismen von Windows zu untersuchen, da Sie damit interne Systeminformationen einsehen können, die über andere Werkzeugen nicht verfügbar sind, und eine bessere Vorstellung des Codeflusses innerhalb des Kernels erhalten. Bevor wir die verschiedenen Möglichkeiten für das Kernel-Debugging beschreiben, wollen wir uns einige Dateien ansehen, die Sie dafür brauchen. Symbole für das Kernel-Debugging Symboldateien enthalten die Namen von Funktionen und Variablen und das Layout und Format von Datenstrukturen. Sie werden vom Linker erstellt und von Debuggern genutzt, um beim Debugging auf diese Namen zu verweisen und sie anzuzeigen. Diese Informationen sind gewöhnlich nicht im Binärabbild gespeichert, da sie zur Ausführung des Codes nicht erforderlich sind. Dadurch können die Binärdateien kleiner und schneller gemacht werden. Allerdings müssen Sie beim Debugging dafür sorgen, dass der Debugger auf die Symboldateien für die Images zugreifen kann, mit denen Sie arbeiten wollen. Um irgendeines der Kernel-Debugging-Tools verwenden zu können, um interne Daten strukturen des Windows-Kernels wie die Prozessliste, Threadblockierungen, die Liste der geladenen Treiber, Angaben über die Speichernutzung usw. zu untersuchen, brauchen Sie die Symboldateien mindestens für das Kernelabbild Ntoskrnl.exe. (Mehr über diese Datei erfahren Sie im Abschnitt »Die Architektur im Überblick« in Kapitel 2.) Die Symboltabellendateien müssen dabei jeweils der Version des Images entsprechen, das Sie untersuchen. Wenn Sie beispielsweise den Kernel mit einem Windows-Servicepack oder einem Hotfix aktualisiert haben, müssen Sie auch entsprechend aktualisierte Symboldateien verwenden. Es ist zwar möglich, die Symbole für verschiedene Windows-Versionen herunterzuladen und zu installieren, doch aktualisierte Versionen für Hotfixes sind nicht immer verfügbar. Die einfachste Möglichkeit, um die korrekte Version der Symbole für das Debugging zu bekommen, besteht darin, einen Microsoft-Symbolserver zu nutzen, indem Sie im Debugger eine besondere Syntax für den Symbolpfad angeben. Beispielsweise sorgt der folgende Symbolpfad 42 Kapitel 1 Begriffe und Werkzeuge
dafür, dass der Debugger die erforderlichen Symbole von dem Symbolserver im Internet herunterlädt und eine lokale Kopie im Ordner C:\symbols ablegt: srv*c:\symbols*http://msdl.microsoft.com/download/symbols Debugging Tools for Windows Das Paket Debugging Tools for Windows enthält ausgefeilte Debugging-Werkzeuge, die wir in diesem Buch zur Untersuchung der inneren Mechanismen von Windows einsetzen. Die neueste Version ist im Windows SDK enthalten. (Weitere Informationen über die verschiedenen Installationsarten finden Sie auf https://msdn.microsoft.com/en-us/library/windows/hardware/ ff551063.aspx.) Die Werkzeuge eignen sich zum Debugging von Prozessen sowohl im Benutzer- als auch im Kernelmodus. In dem Paket sind die vier Debugger cdb, ntsd, kd und WindDbg enthalten. Sie alle basieren auf derselben Debugging-Engine, die in DbgEng.dll implementiert und in der Hilfedatei für das Paket ziemlich gut dokumentiert ist. Die Debugger weisen folgende Eigenschaften auf: QQ cdb und ntsd sind Debugger für den Benutzermodus, die in einer Konsole ausgeführt werden. Der einzige Unterschied zwischen ihnen besteht darin, dass ntsd ein neues Konsolenfenster öffnet, wenn er von einem bereits vorhandenen Konsolenfenster aus aufgerufen wird, was bei cdb nicht der Fall ist. QQ kd ist ein Debugger für den Kernelmodus, der in einer Konsole ausgeführt wird. QQ WinDbg kann sowohl für den Benutzer- als auch den Kernelmodus verwendet werden, aber nicht gleichzeitig. Er verfügt über eine grafische Benutzeroberfläche. QQ Die Benutzermodus-Debugger (cdb, ntsd und WinDbg) sind im Grunde genommen gleichwertig. Welchen Sie davon bevorzugen, ist Geschmackssache. QQ Auch die Kernelmodus-Debugger (kd und WinDbg) sind gleichwertig. Debugging im Benutzermodus Mit den Debugging-Tools können Sie sich auch in einen Benutzermodus-Prozess einklinken und den Prozessarbeitsspeicher untersuchen und ändern. Dabei gibt es zwei Vorgehensweisen: QQ Invasiv Wenn Sie sich in einen laufenden Prozess einklinken, verwenden Sie standardmäßig die Windows-Funktion DebugActiveProcess, um eine Verbindung zwischen dem Debugger und dem zu untersuchenden Prozess herzustellen. Dadurch können Sie den Prozessspeicher untersuchen und ändern, Haltepunkte setzen und andere Debuggingfunktionen ausführen. Wenn Sie den Debugger nur trennen und nicht ausschalten, können Sie den Debuggingvorgang abbrechen, ohne den Zielprozess zu beenden. QQ Nicht invasiv Bei dieser Option öffnet der Debugger den Prozess mit der Funktion OpenProcess, ohne sich in ihn einzuklinken. Dadurch können Sie den Arbeitsspeicher des Zielprozesses untersuchen und ändern, aber keine Haltepunkte setzen. Die nicht invasive Vorgehensweise ist auch dann möglich, wenn sich ein anderer Debugger invasiv eingeklinkt hat. Die internen Mechanismen von Windows untersuchen Kapitel 1 43
Mit den Debugging-Tools können Sie auch Dumpdateien von Benutzermodus-Prozessen öffnen. Diese Dateien werden in Kapitel 8 von Band 2 im Abschnitt über Ausnahmezuteilung erklärt. Debugging im Kernelmodus Wie bereits erwähnt, sind zwei der Tools auch für das Kerneldebugging geeignet, nämlich das Befehlszeilenwerkzeug Kd.exe und das GUI-Programm WinDbg.exe. Damit können Sie drei verschiedene Arten von Kerneldebugging durchführen: QQ Öffnen einer Absturzabbilddatei, die infolge eines Windows-Systemabsturzes erstellt wurde. (Mehr über Kernelabsturzabbilder erfahren Sie in Kapitel 15, »Analyse von Absturzabbildern«, von Band 2.) QQ Verbindung zu einem laufenden System und Untersuchung von dessen Status (bzw. Setzen von Haltepunkten, wenn Sie Gerätetreibercode debuggen). Für diesen Vorgang brauchen Sie zwei Computer, nämlich das Ziel (das zu debuggende System) und den Host (das System, auf dem der Debugger ausgeführt wird). Die Verbindung kann über ein Nullmodemkabel, ein IEEE-1394-Kabel, ein USB-2.0/3.0-Debuggingkabel oder das lokale Netzwerk erfolgen. Das Zielsystem muss im Debugmodus gestartet werden. Dafür können Sie Bcdedit.exe oder msconfig.exe entsprechend einrichten. (Möglicherweise müssen Sie dabei auf den sicheren Start in den UEFI-BIOS-Einstellungen verzichten.) Wenn Sie Windows 7 oder frühere Versionen über ein Virtualisierungsprodukt wie Hyper-V, VirtualBox oder VMware Workstation debuggen, können Sie auch den seriellen Port des Gastbetriebssystems als benannte Pipe verfügbar machen und darüber eine Verbindung herstellen. Für Gastbetriebssysteme ab Windows 8 sollten Sie jedoch das Debugging über das lokale Netzwerk durchführen, indem Sie das Hostnetzwerk über eine virtuelle Netzwerkkarte im Gastbetriebssystem bereitstellen. Das führt zu einem Leistungsgewinn um den Faktor 1000. QQ Bei Windows-Systemen können Sie auch Verbindung zum lokalen System aufnehmen und dessen Zustand untersuchen. Diese Vorgehensweise wird lokales Kerneldebugging genannt. Für das Debugging mit WinDbg müssen Sie das System erst in den Debugmodus versetzen (etwa indem Sie Msconfig.exe ausführen, die Registerkarte Start aufrufen, dort auf Erweiterte Optionen klicken, Debug aktivieren und Windows neu starten). Starten Sie anschließend WinDbg mit Administratorrechten, öffnen Sie das Menü File, wählen Sie Kernel Debug, klicken Sie auf die Registerkarte Local und dann auf OK (oder verwenden Sie Bcdedit.exe). Abb. 1–6 zeigt eine Beispielausgabe auf einem Computer mit der 64-Bit-Version von Windows 10. Im lokalen Kerneldebuggingmodus funktionieren einige der Befehle für das Kerneldebugging nicht. Unter anderem können Sie keine Haltepunkte setzen und mit dem Befehl .dump keinen Speicherauszug erstellen. Letzteres ist allerdings mit LiveKd möglich, das weiter hinten in diesem Abschnitt beschrieben wird. 44 Kapitel 1 Begriffe und Werkzeuge
Abbildung 1–6 Lokales Kerneldebugging Sobald die Verbindung im Kerneldebuggingmodus hergestellt ist, können Sie eine der vielen Debuggererweiterungen nutzen – der sogenannten Bang-Befehle, weil sie mit einem Ausrufezeichen beginnen –, um den Inhalt von internen Datenstrukturen wie Threads, Prozessen und E/A-Anforderungspaketen sowie Speicherverwaltungsinformationen anzuzeigen. In diesem Buch sind immer die zum jeweiligen Thema passenden Kerneldebuggerbefehle und deren Ausgaben angegeben. Eine hervorragende Möglichkeit zum Nachschlagen bietet die Hilfedatei Debugger.chm, die im Installationsordner von WinDbg enthalten ist und den gesamten Funktionsumfang des Kerneldebuggers und seiner Erweiterungen dokumentiert. Außerdem können Sie mit dem Befehl dt (display type) für mehr als 1000 Kernelstrukturen Typinformationen anzeigen lassen, die in der Kernelsymboldatei für Windows enthalten sind. Experiment: Typinformationen über Kernelstrukturen Um sich die Liste der Kernelstrukturen anzusehen, deren Typinformationen in den Kernelsymbolen enthalten sind, geben Sie im Kerneldebugger dt nt!_* ein. Dadurch erhalten Sie die folgende (hier gekürzt wiedergegebene) Ausgabe: lkd> dt nt!_* ntkrnlmp!_KSYSTEM_TIME ntkrnlmp!_NT_PRODUCT_TYPE ntkrnlmp!_ALTERNATIVE_ARCHITECTURE_TYPE ntkrnlmp!_KUSER_SHARED_DATA ntkrnlmp!_ULARGE_INTEGER ntkrnlmp!_TP_POOL ntkrnlmp!_TP_CLEANUP_GROUP ntkrnlmp!_ACTIVATION_CONTEXT ntkrnlmp!_TP_CALLBACK_INSTANCE → Die internen Mechanismen von Windows untersuchen Kapitel 1 45
ntkrnlmp!_TP_CALLBACK_PRIORITY ntkrnlmp!_TP_CALLBACK_ENVIRON_V3 ntkrnlmp!_TEB Dabei ist ntkrnlmp der interne Dateiname des 64-Bit-Kernels (siehe Kapitel 2). Mit dem Befehl dt können Sie auch mit Jokerzeichen nach bestimmten Strukturen suchen. Wenn Sie beispielsweise den Namen der Struktur für ein Interruptobjekt benötigen, können Sie dt nt!_*interrupt* eingeben: lkd> dt nt!_*interrupt* ntkrnlmp!_KINTERRUPT_MODE ntkrnlmp!_KINTERRUPT_POLARITY ntkrnlmp!_PEP_ACPI_INTERRUPT_RESOURCE ntkrnlmp!_KINTERRUPT ntkrnlmp!_UNEXPECTED_INTERRUPT ntkrnlmp!_INTERRUPT_CONNECTION_DATA ntkrnlmp!_INTERRUPT_VECTOR_DATA ntkrnlmp!_INTERRUPT_HT_INTR_INFO ntkrnlmp!_INTERRUPT_REMAPPING_INFO Wenn Sie bei dt wie folgt den Namen einer einzelnen Struktur angeben (wobei nicht zwischen Groß- und Kleinschreibung unterschieden wird), können Sie genauere Informationen darüber einsehen: lkd> dt nt!_KINTERRUPT +0x000 Type : Int2B +0x002 Size : Int2B +0x008 InterruptListEntry : _LIST_ENTRY +0x018 ServiceRoutine : Ptr64 unsigned char +0x020 MessageServiceRoutine : Ptr64 unsigned char +0x028 MessageIndex : Uint4B +0x030 ServiceContext : Ptr64 Void +0x038 SpinLock : Uint8B +0x040 TickCount : Uint4B +0x048 ActualLock : Ptr64 Uint8B +0x050 DispatchAddress : Ptr64 void +0x058 Vector : Uint4B +0x05c Irql : UChar +0x05d SynchronizeIrql : UChar +0x05e FloatingSave : UChar +0x05f Connected : UChar +0x060 Number : Uint4B +0x064 ShareVector : UChar +0x065 EmulateActiveBoth : UChar → 46 Kapitel 1 Begriffe und Werkzeuge
+0x066 ActiveCount : Uint2B +0x068 InternalState : Int4B +0x06c Mode : _KINTERRUPT_MODE +0x070 Polarity : _KINTERRUPT_POLARITY +0x074 ServiceCount : Uint4B +0x078 DispatchCount : Uint4B +0x080 PassiveEvent : Ptr64 _KEVENT +0x088 TrapFrame : Ptr64 _KTRAP_FRAME +0x090 DisconnectData : Ptr64 Void +0x098 ServiceThread : Ptr64 _KTHREAD +0x0a0 ConnectionData : Ptr64 _INTERRUPT_CONNECTION_DATA +0x0a8 IntTrackEntry : Ptr64 Void +0x0b0 IsrDpcStats : _ISRDPCSTATS +0x0f0 RedirectObject : Ptr64 Void +0x0f8 Padding : [8] UChar Standardmäßig zeigt dt keine Unterstrukturen an. Dazu müssen Sie den Schalter -r oder -b verwenden. (Den genauen Unterschied zwischen diesen beiden Schaltern entnehmen Sie bitte der Dokumentation.) Beispielsweise können Sie diese Schalter bei der Anzeige des Kernelinterruptobjekts angeben, um sich das Format der Struktur _LIST_ENTRY anzeigen zu lassen, die im Feld InterruptListEntry gespeichert ist: lkd> dt nt!_KINTERRUPT -r +0x000 Type : Int2B +0x002 Size : Int2B +0x008 InterruptListEntry : _LIST_ENTRY +0x000 Flink : Ptr64 _LIST_ENTRY +0x000 Flink +0x008 Blink +0x008 Blink : Ptr64 _LIST_ENTRY : Ptr64 _LIST_ENTRY : Ptr64 _LIST_ENTRY +0x000 Flink +0x008 Blink +0x018 ServiceRoutine : Ptr64 _LIST_ENTRY : Ptr64 _LIST_ENTRY : Ptr64 unsigned char Bei dt können Sie sogar die Verschachtelungstiefe der anzuzeigenden Strukturen festlegen, indem Sie hinter dem Schalter -r eine Zahl angeben. Mit dem Befehl im folgenden Beispiel wählen Sie eine Verschachtelungstiefe von einer Ebene aus: lkd> dt nt!_KINTERRUPT -r1 Die Hilfedatei zu den Debugging Tools for Windows erklärt Ihnen, wie Sie die Kerneldebugger einrichten und verwenden. Weitere Einzelheiten darüber, die vor allem für die Autoren von Gerätetreibern interessant sind, finden Sie in der WDK-Dokumentation. Die internen Mechanismen von Windows untersuchen Kapitel 1 47
LiveKd LiveKd ist ein kostenloses Tool von Sysinternals, mit dem es möglich ist, das laufende System mit den zuvor beschriebenen Kerneldebuggern von Microsoft zu untersuchen, ohne es im Debugmodus zu starten. Das kann erforderlich sein, wenn Sie eine Störungssuche auf Kernelebene an einem Computer durchführen müssen, der nicht im Debugmodus läuft. Da sich manche Probleme nur schwer zuverlässig reproduzieren lassen, kann es sein, dass sich der Fehler nach einem Neustart mit der Debugoption nicht mehr so deutlich zeigt. LiveKd führen Sie genauso aus wie WinDbg und kd. Es übergibt alle von Ihnen eingegebenen Befehlszeilenoptionen an den Debugger Ihrer Wahl. Standardmäßig führt LiveKd die Befehlszeilenversion des Kerneldebuggers aus (kd), doch mit der Option -w können Sie auf WinDbg umschalten. Um die Hilfedateien für die Befehlszeilenoptionen von LiveKd einzusehen, verwenden Sie den Schalter -?. LiveKd stellt dem Debugger eine simulierte Absturzabbilddatei bereit. Daher können Sie in LiveKd jegliche Operationen durchführen, die auch bei einem Absturzabbild möglich sind. Da LiveKd für dieses Abbild auf den physischen Arbeitsspeicher zurückgreift, kann der Debugger auf Datenstrukturen stoßen, die gerade geändert werden und daher inkonsistent sind. Jedes Mal, wenn der Debugger neu gestartet wird, sieht er eine aktuelle Ansicht des Systemzustands. Sie können diese Momentaufnahme auch aktualisieren, indem Sie den Befehl q eingeben, um den Debugger zu beenden. LiveKd fragt Sie dann, ob Sie ihn neu starten wollen. Wenn der Debugger bei der Ausgabe in eine Schleife gerät, können Sie sie mit (Strg) + (C) abbrechen und beenden. Hängt der Debugger, drücken Sie (Strg) + (Untbr), wodurch der Debuggerprozess beendet wird. LiveKd fragt Sie hier ebenfalls, ob Sie ihn neu starten wollen. Windows Software Development Kit Das Windows Software Development Kit (SDK) ist im Rahmen des MSDN-Abonnementprogramms erhältlich. Sie können es kostenlos von https://developer.microsoft.com/en-US/ windows/downloads/windows-10-sdk herunterladen. Auch bei der Installation von Visual Studio haben Sie die Möglichkeit, das SDK zu installieren. Im Abonnement erhalten Sie immer die neueste Version, wohingegen in Visual Studio auch eine ältere Version enthalten sein kann, die beim Kauf von VS gerade aktuell war. Neben den Debugging Tools for Windows enthält das SDK auch die erforderlichen C-Headerdateien und Bibliotheken zum Kompilieren und Verlinken von Windows-Anwendungen. Für unsere Zwecke sind vor allem die Windows-API-Headerdateien (z. B. C:\Program Files (x86)\Windows Kits\10\Include) und die SDK-Tools (im Ordner Bin) von Bedeutung. Ebenso von Interesse ist die Dokumentation, die online zur Verfügung steht und für den Offlinezugriff heruntergeladen werden kann. Einige der Werkzeuge sind auch als Beispielquellcode sowohl im Windows SDK als auch in MSDN Library erhältlich. 48 Kapitel 1 Begriffe und Werkzeuge
Windows Driver Kit Das Windows Driver Kit (WDK) ist ebenfalls im MSDN-Abonnementprogramm verfügbar und kann kostenlos heruntergeladen werden. Die WDK-Dokumentation finden Sie in MSDN Library. Das WDK ist zwar vor allem für Entwickler von Gerätetreibern gedacht, bietet aber auch eine Fülle von Informationen über interne Windows-Mechanismen. Um Ihnen ein Beispiel zu geben: Kapitel 6 erklärt zwar die Architektur des E/A-Systems, das Treibermodell und die grundlegenden Datenstrukturen von Gerätetreibern, aber nicht die einzelnen Kernelunterstützungsfunktionen. Eine ausführliche Beschreibung dieser Funktionen und der von den Gerätetreibern verwendeten Mechanismen finden Sie jedoch in der WDK-Dokumentation, sowohl in Form von Tutorials als auch eines Nachschlagewerks. Außer dieser Dokumentation enthält das WDK Headerdateien (insbesondere ntddk.h, ntifs.h und wdm.h), die entscheidende interne Datenstrukturen und Konstanten sowie Schnittstellen zu vielen internen Systemroutinen definieren. Diese Dateien sind praktisch, wenn Sie die internen Datenstrukturen von Windows mit einem Kerneldebugger untersuchen, da in diesem Buch zwar das allgemeine Layout und der Inhalt dieser Strukturen angegeben wird, aber keine Beschreibungen der einzelnen Felder (z. B. Angaben über Größe und Datentyp). Zahlreiche dieser Datenstrukturen – etwa Objektdispatcherheader, Warteblöcke, Ereignisse, Mutanten, Semaphoren usw. – sind jedoch ausführlich im WDK beschrieben. Wenn Sie über die Darstellung in diesem Buch noch tiefer in das E/A-System und das Treibermodell einsteigen möchten, sollten Sie die WDK-Dokumentation lesen, insbesondere die Handbücher Kernel-Mode Driver Architecture Design Guide und Kernel-Mode Driver Reference. Ebenfalls nützlich sind die Bücher Programming the Microsoft Windows Driver Model, Second Edition von Walter Oney (Microsoft Press, 2002) und Developing Drivers with the Windows Driver Foundation von Penny Orwick und Guy Smith (Microsoft Press, 2007). Sysinternals-Werkzeuge Für viele Experimente in diesem Buch werden Freeware-Tools verwendet, die Sie von Sysinternals herunterladen können. Die meisten davon hat Mark Russinovich geschrieben, einer der Autoren dieses Buches. Die am häufigsten genutzten dieser Werkzeuge sind Process Explorer und Process Monitor. Beachten Sie, dass Sie für viele dieser Tools Kernelmodus-Gerätetreiber installieren und ausführen müssen, wofür Administrator- oder erhöhte Rechte erforderlich sind. Einige der Werkzeuge können jedoch auch mit eingeschränktem Funktionsumfang und eingeschränkter Ausgabe von Standardbenutzerkonten ohne erhöhte Rechte ausgeführt werden. Die Sysinternals-Tools werden regelmäßig aktualisiert. Achten Sie daher darauf, dass Sie die neueste Version verwenden. Um sich über Aktualisierungen zu informieren, folgen Sie dem Blog auf der Sysinternals-Website (für die es auch einen RSS-Feed gibt). Eine Beschreibung all dieser Werkzeuge und ihrer Verwendung sowie Fallstudien über damit gelöste Probleme finden Sie in Windows Sysinternals Administrator’s Reference von Mark Russinovich und Aaron Margosis (Microsoft Press, 2011). Wenn Sie Fragen über die Tools stellen oder darüber diskutieren möchten, nutzen Sie das Sysinternals-Forum. Die internen Mechanismen von Windows untersuchen Kapitel 1 49
Zusammenfassung In diesem Kapitel haben Sie eine Einführung in wichtige technische Prinzipien und Grundbegriffe von Windows erhalten, die im weiteren Verlauf dieses Buches verwendet werden. Des Weiteren haben Sie einen Eindruck von den vielen praktischen Tools bekommen, mit denen Sie die inneren Mechanismen von Windows untersuchen können. Damit sind Sie jetzt gewappnet, die interne Gestaltung des Systems zu erkunden. Dabei beginnen wir mit einem Überblick über die Systemarchitektur und ihre Hauptkomponenten. 50 Kapitel 1 Begriffe und Werkzeuge
K apite l 2 Systemarchitektur Nachdem Sie die erforderlichen Grundbegriffe, Prinzipien und Werkzeuge kennengelernt haben, können Sie nun damit beginnen, die Designziele und internen Strukturen des Betriebssystems Microsoft Windows zu erkunden. In diesem Kapitel geht es um die Gesamtarchitektur des Systems – die Hauptkomponenten, ihre Wechselbeziehungen zueinander und den Kontext, in dem sie ausgeführt werden. Um Ihnen ein Gerüst für das Verständnis der inneren Mechanismen von Windows zu geben, wollen wir uns zunächst die Anforderungen und Ziele ansehen, die das ursprüngliche Design und die Spezifikation des Systems bestimmt haben. Anforderungen und Designziele Die folgenden Anforderungen bestimmten die Spezifikation von Windows NT im Jahr 1989: QQ Es sollte ein präemptives, eintrittsvariantes echtes 32-Bit-Betriebssystem mit virtuellem Speicher entwickelt werden. QQ Das Betriebssystem sollte auf mehreren Hardwarearchitekturen und -plattformen laufen. QQ Es sollte auf Systemen mit symmetrischem Multiprocessing laufen und gut skalierbar sein. QQ Es sollte eine hervorragende Plattform für verteilte Verarbeitung bilden, sowohl als Netzwerkclient als auch als Server. QQ Es sollte die meisten vorhandenen 16-Bit-MS-DOS- und Microsoft Windows 3.1-Anwendungen ausführen. QQ Es sollte die staatlichen Anforderungen für POSIX-1003.1-Konformität erfüllen. QQ Es sollte die staatlichen und die Branchenanforderungen für Betriebssystemsicherheit erfüllen. QQ Es sollte Unicode unterstützen, um leicht an den weltweiten Markt angepasst werden zu können. Um ein System zu erschaffen, das all diese Anforderungen erfüllte, mussten Tausende von Entscheidungen getroffen werden. Um diesen Vorgang zu steuern, stellte das Designteam von Windows NT zu Beginn des Projekts die folgenden Designziele auf: QQ Erweiterbarkeit Der Code muss so geschrieben werden, dass er bequem ergänzt und geändert werden kann, wenn sich die Marktanforderungen wandeln. QQ Portierbarkeit Das System muss auf verschiedenen Hardwarearchitekturen lauffähig und auch relativ einfach auf neue Architekturen übertragbar sein, wenn der Markt dies verlangt. 51
QQ Zuverlässigkeit und Stabilität Das System muss sich selbst sowohl gegen interne Fehlfunktionen als auch gegen externe Manipulation schützen. Es darf nicht möglich sein, dass Anwendungen das Betriebssystem oder andere Anwendungen schädigen. QQ Kompatibilität Windows NT soll zwar die vorhandene Technologie erweitern, seine Benutzerschnittstelle und die APIs sollen aber mit älteren Versionen von Windows und MSDOS kompatibel sein. Es soll auch gut mit anderen Systemen wie UNIX, OS/2 und NetWare zusammenwirken können. QQ Leistung Das System soll im Rahmen der Vorgaben durch die anderen Designziele auf jeder Hardwareplattform so schnell und reaktiv sein wie möglich. Wenn wir die internen Strukturen und Vorgänge von Windows untersuchen, werden Sie noch sehen, wie diese Designziele und die Marktanforderungen in der Konstruktion erfolgreich ver knüpft wurden. Bevor wir unsere Erforschung beginnen, wollen wir jedoch das allgemeine Designmodell von Windows betrachten und mit dem anderer moderner Betriebssysteme vergleichen. Das Modell des Betriebssystems In den meisten Mehrbenutzer-Betriebssystemen sind die Anwendungen vom Betriebssystem getrennt. Der Kernelcode des Betriebssystems läuft in einem privilegierten Prozessormodus (der in diesem Buch als Kernelmodus bezeichnet wird) und hat Zugriff auf die Systemdaten und die Hardware. Der Anwendungscode dagegen wird in einem nicht privilegierten Prozessormodus ausgeführt (dem Benutzermodus), in dem nur eine begrenzte Menge der Schnittstellen zur Verfügung steht, der Zugriff auf Systemdaten eingeschränkt ist und es keinen direkten Zugriff auf die Hardware gibt. Wenn ein Benutzermodusprogramm einen Systemdienst aufruft, führt der Prozesse eine besondere Anweisung aus, die den aufrufenden Thread in den Kernelmodus umschaltet. Hat der Systemdienst seine Arbeit abgeschlossen, schaltet das Betriebssystem den Threadkontext wieder in den Benutzermodus und erlaubt dem Aufrufer, fortzufahren. Windows ähnelt den meisten UNIX-Systemen in der Hinsicht, dass es sich um ein monoli thisches Betriebssystem handelt, d. h., dass sich der Großteil des Betriebssystems und der Gerätetreibercode denselben geschützten Kernelmodus-Arbeitsspeicherbereich teilen. Dadurch ist jede Betriebssystemkomponente und jeder Gerätetreiber theoretisch in der Lage, Daten zu beschädigen, die von anderen Systemkomponenten genutzt werden. Wie Sie in Kapitel 1 gesehen haben, geht Windows dieses Problem jedoch dadurch an, dass es die Qualität von Drittanbietertreibern stärkt und ihre Herkunft einschränkt. Dies wird mit Programmen wie WHQL erreicht und durch KMCS durchgesetzt. Außerdem enthält Windows weitere Kernelschutztechnologien wie die virtualisierungsgestützte Sicherheit sowie DeviceGuard und HyperGuard. Wie all diese Einzelteile zusammenpassen, schauen wir uns später in diesem Kapitel an. Weitere Einzelheiten erfahren Sie in Kapitel 7, »Sicherheit«, und Kapitel 8, »Systemmechanismen«, von Band 2. All diese Betriebssystemkomponenten sind vollständig gegen fehlgehende Anwendungen geschützt, da Anwendungen keinen direkten Zugriff auf den Code und die Daten im privilegierten Teil des Betriebssystems haben (wobei sie jedoch andere Kerneldienste aufrufen können). Dieser Schutz ist einer der Gründe, warum Windows einen Ruf als widerstandsfähige und 52 Kapitel 2 Systemarchitektur
stabile Plattform für Anwendungsserver und Arbeitsstationen hat. Gleichzeitig ist es, was die wichtigsten Betriebssystemdienste wie die Verwaltung des virtuellen Speichers, die Datei-E/A, Netzwerkkommunikation und Datei- und Druckerfreigabe angeht, schnell und behände. Die Kernelmoduskomponenten von Windows verkörpern auch die Grundprinzipien des objektorientierten Designs. Beispielsweise langen sie im Allgemeinen nicht in die Datenstrukturen anderer Komponenten hinein, um auf deren Informationen zuzugreifen, sondern nutzen formale Schnittstellen, um Parameter zu übergeben, auf Datenstrukturen zuzugreifen und diese zu bearbeiten. Auch wenn zur Darstellung gemeinsam genutzter Systemressourcen überall Objekte verwendet werden, ist Windows kein objektorientiertes System im strengen Sinne. Der meiste Kernelmoduscode des Betriebssystems ist zur leichteren Portierbarkeit in C geschrieben. Diese Programmiersprache unterstützt objektorientierte Konstrukte wie polymorphe Funktionen und Klassenvererbung nicht direkt. Daher borgt sich die C-Implementierung von Objekten in Windows einige Merkmale bestimmter objektorientierter Sprachen, hängt aber nicht davon ab. Die Architektur im Überblick Nach dem kurzen Überblick über die Designziele und das Modell von Windows wollen wir uns nun die Hauptkomponenten ansehen, die die Architektur ausmachen. Abb. 2–1 zeigt eine vereinfachte Darstellung der Architektur, aber nicht alles. Beispielsweise sind die Netzwerkkomponenten und die verschiedenen Gerätetreiberschichten hier nicht enthalten. Umgebungs teilsysteme System prozesse Dienst prozesse Benutzerprozesse Teilsystem-DLLs Benutzermodus NTDLL.DLL Kernelmodus Exekutive Fenster und Grafik Gerätetreiber Kernel Hardwareabstraktionsschicht (HAL) Hypervisor Hyper-V Kernelmodus (Hypervisor-Kontext) Abbildung 2–1 Vereinfachte Darstellung der Windows-Architektur In Abb. 2–1 sind die Teile von Windows für den Benutzer- und den Kernelmodus durch eine horizontale Linie getrennt. Wie bereits in Kapitel 1 erwähnt, werden Benutzermodusthreads in einem privaten Prozessadressraum ausgeführt (wobei sie jedoch bei der Ausführung im Kernelmodus auch Zugriff auf den Systemadressraum haben). Daher haben System-, Dienst- und Benutzerprozesse sowie die Umgebungsteilsysteme jeweils ihren eigenen privaten Prozess Die Architektur im Überblick Kapitel 2 53
adressraum. Es gibt auch noch eine zweite Trennlinie zwischen den Teilen von Windows für den Kernelmodus und dem Hypervisor. Der Hypervisor hat zwar dieselbe CPU-Rechteebene wie der Kernel (0), verwendet aber besondere CPU-Anweisungen (VT-x auf Intel, SVM auf AMD) und kann sich daher vom Kernel isolieren und ihn (und die Anwendungen) gleichzeitig überwachen. Daher werfen auch manche Leute mit dem Begriff Ring -1 um sich (der nicht korrekt ist). Es gibt die folgenden vier grundlegenden Arten von Benutzermodusprozessen: QQ Benutzerprozesse Dies können Prozesse der folgenden Typen sein: Windows 32 Bit oder 64 Bit (dazu gehören Windows-Apps, die oberhalb der Windows Runtime in Windows 8 und höher laufen), Windows 3.1 16 Bit, MS-DOS 16 Bit, POSIX 32 Bit und 64 Bit. Beachten Sie, dass 16-Bit-Anwendungen nur auf 32-Bit-Windows ausgeführt werden können und POSIX-Anwendungen ab Windows 8 nicht mehr unterstützt werden. QQ Dienstprozesse Dies sind Hostprozesse für Windows-Dienste, z. B. den Taskplaner und den Druckspooler. Dienste müssen generell unabhängig von Benutzeranmeldungen ausgeführt werden können. Viele Windows-Serveranwendungen, z. B. Microsoft SQL Server und Microsoft Exchange Server, enthalten ebenfalls Komponenten, die als Dienste ausgeführt werden. Eine ausführliche Beschreibung von Diensten finden Sie in Kapitel 9, »Verwaltungsmechanismen«, von Band 2. QQ Systemprozesse Dies sind unveränderliche oder fest verdrahtete Prozesse, z. B. der Anmeldeprozess und der Sitzungs-Manager. Es handelt sich dabei nicht um Windows-Dienste, d. h., sie werden nicht vom Dienststeuerungs-Manager gestartet. QQ Serverprozesse des Umgebungsteilsystems Diese Prozesse implementieren die Unterstützung für die Betriebssystemumgebung (oder »Persönlichkeit«), die dem Benutzer und Programmierer angezeigt wird. Windows NT enthielt ursprünglich drei Umgebungsteilsysteme, nämlich Windows, POSIX und OS/2. Allerdings wurde das OS/2-Teilsystem mit Windows 2000 zum letzten Mal ausgeliefert und das POSIX-Teilsystem mit Windows XP. Die Ultimate- und die Enterprise-Edition der Clientversion von Windows 7 sowie Windows Server 2008 R2 enthielten noch eine Unterstützung für ein erweitertes POSIX-Teilsystem namens SUA (Subsystem for UNIX-based Applications). SUA wurde jedoch eingestellt und wird auch nicht mehr optional für Windows angeboten (weder für Client- noch für Serverversionen). Windows 10 Version 1607 enthält ein Windows-Teilsystem für Linux (WSL), allerdings nur als Beta-Version ausschließlich für Entwickler. Es ist auch kein echtes Teilsystem wie die anderen in diesem Abschnitt. WSL und die zugehörigen Pico-Provider werden in diesem Kapitel noch ausführlicher beschrieben. Weitere Informationen über Pico-Prozesse erhalten Sie in Kapitel 3, »Prozesse und Jobs«. Beachten Sie in Abb. 2–1 den Kasten »Teilsystem-DLLs« unter »Dienstprozesse« und »Benutzerprozesse«. In Windows rufen Benutzeranwendungen die nativen Betriebssystemdienste nicht direkt auf, sondern nutzen eine oder mehrere Teilsystem-DLLs. Deren Aufgabe besteht darin, eine dokumentierte Funktion in die entsprechenden internen (und in der Regel nicht dokumentierten) nativen Systemdienstaufrufe zu übersetzen, die meistens in Ntdll.dll implementiert 54 Kapitel 2 Systemarchitektur
sind. Dabei kann eine Nachricht an den Prozess des Umgebungsteilsystems gesendet werden, der den Benutzerprozess handhabt. Windows enthält die folgenden Kernelmoduskomponenten: QQ Exekutive Die Windows-Exekutive umfasst die grundlegenden Betriebssystemdienste wie Speicherverwaltung, Prozess- und Threadverwaltung, Sicherheit, E/A, Netzwerk und Kommunikation zwischen Prozessen. QQ Der Windows-Kernel Er besteht aus den maschinennahen Betriebssystemfunktionen wie Threadplanung, Interrupt- und Ausnahmezuteilung und Multiprozessorsynchronisierung. Außerdem bietet er eine Reihe von Routinen und Grundobjekten, die der Rest der Exekutive verwendet, um Konstrukte höherer Ebene zu implementieren. QQ Gerätetreiber Dazu gehören sowohl Hardwaregerätetreiber, die E/A-Funktionsaufrufe des Benutzers in gerätespezifische E/A-Anforderungen übersetzen, aber auch Gerätetreiber nicht für Hardware, z. B. das Dateisystem und Netzwerktreiber. QQ Hardwareabstraktionsschicht (HAL) Diese Codeschicht schirmt den Kernel, die Gerätetreiber und den Rest der Windows-Exekutive gegen plattformspezifische Hardwareunterschiede ab (z. B. Unterschiede zwischen Motherboards). QQ Fenster- und Grafiksystem Dieses System implementiert die GUI-Funktionen (auch bekannt als Windows USER- und GDI-Funktionen) für den Umgang mit Fenstern, die Steuer elemente der Benutzerschnittstelle, das Zeichnen usw. QQ Hypervisor-Schicht Diese Schicht besteht aus einer einzigen Komponente, nämlich dem Hypervisor selbst. In dieser Umgebung gibt es keine Treiber oder anderen Module. Allerdings besteht der Hypervisor selbst aus mehreren internen Schichten und Diensten wie seinem eigenen Speicher-Manager, dem Scheduler für den virtuellen Prozessor, einer Interrupt- und Timerverwaltung, Synchronisierungsroutinen, einer Partitionsverwaltung (für die VM-Instanzen), Kommunikation zwischen den Partitionen (IPC) usw. Tabelle 2–1 nennt die Dateinamen der wichtigsten Windows-Komponenten (die Sie kennen müssen, da wir in diesem Buch mehrere Systemdateien namentlich erwähnen). Alle diese Komponenten werden in diesem oder den folgenden Kapiteln ausführlicher beschrieben. Datei Komponenten Ntoskrnl.exe Exekutive und Kernel Hal.dll HAL Win32k.sys Kernelmodusteil des Windows-Teilsystems (GUI) Hvix64.exe (Intel), Hvax64.exe (AMD) Hypervisor .sys-Dateien in \SystemRoot\System32\Drivers Haupttreiberdateien, darunter für Direct X, Volume- Manager, TCP/IP, TPM und ACPI-Unterstützung Ntdll.dll Interne Unterstützungsfunktionen und Dispatch-Stubs zur Zuteilung von Systemdiensten an Ausführungsfunktionen Kernel32.dll, Advapi32.dll, User32.dll, Gdi32.dll Haupt-DLLs des Windows-Teilsystems Tabelle 2–1 Wichtige Windows-Systemdateien Die Architektur im Überblick Kapitel 2 55
Bevor wir uns diese Systemkomponenten genauer ansehen, wollen wir jedoch noch einige Grundlagen des Windows-Kerneldesigns vorstellen. Dabei beginnen wir mit der Frage, wie Windows die Portierbarkeit über mehrere Hardwarearchitekturen hinweg erreicht. Portierbarkeit Windows wurde so entworfen, dass es auf verschiedenen Hardwarearchitekturen laufen kann. Das ursprüngliche Release von Windows NT unterstützte x86 und MIPS. Kurz darauf folgte die Unterstützung für Alpha AXP von Digital Equipment Corporation (die von Compaq aufgekauft wurde, das wiederum mit Hewlett-Packard fusionierte). (Alpha AXP war ein 64-Bit-Prozessor, doch Windows NT lief im 32-Bit-Modus. Während der Entwicklung von Windows 2000 wurde auch eine native 64-Bit-Version auf dem Alpha AXP ausgeführt, allerdings wurde sie niemals veröffentlicht.) In Windows NT 3.51 kam die Unterstützung für eine vierte Prozessorarchitektur hinzu, nämlich den Motorola PowerPC. Aufgrund wechselnder Nachfrage am Markt wurde die Unterstützung für MIPS und PowerPC jedoch aufgegeben, bevor die Entwicklung von Windows 2000 begann. Spät nahm Compaq die Unterstützung für die Alpha AXP-Architektur zurück, sodass Windows 2000 nur noch auf x86 lief. In Windows XP und Windows Server 2003 kam die Unterstützung für zwei 64-Bit-Prozessorfamilien hinzu, den Intel Itanium IA-64 und den AMD64 mit der gleichwertigen 64-Bit-Erweiterungstechnologie von Intel (EM64T). Die beiden letztgenannten Implementierungen wurden als erweiterte 64-Bit-Systeme bezeichnet und werden in diesem Buch x64 genannt. (Wie 32-Bit-Anwendungen auf 64-Bit-Windows ausgeführt werden konnten, wird in Kapitel 8 von Band 2 erklärt.) Ab Server 2008 R2 wurden IA-64-Systeme jedoch nicht mehr von Windows unterstützt. Bei jüngeren Windows-Editionen kam auch eine Unterstützung für die ARM-Prozessorarchitektur hinzu. Beispielsweise handelte es sich bei Windows RT um eine Version von Windows 8 für ARM, die inzwischen allerdings eingestellt wurde. Windows 10 Mobile – der Nachfolger von Windows Phone 8.x – läuft auf ARM-Prozessoren wie den Snapdragon-Modellen von Qualcomm. Windows 10 IoT kann sowohl auf x86- als auch auf ARM-Geräten wie dem Raspberry Pi 2 (mit dem Prozessor ARM Cortex-A7) und dem Raspberry Pi 3 (ARM Cortex-A53) ausgeführt werden. Da es inzwischen auch 64-Bit-ARM-Hardware gibt und eine steigende Anzahl von Geräten die Prozessoren der neuen Familien AArch64 und ARM64 verwendet, wird es wahrscheinlich irgendwann auch eine Unterstützung dafür geben. Die architektur- und plattformübergreifende Portierbarkeit erreicht Windows auf allem auf die beiden folgenden Weisen: QQ Geschichtetes Design Windows ist in Schichten aufgebaut. Dabei stehen die maschinennahen Teile des Systems, die von der Prozessorarchitektur oder der Plattform abhängen, in getrennten Modulen, sodass die höheren Ebenen des Systems von den Unterschieden zwischen diesen Architekturen und Plattformen abgeschirmt sind. Die beiden Hauptkomponenten, die für die Portierbarkeit sorgen, sind der Kernel (in Ntoskrnl.exe) und die HAL (in Hal.dll). Beide Komponenten werden weiter hinten in diesem Kapitel noch ausführlicher behandelt. Architekturspezifische Funktionen wie Threadkontextwechsel und Trap-Dispatching sind im Kernel implementiert, und Funktionen, die sich auf Systemen mit derselben Architektur unterscheiden können (z. B. auf unterschiedlichen Motherboards) 56 Kapitel 2 Systemarchitektur
in der HAL. Die einzige andere Komponente mit einem erheblichen Anteil an architekturspezifischem Code ist der Speicher-Manager, aber im Vergleich zum Gesamtsystem ist dieser Anteil sehr gering. Der Hypervisor weist einen ähnlichen Aufbau auf: Die meisten Teile werden von der AMD- und der Intel-Implementierung (SVM bzw. VT-x) gemeinsam genutzt, und daneben gibt es noch einige wenige prozessorspezifische Teile (daher auch die beiden Dateinamen in Tabelle 2–1). QQ Verwendung von C Der Großteil von Windows ist in C geschrieben, einige Teile in C++. Eine Assemblersprache wird nur für die Teile des Betriebssystems verwendet, die direkt mit der Systemhardware kommunizieren müssen (z. B. Interrupt-Trap-Handler) oder die äußerst leistungsempfindlich sind (z. B. Kontextwechsel). Assemblercode gibt es nicht nur im Kernel und in der HAL, sondern auch an einigen anderen Stellen des Kernbetriebssystems (z. B. in den Routinen für gesperrte Anweisungen und in einem Modul in der Einrichtung für lokale Prozeduraufrufe), im Kernelmodus-Teil des Windows-Teilsystems und sogar in einigen Benutzermodus-Bibliotheken, darunter im Prozessstartcode in Ntdll.dll (einer Systembibliothek, die weiter hinten in diesem Kapitel noch genauer beschrieben wird). Symmetrisches Multiprocessing Multitasking ist eine Betriebssystemtechnik, mit der sich mehrere Ausführungsthreads einen einzigen Prozessor teilen können. Auf einem Computer mit mehreren Prozessoren können jedoch mehrere Threads gleichzeitig ausgeführt werden. Während ein Multitasking-Betriebssystem nur vorgibt, mehrere Threads gleichzeitig auszuführen, führt ein Multiprocessing-Betriebssystem jedoch tatsächlich eine Mehrfachverarbeitung durch und führt auf jedem der Prozessoren einen Thread aus. Wie zu Anfang dieses Kapitels erwähnt, bestand eines der wichtigsten Designziele für Windows darin, dass es auch auf Mehrprozessorsystemen gut laufen sollte. Windows ist ein Betriebssystem mit symmetrischem Multiprocessing (SMP). Es gibt keinen Masterprozessor – das Betriebssystem und alle Benutzerthreads können auf beliebigen Prozessoren ausgeführt werden. Außerdem teilen sich alle Prozessoren denselben Speicherbereich. Das steht im Gegensatz zum asymmetrischen Multiprocessing (ASMP), bei dem das Betriebssystem gewöhnlich einen Prozessor für die Ausführung des Kernelcodes auswählt, während auf den anderen Prozessoren nur Benutzercode läuft. Die Unterschiede dieser beiden Modelle veranschaulicht Abb. 2–2. Die Architektur im Überblick Kapitel 2 57
Symmetrisch Asymmetrisch Arbeitsspeicher CPU A Arbeitsspeicher CPU B Betriebs system thread CPU A Benutzer thread Benutzer thread Benutzer thread Betriebs system thread CPU B Betriebs system thread Benutzer thread Betriebs system thread Benutzer thread Benutzer thread Benutzer thread Benutzer thread Abbildung 2–2 Symmetrisches und asymmetrisches Multiprocessing im Vergleich Windows unterstützt vier moderne Arten von Mehrprozessorsystemen: Mehrkernsysteme, SMT-Systeme (Simultaneous Multi-Threaded), heterogene Systeme und NUMA-Systeme (Non-Uniform Memory Access). Sie werden in den folgenden Absätzen kurz vorgestellt. (Eine vollständige, ausführliche Beschreibung der Unterstützung für die Threadplanung auf solchen Systemen finden Sie im entsprechenden Abschnitt in Kapitel 4, »Threads«.) SMT wurde durch die Unterstützung für die Hyperthreading-Technologie von Intel in Windows eingeführt, mit der für jeden physischen Prozessorkern zwei logische Prozessoren bereitgestellt werden können. Neuere AMD-Prozessoren mit Zen-Mikroarchitektur nutzen eine ähnliche SMT-Technik, um ebenfalls die Anzahl der logischen Prozessoren zu verdoppeln. Jeder logische Prozessor hat seinen eigenen CPU-Status, aber die Ausführungs-Engine und der Onboard-Cache werden gemeinsam genutzt. Dadurch kann eine logische CPU weiterarbeiten, während die andere angehalten ist (z. B. nach einem Cachefehler oder einer falsch vorausgesagten Verzweigung). In ihrem Marketingmaterial nennen beide Firmen die zusätzlichen Kerne leider Threads, weshalb man oft Behauptungen wie »vier Kerne, acht Threads« liest. Das bedeutet, dass bis zu acht Threads geplant werden können, es also acht logische Prozessoren gibt. Die Scheduler-Algorithmen wurden verbessert, um die Vorteile von SMT-fähigen Computern optimal zu nutzen, z. B. durch Planung von Threads auf leerlaufenden physischen Prozessoren, anstatt einen leerlaufenden logischen Prozessor auf einem physischen Prozessor auszuwählen, dessen andere logische Prozessoren beschäftigt sind. Weitere Einzelheiten über die Threadplanung finden Sie in Kapitel 4. Auf NUMA-Systemen sind die Prozessoren zu kleineren Einheiten gruppiert, die als Knoten bezeichnet werden. Jeder Knoten verfügt über seine eigenen Prozessoren und seinen eigenen Arbeitsspeicher und ist über einen cachekohärenten Bus mit dem Gesamtsystem verbunden. Auf NUMA läuft Windows immer noch als SMP-System in dem Sinne, dass alle Prozessoren Zugriff auf den gesamten Arbeitsspeicher haben. Allerdings kann der Knoten schneller auf seinen lokalen Arbeitsspeicher zugreifen als auf den Arbeitsspeicher anderer Knoten. Daher versucht das System, die Leistung dadurch zu steigern, dass es Threads auf Prozessoren einplant, die 58 Kapitel 2 Systemarchitektur
zum selben Knoten gehören wie der verwendete Arbeitsspeicher. Es bemüht sich, Speicherzuweisungsanforderungen innerhalb des Knotens zu erfüllen, weist aber auch Arbeitsspeicher auf anderen Knoten zu, wenn dies notwendig sein sollte. Windows bietet eine native Unterstützung für Mehrkernsysteme. Da sie über mehrere physische Kerne (im selben Gehäuse) verfügen, behandelt der SMP-Code von Windows sie als getrennte Prozessoren. Eine Ausnahme davon bilden bestimmte Buchführungs- und Identifizierungsaufgaben (wie die in Kürze beschriebene Lizenzierung), bei denen zwischen Kernen auf demselben Prozessor und Kernen auf verschiedenen Sockeln unterschieden wird. Das ist insbesondere beim Umgang mit Cachetopologien zur Optimierung der gemeinsamen Datennutzung wichtig. Die ARM-Versionen von Windows unterstützen auch das sogenannte heterogene Multiprocessing, dessen Implementierung auf solchen Prozessoren als big.LITTLE bezeichnet wird. Diese Form des SMP-Designs weicht von der herkömmlichen darin ab, dass die Prozessorkerne nicht alle die gleichen Fähigkeiten haben, aber anders als reines heterogenes Multiprocessing immer noch in der Lage sind, dieselben Anweisungen auszuführen. Der Unterschied liegt in den Takt raten und dem Verhältnis des Stromverbrauchs bei Volllast und Leerlauf, wodurch langsame Kerne mit schnelleren kombiniert werden können. Stellen Sie sich vor, Sie würden auf einem älteren Zweikernsystem mit 1 GHz, das an eine moderne Internetverbindung angeschlossen ist, eine E-Mail senden. Der Vorgang würde wahrscheinlich nicht langsamer ablaufen als auf einem 8-Kern-Rechner mit 3,6 GHz, da nicht die Rechenleistung den entscheidenden Engpass bildet, sondern die Tippgeschwindigkeit eines Menschen und die Netzwerkbandbreite. Auf der anderen Seite kann ein modernes System selbst im sparsamsten Energiesparmodus noch erheblich mehr Strom verbrauchen als ein älteres System. Das gilt selbst dann, wenn sich das moderne System selbst auf 1 GHz herunterregeln könnte, denn dann hätte sich das alte System vermutlich auch schon auf 200 MHz herabgestuft. Durch die Möglichkeit, solche alten Prozessoren mit Spitzenmodellen zu kombinieren, kann eine ARM-Plattform mit einem kompatiblen Kernelscheduler die Verarbeitungsleistung maximieren, wenn es erforderlich ist (indem alle Kerne eingeschaltet werden), in einem extremen Energiesparmodus laufen (indem nur ein einziger kleiner Kern online gehalten wird, was für SMS und das Senden von Push-E-Mail ausreicht) oder einen Ausgleich finden (indem einige große Kerne und für andere Aufgaben einige kleine online sind). Durch die Unterstützung der Richtlinien für heterogenes Scheduling können Threads in Windows 10 die Richtlinie auswählen, die ihre Bedürfnisse am besten erfüllt, und mit dem am besten geeigneten Scheduler und Energie-Manager arbeiten. Mehr über diese Richtlinien erfahren Sie in Kapitel 4. Windows wurde nicht im Hinblick auf eine bestimmte Anzahl von Prozessoren entworfen. Aus Gründen des Komforts und der Effizienz hält Windows jedoch Informationen über die Prozessoren (Gesamtzahl, Prozessoren im Leerlauf, beschäftigte Prozessoren usw.) in einer Bitmaske (oder Affinitätsmaske) fest. Die Bitanzahl dieser Maske entspricht dem nativen Datentyp des Computers (32 Bit oder 64 Bit), sodass der Prozessor die Bits direkt in einem Register bearbeiten kann. Daher waren Windows-Systeme ursprünglich auf die Anzahl der CPUs beschränkt, die in ein natives Word passt, da sich die Affinitätsmaske nicht willkürlich vergrößern ließ. Um die Kompatibilität zu erhalten und auch Systeme mit mehr Prozessoren zu unterstützen, implementiert Windows ein Konstrukt höherer Ordnung, nämlich die Prozessorgruppe. Dabei handelt es sich um einen Satz von Prozessoren, die durch eine einzige Affinitätsmaske defi- Die Architektur im Überblick Kapitel 2 59
niert werden können. Bei Änderungen der Affinität können der Kernel und die Anwendungen wählen, auf welche Gruppe sie sich beziehen. Kompatible Anwendungen können die Anzahl der unterstützten Gruppen abfragen (zurzeit bis zu 20, wobei die maximale Anzahl logischer Prozessoren zurzeit bei 640 liegt) und dann die Bitmasken für die einzelnen Gruppen aufzählen. Ältere Anwendungen sehen nur ihre aktuelle Gruppe, sodass sie ebenfalls funktionieren. Wie Windows Prozessoren zu Gruppen zuweist (was auch im Zusammenhang mit NUMA steht), erfahren Sie in Kapitel 4. Die tatsächliche Anzahl der unterstützten lizenzierten Prozessoren hängt von der verwendeten Windows Edition ab (siehe Tabelle 2–2 weiter hinten). Diese Anzahl ist in der Lizenzrichtliniendatei %SystemRoot%\ServiceProfiles\LocalService\AppData\Local\Microsoft\WSLicense\tokens. dat in der Variablen kernel-RegisteredProcessors gespeichert (im Grunde genommen eine Liste von Schlüssel-Wert-Paaren). Skalierbarkeit Eines der Hauptprobleme von Multiprozessorsystemen ist die Skalierbarkeit. Um auf SMP-Systemen korrekt ausgeführt werden zu können, muss der Betriebssystemcode strenge Richtlinien und Regeln erfüllen. Der Wettstreit um Ressourcen und andere Leistungsprobleme sind auf Mehrprozessorsystemen komplizierter als auf Einzelprozessorsystemen und müssen beim Design des Systems berücksichtigt werden. Windows weist mehrere Merkmale auf, die für seinen Erfolg als Multiprozessor-Betriebssystem von entscheidender Bedeutung waren: QQ Die Möglichkeit, Betriebssystemcode auf einem beliebigen verfügbaren Prozessor und auch gleichzeitig auf mehreren Prozessoren auszuführen QQ Mehrere Ausführungsthreads in einem Prozess, die alle gleichzeitig auf verschiedenen Prozessoren laufen können QQ Feine Synchronisierung im Kernel (z. B. Spinlocks, Warteschlangen-Spinlocks und Pushlocks, wie sie in Kapitel 8 von Band 2 beschrieben werden) und in Gerätetreibern und Serverprozessen, wodurch mehr Komponenten gleichzeitig auf mehreren Prozessoren laufen können QQ Programmiermechanismen wie E/A-Vervollständigungsports (siehe Kapitel 6, »Das E/A- System«) für eine effiziente Implementierung von Multithread-Serverprozessen, die sich auf Mehrprozessorsystemen gut skalieren lassen Die Skalierbarkeit des Windows-Kernels hat sich im Laufe der Zeit entwickelt. Beispielsweise wurden Planungswarteschlangen für einzelne CPUs mit feinen Sperren in Windows Server 2003 eingeführt, was es ermöglichte, Entscheidungen zur Threadplanung parallel auf mehreren Prozessoren durchzuführen. In Windows 7 und Windows Server 2008 R2 wurde die globale Schedulersperre bei Warte-Dispatchoperationen beseitigt. Diese schrittweise Verfeinerung der Sperrmechanismen hat auch in anderen Bereichen stattgefunden, z. B. im Speicher-, im Cacheund im Objekt-Manager. 60 Kapitel 2 Systemarchitektur
Unterschiede zwischen den Client- und Serverversionen Windows wird sowohl als Client- als auch als Serverversion verkauft. Von Windows 10 gibt es sechs Desktop-Clientversionen: Windows 10 Home, Windows 10 Pro, Windows 10 Education, Windows 10 Pro Education, Windows 10 Enterprise und Windows 10 Enterprise Long Term Serving Branch (LTSB). Des Weiteren sind Editionen für andere Geräte als Desktopcomputer erhältlich, darunter Windows 10 Mobile, Windows 10 Mobile Enterprise, Windows 10 IoT Core, IoT Core Enterprise und IoT Mobile Enterprise. Für den besonderen Bedarf einzelner Regionen gibt es noch mehr Varianten, etwa die N-Serie. Von Windows Server 2016 gibt es ebenfalls sechs Versionen: Windows Server 2016 Datacenter, Windows Server 2016 Standard, Windows Server 2016 Essentials, Windows Server 2016 MultiPoint Premium Server, Windows Storage Server 2016 und Microsoft Hyper-V Server 2016. Zwischen den Versionen bestehen folgende Unterschiede: QQ Preisgestaltung nach Anzahl der Kerne (statt der Prozessorsockel) bei Windows Server 2016 Datacenter und Standard QQ Anzahl der insgesamt unterstützten logischen Prozessoren QQ Anzahl der Hyper-V-Container, die ausgeführt werden dürfen (bei Serversystemen; in Clientsystemen können nur Windows-Container auf der Grundlage getrennter Namespaces verwendet werden) QQ Unterstützte Größe des physischen Arbeitsspeichers (eigentlich die höchste für den RAM nutzbare physische Adresse; mehr über Grenzwerte für den physischen Arbeitsspeicher erfahren Sie in Kapitel 5, »Speicherverwaltung«) QQ Anzahl der zugelassenen gleichzeitigen Netzwerkverbindungen (beispielsweise sind in den Clientversionen höchstens zehn gleichzeitige Verbindungen für Datei- und Druckdienste erlaubt) QQ Unterstützung für Multitouch und Desktopgestaltung QQ Unterstützung für Merkmale wie BitLocker, VHD-Start, AppLocker, Hyper-V und über 100 weitere einstellbare Lizenzrichtlinienwerte QQ Geschichtete Dienste, die in den Server-, aber nicht in den Clientversionen verfügbar sind (z. B. Verzeichnisdienste, Host-Überwachungsdienst, direkte Speicherplätze, abgeschirmte virtuelle Maschinen und Clusterdienste) Tabelle 2–2 nennt die Unterschiede bei der Unterstützung für Arbeitsspeicher und Prozessoren zwischen einigen Editionen von Windows 10, Windows Server 2012 R2 und Windows Server 2016. Eine ausführliche Vergleichstabelle für die verschiedenen Ausgaben von Windows Server 2012 R2 finden Sie auf https://www.microsoft.com/en-us/download/details.aspx?id=41703. Speichergrenzwerte der Editionen von Windows 10 und Server 2016 sowie früherer Betriebssysteme sind auf https://msdn.microsoft.com/en-us/library/windows/desktop/aa366778.aspx aufgeführt. Die Client- und Servereditionen werden zwar in verschiedenen Paketen verkauft, nutzen aber alle die gleichen Hauptsystemdateien, darunter das Kernelabbild Ntoskrnl.exe (und die Die Architektur im Überblick Kapitel 2 61
PAE-Version Ntkrnlpa.exe), die HAL-Bibliotheken, die Gerätetreiber, die grundlegenden Systemprogramme und die DLLs. Anzahl Prozessorsockel (32-Bit-Edition) Physischer Arbeitsspeicher (32-Bit-Edition) Anzahl logischer Prozessoren/ Sockel (64-BitEdition) Physischer Arbeitsspeicher (x64-Editionen) Windows 10 Home 1 4 GB 1 Sockel 128 GB Windows 10 Pro 2 4 GB 2 Sockel 2 TB Windows 10 Enterprise 2 4 GB 2 Sockel 2 TB Windows Server 2012 R2 Essentials – – 2 Sockel 64 GB Windows Server 2016 Standard – – 512 logische Prozessoren 24 TB Windows Server 2016 Datacenter – – 512 logische Prozessoren 24 TB Tabelle 2–2 Prozessor- und Speichergrenzwerte für einige Windows-Editionen Woher weiß das System angesichts so vieler verschiedener Editionen, die alle dasselbe Kernelabbild verwenden, welche Edition gestartet wurde? Dazu fragt es die Registrierungswerte ProductType und ProductSuite unter dem Schlüssel HKLM\SYSTEM\CurrentControlSet\Control\ ProductOptions ab. Dabei dient ProductType zur Unterscheidung zwischen Client- und Serversystem (gleich welcher Variante). In die Registrierung geladen werden diese Werte auf der Grundlage der zuvor beschriebenen Lizenzrichtliniendatei. Tabelle 2–3 führt die gültigen Werte auf. Die Abfrage kann im Benutzermodus durch die Funktion VerifyVersionInfo erfolgen oder von einem Gerätetreiber mit den Kernelmodusfunktionen RtlGetVersion und RtlVerifyVersionInfo durchgeführt werden, die beide im Windows Driver Kit dokumentiert sind. Windows-Edition Wert von ProductType Windows-Client WinNT Windows-Server (Domänencontroller) LanmanNT Windows-Server (reiner Server) ServerNT Tabelle 2–3 Registrierungswerte für ProductType Der Registrierungswert ProductPolicy enthält eine zwischengespeicherte Kopie der Daten in der Datei tokens.dat, die zwischen den Windows-Editionen und den von diesen jeweils aktivierten Funktionen unterscheidet. Wenn die Hauptdateien der Client- und Serverversionen im Grunde genommen identisch sind, wie unterscheiden sich die Systeme dann im Betrieb? Kurz Serversysteme sind standardmäßig für den Durchsatz von Hochleistungs-Anwendungsservern optimiert, wohingegen die Clientversionen (auch wenn sie einige Serverfähigkeiten haben) auf kurze Reaktionszeiten für interaktive Desktopbenutzung ausgelegt sind. So werden beispielsweise beim Hochfahren des 62 Kapitel 2 Systemarchitektur
Systems je nach Produkttyp unterschiedliche Entscheidungen zur Ressourcenzuweisung getroffen, z. B. was die Größe und Anzahl der Betriebssystemheaps (oder -pools), die Anzahl der internen Systemarbeitsthreads und die Größe des Systemdatencache angeht. Auch einige Laufzeitentscheidungen, z. B. die Abwägung des Speicher-Managers zwischen den Speicheranforderungen des Systems und der Prozesse, unterscheiden sich zwischen den Server- und den Clienteditionen. Sogar einige Einzelheiten der Threadplanung weisen in den beiden Familien jeweils ein unterschiedliches Standardverhalten auf (die Standardlänge der Zeitscheibe oder das Threadquantum; siehe Kapitel 4). Wenn es auf einem Gebiet erhebliche Unterschiede zwischen den beiden Produkten gibt, so wird in den entsprechenden Kapiteln dieses Buches jeweils darauf hingewiesen. Ansonsten gilt alles, was in diesem Buch beschrieben wird, sowohl für die Client- als auch die Serverversionen. Experiment: Die von der Lizenzierungsrichtlinie aktivierten Merkmale ermitteln Wie bereits erwähnt, unterstützt Windows mehr als 100 Merkmale, die durch den Softwarelizenzierungsmechanismus aktiviert werden. Diese Richtlinieneinstellungen bestimmen verschiedene Unterschiede nicht nur zwischen einer Client- und einer Serverinstallation, sondern auch zwischen den einzelnen Editionen (oder SKU) des Betriebssystems, z. B. die Unterstützung für BitLocker (verfügbar in der Serverversion und in den Editionen Pro und Enterprise der Clientversion). Mit dem Tool SlPolicy aus den Downloads zu diesem Buch können Sie viele dieser Richtlinienwerte anzeigen lassen. Die Richtlinieneinstellungen sind nach Einrichtungen (»facilities«) geordnet, also nach den Besitzermodulen, auf die die Richtlinie angewendet wird. Um eine Liste aller Einrichtungen einzusehen, die das Tool kennt, führen Sie SlPolicy.exe mit dem Schalter -f aus: C:\>SlPolicy.exe -f Software License Policy Viewer Version 1.0 (C)2016 by Pavel Yosifovich Desktop Windows Manager Explorer Fax Kernel IIS ... Wenn Sie den Namen einer Einrichtung hinter dem Schalter angeben, wird der Richtlinienwert für diese Einrichtung angezeigt. Wollen Sie sich beispielsweise die Grenzwerte für CPUs und den Arbeitsspeicher ansehen, so müssen Sie die Kernel-Einrichtung verwenden. Auf einem Computer mit Windows 10 Pro ergibt sich dabei folgende Ausgabe: C:\>SlPolicy.exe -f Kernel Software License Policy Viewer Version 1.0 (C)2016 by Pavel Yosifovich Kernel ------ → Die Architektur im Überblick Kapitel 2 63
Maximum allowed processor sockets: 2 Maximum memory allowed in MB (x86): 4096 Maximum memory allowed in MB (x64): 2097152 Maximum memory allowed in MB (ARM64): 2097152 Maximum physical page in bytes: 4096 Device Family ID: 3 Native VHD boot: Yes Dynamic Partitioning supported: No Virtual Dynamic Partitioning supported: No Memory Mirroring supported: No Persist defective memory list: No Bei Windows Server 2012 R2 Datacenter Edition dagegen sieht das Ergebnis wie folgt aus: Kernel -----Maximum allowed processor sockets: 64 Maximum memory allowed in MB (x86): 4096 Maximum memory allowed in MB (x64): 4194304 Add physical memory allowed: Yes Add VM physical memory allowed: Yes Maximum physical page in bytes: 0 Native VHD boot: Yes Dynamic Partitioning supported: Yes Virtual Dynamic Partitioning supported: Yes Memory Mirroring supported: Yes Persist defective memory list: Yes Testbuild Es gibt eine besondere interne Debugversion von Windows, die als Testbuild (oder manchmal auch überprüfter Build) bezeichnet wird. (Extern erhältlich ist sie nur für Windows 8.1 und früher und nur mit MSDN-Abonnement.) Dabei handelt es sich um eine Neukompilierung des Windows-Quellcodes mit dem Flag DBG, das dafür sorgt, dass Kompilierungszeitcode, Code für bedingtes Debugging und Ablaufverfolgungscode eingeschlossen werden. Um den Maschinencode leichter verständlich zu machen, erfolgt keine Nachbearbeitung der Windows-Binärdateien, die sonst zur Optimierung des Codelayouts und damit für eine schnelle Ausführung durchgeführt wird (siehe den Abschnitt »Debugging performance-optimized code« in der Hilfedatei zu den Debugging Tools for Windows). Der Testbuild ist hauptsächlich als Hilfestellung für die Entwickler von Gerätetreibern gedacht, da er eine strengere Fehlerprüfung von Kernelmodusfunktionen durchführt, die von Gerätetreibern oder anderem Systemcode aufgerufen werden. Wenn beispielsweise ein Treiber (oder anderer Kernelmoduscode) eine Systemfunktion auf ungültige Weise aufruft (indem er etwa ein 64 Kapitel 2 Systemarchitektur
Spinlock auf der falschen Interrupt-Anforderungsebene erwirbt) und das Problem erkannt wird, da die Funktion die Parameter prüft, so hält das System die Ausführung an, damit keine Datenstrukturen beschädigt werden, was später zu einem Absturz führen könnte. Da ein kompletter überprüfter Build häufig instabil ist und in den meisten Umgebungen nicht ausgeführt werden kann, stellt Microsoft ab Windows 10 nur einen überprüften Kernel mit HAL zur Verfügung. Dadurch können Entwickler diese Einrichtungen nutzen, ohne sich mit den Problemen beschäftigen zu müssen, die ein vollständiger überprüfter Build hervorruft. Der überprüfte Kernel und die überprüfte HAL stehen kostenlos im WDK zur Verfügung, und zwar im Verzeichnis \Debug des Stamminstallationspfads. Ausführliche Anleitungen zu ihrer Nutzung erhalten Sie im Abschnitt »Installing Just the Checked Operating System and HAL« in der WDK-Dokumentation. Experiment: Wird der Testbuild ausgeführt? Im Lieferumfang von Windows ist kein Werkzeug enthalten, mit dem Sie bestimmen können, ob Sie den Testbuild oder den Einzelhandelsbuild (freier Build) des Kernels ausführen. Allerdings ist diese Information über die Eigenschaft Debug der WMI-Klasse Win32_OperatingSystem verfügbar (Windows Management Instrumentation). Mit dem folgenden PowerShell-Skript können Sie diese Eigenschaft anzeigen lassen. (Das können Sie ausprobieren, indem Sie einen PowerShell-Skripthost öffnen.) PS C:\Users\pavely> Get-WmiObject win32_operatingsystem | select debug debug ----False In diesem Beispiel hat die Eigenschaft Debug den Wert False, was bedeutet, dass nicht der Testbuild ausgeführt wird. Eine Menge des zusätzlichen Codes in den Binärdateien des Testbuilds kommt durch die Makros ASSERT und NT_ASSERT zustande, die in der WDK-Headerdatei Wdm.h definiert sind und in der WDK-Dokumentation erklärt werden. Diese Makros testen eine Bedingung, z. B. die Gültigkeit einer Datenstruktur oder eines Parameters. Lautet das Ergebnis FALSE, so rufen die Makros entweder die Kernelmodusfunktion RtlAssert auf, die wiederum DbgPrintEx aufruft, um den Text der Debugnachricht an den Debugnachrichtenpuffer zu senden, oder sie geben einen Assertion-Interrupt aus, bei dem es sich auf x64- und x86-Systemen um den Interrupt 0x2B handelt. Ist ein Kerneldebugger angeschlossen und sind die entsprechenden Symbole geladen, so wird die Nachricht automatisch angezeigt und der Benutzer gefragt, was wegen des Assertionfehlers unternommen werden soll (Haltepunkt setzen, ignorieren, Prozess abbrechen oder Thread abbrechen). Ist das System ohne Kerneldebugger gestartet worden (also ohne die Option debug in der Bootkonfigurationsdatenbank) und ist daher kein Kerneldebugger angeschlossen, so führt der Fehlschlag eines Assertiontests zur Fehlerprüfung (und zum Absturz) des Systems. Eine Liste der Assertionchecks, die einige der Kernelunterstützungsroutinen durchführen, finden Sie im Abschnitt »Checked Build ASSERTs« in der WDK-Dokumentation (allerdings wird diese Liste nicht mehr gepflegt und ist veraltet). Die Architektur im Überblick Kapitel 2 65
Der überprüfte Build kann auch sehr nützlich zum Testen von Benutzermoduscode sein, da das System bei diesem Build ein anderes Timing hat. (Das liegt daran, dass im Kernel zusätzliche Überprüfungen erfolgen und die Komponenten ohne Optimierung kompiliert worden sind.) Oft hängen Fehler bei der Synchronisierung mehrerer Threads von bestimmten Timingbedingungen ab. Wenn Sie Ihre Tests auf einem System mit einem überprüften Build ausführen (oder zumindest mit überprüftem Kernel und HAL), dann können aufgrund des veränderten Systemtimings Bugs zutage treten, die sich auf einem normalen System nicht zeigen. Die virtualisierungsgestützte Sicherheitsarchitektur im Überblick Durch die Trennung zwischen Benutzer- und Kernelmodus wird das Betriebssystem von Benutzermoduscode abgeschirmt, ob gefährlich oder nicht. Wenn unerwünschter Kernelmoduscode es jedoch ins System schafft (durch eine noch nicht gepatchte Schwachstelle im Kernel oder einem Treiber oder weil der Benutzer dazu verführt wurde, einen schädlichen oder angreifbaren Treiber zu installieren), ist das System im Grunde genommen bereits geknackt, da jeglicher Kernmoduscode Vollzugriff auf das gesamte System hat. Die in Kapitel 1 beschriebenen Technologien, die den Hypervisor nutzen, um zusätzlichen Schutz gegen Angriffe zu bieten, bilden zusammen die virtualisierungsgestützten Sicherheitseinrichtungen (Virtualization-Based Security, VBS), die die normale, vom Prozessor durchgeführte Trennung aufgrund von Rechten durch virtuelle Vertrauensebenen (Virtual Trust Levels, VTLs) erweitert. VTLs bieten nicht nur eine neue, orthogonale Möglichkeit, um den Zugriff auf Speicher-, Hardware- und Prozessorressourcen zu isolieren, sondern erfordern auch, dass neuer Code und neue Komponenten mit den höheren Vertrauensebenen umgehen können. Der reguläre Kernel und die regulären Treiber laufen auf VTL 0, weshalb ihnen nicht gestattet werden darf, VTL-1-Ressourcen zu steuern und zu definieren. Abb. 2–3 zeigt die Architektur von Windows 10 Enterprise und Windows Server 2016 mit aktiver VBS. (Manchmal wird auch die Bezeichnung Virtual Secure Mode oder kurz VSM verwendet.) Bei Windows 10 Version 1607 und Server 2016 ist VBS standardmäßig aktiv, wenn die Hardware dies zulässt. Bei älteren Versionen von Windows 10 können Sie VBS über eine Richtlinie oder im Dialogfeld Hinzufügen von Windows-Funktionen aktivieren (wählen Sie dazu die Option Isolierter Benutzermodus). 66 Kapitel 2 Systemarchitektur
VTL 0 VTL 1 Benutzermodus Isolierter Benutzermodus (IUM) Kernelmodus Sicherer Kernel Hypervisor Hyper-V SLAT Hardware I/O MMU Abbildung 2–3 VBS-Architektur von Windows 10 und Server 2016 Wie Sie in Abb. 2–3 sehen, läuft der zuvor beschriebene Benutzer- und Kernelcode wie schon in Abb. 2–1 oberhalb des Hypervisors Hyper-V. Der Unterschied besteht darin, dass es bei aktivierter VBS jetzt die VTL 1 gibt, die einen eigenen sicheren Kernel im privilegierten Prozessormodus (Ring 0 auf x86/x64) enthält. Ebenso gibt es mit dem isolierten Benutzermodus (IUM) jetzt einen Laufzeit-Benutzermodus, der im nicht privilegierten Modus (Ring 3) läuft. Bei dieser Architektur befindet sich der sichere Kernel in seiner eigenen, separaten Binärdatei, die als securekernel.exe auf der Festplatte steht. Der IUM ist sowohl eine Umgebung, die zulässige Systemaufrufe regulärer Benutzermodus-DLLs beschränkt (und damit auch, welche dieser DLLs geladen werden können), als auch ein Framework, das besondere, nur auf VTL 1 ausführbare sichere Systemaufrufe hinzufügt. Diese zusätzlichen Systemaufrufe werden auf ähnliche Weise verfügbar gemacht wie die regulären, nämlich durch eine interne Systembibliothek namens lumdll.dll (die VTL-1-Version von Ntdll.dll) und eine Bibliothek für das Windows-Teilsystem namens lumbase.dll (die VTL-Version von Kernelbase.dll). Diese Implementierung des IUM verwendet zum größten Teil dieselben Standardbibliotheken der Win32-API, was den Speicheroverhead von VTL-1-Benutzermodusanwendungen zu reduzieren hilft, da im Grunde genommen derselbe Benutzermoduscode vorhanden ist wie in ihren VTL-0-Gegenstücken. Mechanismen für das Kopieren bei Schreibvorgängen (siehe Kapitel 5) verhindern, dass VTL-0-Anwendungen Änderungen an Binärdateien vornehmen, die auf VTL 1 genutzt werden. Bei VBS gelten die üblichen Regeln für Benutzer- und Kernelmodus, die jetzt aber noch um VTL-Aspekte erweitert werden. Kernelmoduscode auf VTL 0 darf keinen Benutzermoduscode auf VTL 1 anfassen, da VTL 1 höhere Rechte hat. Allerdings darf Benutzermoduscode auf VTL 1 auch keinen Kernelmoduscode auf VTL 0 anfassen, da ein solcher Zugriff vom Benutzermodus (Ring 3) auf den Kernelmodus (Ring 0) grundsätzlich untersagt ist. VTL-1-Benutzermodusanwendungen müssen außerdem weiterhin die regulären Windows-Systemaufrufe und die zugehörigen Zugriffsprüfungen durchlaufen, um auf Ressourcen zuzugreifen. Das können Sie sich auf folgende Weise verdeutlichen: Rechteebenen (Benutzer- oder Kernelmodus) sorgen für Macht, VTLs dagegen für Isolierung. Eine VTL-1-Benutzermodusanwendung ist nicht mächtiger als eine VTL-0-Anwendung oder ein VTL-0-Treiber, aber davon Die virtualisierungsgestützte Sicherheitsarchitektur im Überblick Kapitel 2 67
isoliert. In vielen Fällen sind VTL-1-Anwendungen nicht nur nicht mächtiger, sondern sogar weniger mächtig. Da der sichere Kernel nicht die volle Palette der Systemfähigkeiten implementiert, wählt er aus, welche Systemaufrufe er an den VTL-0-Kernel weiterleitet (weshalb der sichere Kernel auch als Proxykernel bezeichnet wird). Jede Art von E/A, ob im Zusammenhang mit Dateien, dem Netzwerk oder der Registrierung, ist komplett verboten. Auch Grafik kommt nicht in Betracht, und es darf auch mit keinem einzigen Treiber kommuniziert werden. Da der sichere Kernel jedoch auf VTL 1 und im Kernelmodus läuft, hat er vollständigen Zugriff auf den Arbeitsspeicher und die Ressourcen auf VTL 0. Er kann den Hypervisor nutzen, um den VTL-0-Betriebssystemzugriff auf bestimmte Speicherbereiche einzuschränken. Dies geschieht mithilfe von SLAT (Second Level Address Translation), einer CPU-Hardwaretechnologie. Sie bildet die Grundlage von CredentialGuard, mit dem Geheimnisse in solchen Bereichen gespeichert werden. Der sichere Kernel kann SLAT auch nutzen, um die Ausführung von Speicherbereichen zu untersagen und zu steuern, was eine entscheidende Bedingung von DeviceGuard ist. Um zu verhindern, dass normale Gerätetreiber Hardwaregeräte für den direkten Speicherzugriff nutzen, verwendet das System noch eine weitere Hardware, nämlich die E/A-Speicherverwaltungseinheit (Memory Management Unit, MMU), die den Speicherzugriff für Geräte im Grunde genommen virtualisiert. Damit kann verhindert werden, dass Gerätetreiber den direkten Speicherzugriff (Direct Memory Access, DMA) nutzen, um unmittelbar auf die physischen Speicherbereiche des Hypervisors oder des sicheren Kernels zuzugreifen. Mit einer solchen Vorgehensweise könnte SLAT umgangen werden, da dabei kein virtueller Speicher verwendet wird. Da der Hypervisor die erste Systemkomponente ist, die der Bootloader lädt, kann er die SLAT und die I/O MMU nach seinem Bedarf programmieren und die Ausführungsumgebungen VTL 0 und VTL 1 definieren. Dann wird der Bootloader in VTL 1 erneut ausgeführt, um den sicheren Kernel zu laden, der das System nach seinen Bedürfnissen weiter konfiguriert. Erst danach wird die VTL gesenkt, sodass der normale Kernel in seinem VTL-0-Gefängnis ausgeführt wird, aus dem er nicht entkommen kann. Da Benutzermodusprozesse auf VTL 1 isoliert sind, kann potenziell schädlicher Code zwar keinen größeren Einfluss auf das System gewinnen, aber heimlich versuchen, sichere Systemaufrufe durchzuführen (die ihm erlauben würden, seine eigenen Geheimnisse zu signieren), und möglicherweise schädliche Interaktionen mit anderen VTL-1-Prozessen oder dem sicheren Kernel hervorrufen. Daher dürfen nur besonders signierte Binärdateien, sogenannte Trustlets, auf VTL 1 ausgeführt werden. Jedes Trustlet weist einen eindeutigen Bezeichner und eine Signatur auf, und der sichere Kernel verfügt über hartcodierte Kenntnisse darüber, welche Trustlets bisher erstellt wurden. Daher ist es unmöglich, neue Trustlets zu erstellen, ohne auf den sicheren Kernel zuzugreifen (den nur Microsoft anfassen kann). Auch vorhandene Trustlets können nicht manipuliert werden, da dadurch ihre besondere Microsoft-Signatur verloren ginge. Weitere Informationen über Trustlets erhalten Sie in Kapitel 3. Die Ergänzung um den sicheren Kernel und VBS ist eine spannende Entwicklung in der modernen Betriebssystemarchitektur. Mit weiteren Hardwareänderungen an Bussen wie PCI und USB wird es bald möglich sein, eine ganze Klasse von sicheren Geräten zu unterstützen, die in Kombination mit einer minimalistischen sicheren HAL, einem sicheren Plug-&-Play-Manager und einem sicheren Benutzermodus-Geräteframework bestimmten VTL-1-Anwendungen ei- 68 Kapitel 2 Systemarchitektur
nen direkten und gesonderten Zugriff auf eigens dafür vorgesehene Dienste erlauben können, z. B. für biometrische oder Smartcard-Eingaben. Neue Versionen von Windows 10 werden sehr wahrscheinlich solche technischen Fortschritte nutzen. Hauptsystemkomponenten Nachdem wir uns einen Überblick über die Architektur von Windows verschafft haben, wollen wir uns nun eingehender mit der inneren Struktur und mit der Rolle der einzelnen Komponenten befassen. Abb. 2–4 zeigt eine detaillierte und vollständigere Ansicht der Kernarchitektur und der Komponenten von Windows als Abb. 2–1. Beachten Sie aber, dass dies immer noch nicht alle Komponenten sind. (Insbesondere fehlen die Netzwerkkomponenten, die in Kapitel 10, »Netzwerk«, von Band 2 behandelt werden.) System prozesse Umgebungs teilsysteme Dienste Anwendungen Wininit SitzungsManager Spooler SvcHost Benutzer prozess Teilsystem-DLLs Teilsystem-DLLs Winlogon SCM CSRSS Teilsystem-DLLs NTDLL.DLL Benutzermodus Kernelmodus Systemdienst-Dispatcher ALPC Konfigura tions-Manager ObjektManager ProzessManager SpeicherManager Plug-&-PlayManager EnergieManager Geräte- und Dateisystemtreiber Sicherheits verweis monitor E/AManager Windows USER und GDI Kernel Hardwareabstraktionsschicht (HAL) Grafik treiber HAL-Erweiterungen Hypervisor Hyper-V Abbildung 2–4 Architektur von Windows Die folgenden Abschnitte enthalten ausführliche Beschreibungen der wichtigsten Elemente in diesem Diagramm. Kapitel 8 in Band 2 erläutert die primären Steuermechanismen, die das System einsetzt (wie den Objekt-Manager, Interrupts usw.), Kapitel 11, »Starten und Herunterfahren«, beschreibt den Vorgang des Startens und Herunterfahrens und Kapitel 9 gibt ausführliche Informationen über Verwaltungsmechanismen wie die Registrierung, Dienstprozesse und WMI. In anderen Kapiteln werden noch genauere Erklärungen zu den internen Strukturen und der Funktionsweise von Schlüsselaspekten wie Prozessen und Threads, Speicherverwaltung, Sicherheit, dem E/A-Manager, der Speicherplatzverwaltung, dem Cache-Manager, dem Windows-Dateisystem (NTFS) und dem Netzwerk gemacht. Hauptsystemkomponenten Kapitel 2 69
Umgebungsteilsysteme und Teilsystem-DLLs Ein Umgebungsteilsystem hat die Aufgabe, Anwendungsprogrammen einen Teil der grundlegenden Dienste bereitzustellen, die es im Exekutivsystem von Windows gibt. Jedes Teilsystem bietet dabei Zugriff auf eine andere Auswahl der nativen Windows-Dienste. Das bedeutet, dass eine Anwendung in einem Teilsystem Dinge tun kann, die einer Anwendung in einem anderen Teilsystem nicht möglich sind. Beispielsweise kann eine Windows-Anwendung die SUA-Funk tion fork nicht nutzen. Jedes ausführbare Abbild (.exe) ist an genau ein Teilsystem gebunden. Wird das Abbild ausgeführt, so untersucht der Code zur Prozesserstellung den Typcode für das Teilsystem im Abbild-Header, sodass er das entsprechende Teilsystem über den neuen Prozess informieren kann. Der Typcode wird mit der Option /SUBSYSTEM des Linkers von Microsoft Visual Studio angegeben (oder im Eintrag SubSystem auf der Seite Linker/System in den Projekteigenschaften). Wie bereits erwähnt, rufen Benutzeranwendungen die Windows-Systemdienste nicht direkt auf, sondern durchlaufen dazu eine oder mehrere Teilsystem-DLLs. Diese Bibliotheken exportieren die dokumentierte Schnittstelle, die die mit dem Teilsystem verlinkten Programme aufrufen können. Beispielsweise implementieren die DLLs des Windows-Teilsystems (wie Kernel32.dll, Advapi32.dll, User32.dll und Gdi32.dll) die Funktionen der Windows-API, während die DLL des SUA-Teilsystems (Psxdll.dll) zur Implementierung der SUA-API-Funktionen dient (auf Windows-Versionen, die POSIX unterstützen). Experiment: Den Teilsystemtyp des Abbilds einsehen Den Teilsystemtyp des Abbilds können Sie sich mit dem Werkzeug Dependency Walker (Depends.exe) ansehen. In den folgenden Abbildungen sehen Sie z. B. die Typen für die beiden Windows-Abbilder Notepad.exe (der einfache Windows-Texteditor) und Cmd.exe (die Windows-Eingabeaufforderung): → 70 Kapitel 2 Systemarchitektur
Dies zeigt, dass es sich bei Notepad um ein GUI-Programm handelt, bei Cmd dagegen um ein Konsolenprogramm. Das scheint zu bedeuten, dass es zwei verschiedene Teilsysteme für grafische und für textgestützte Programme gibt, allerdings gibt es in Wirklichkeit nur ein Windows-Teilsystem. GUI-Programme können auch Konsolen haben (durch Aufrufen der Funktion AllocConsole), und genauso gut können Konsolenprogramme grafische Benutzeroberflächen aufrufen. Wenn eine Anwendung eine Funktion in einer Teilsystem-DLL aufruft, können drei verschiedene Situationen vorliegen: QQ Die Funktion ist vollständig im Benutzermodus innerhalb der DLL implementiert. Das heißt, es werden keine Nachrichten an den Prozess des Umgebungsteilsystems gesendet und keine Dienste des Windows-Exekutivsystems aufgerufen. Die Funktion läuft im Benutzermodus ab, und die Ergebnisse werden an den Aufrufer zurückgegeben. Beispiele für solche Funktionen sind GetCurrentProcess (die immer -1 zurückgibt; dieser Wert ist so definiert, dass er in allen prozessbezogenen Funktionen auf den aktuellen Prozess verweist) und GetCurrentProcessId. (Da sich die Prozesskennung für einen laufenden Prozess nicht ändert, wird sie von einem zwischengespeicherten Speicherort abgerufen, um einen Aufruf im Kernel zu vermeiden.) QQ Die Funktion erfordert einen oder mehrere Aufrufe der Windows-Exekutive. Beispielsweise müssen die Funktionen ReadFile und WriteFile die zugrunde liegende (und für die Verwendung im Benutzermodus nicht dokumentierten) Dienste NtReadFile bzw. NtWriteFile des E/A-Systems von Windows aufrufen. QQ Für die Funktion muss einige Arbeit im Prozess des Umgebungsteilsystems erledigt werden. (Diese Prozesse laufen im Benutzermodus und sind dafür verantwortlich, den Status der Clientanwendungen, die unter ihrer Kontrolle ausgeführt werden, zu erhalten.) In diesem Fall wird eine Client/Server-Anforderung an das Umgebungsteilsystem gestellt. Dazu wird eine ALPAC-Nachricht (siehe Kapitel 8 in Band 2) an das Teilsystem gesendet, um eine Operation auszuführen. Die Teilsystem-DLL wartet dann auf eine Antwort, bevor sie die Steuerung an den Aufrufer zurückgibt. Bei einigen Funktionen kann eine Kombination des zweiten und des dritten Falls vorliegen, etwa bei den Windows-Funktionen CreateProcess und ExitWindowsEx. Start von Teilsystemen Teilsysteme werden vom Prozess Smss.exe, dem Sitzungs-Manager, gestartet. Die Informationen über den Start von Teilsystemen werden unter dem Registrierungsschlüssel HKLM\SYSTEM\ CurrentControlSet\Control\Session Manager\SubSystems gespeichert. Die Werte unter diesem Schlüssel sind in Abb. 2–5 dargestellt (Screenshot von Windows 10 Pro). Hauptsystemkomponenten Kapitel 2 71
Abbildung 2–5 Registrierungs-Editor mit Informationen über Windows-Teilsysteme Der Wert Required führt die Teilsysteme auf, die beim Start des Systems geladen werden. In diesem Wert stehen die beiden Strings Windows und Debug. Der Wert Windows enthält die Dateispezifikation des Windows-Teilsystems Csrss.exe, was für Client/Server Runtime Subsystem steht. Debug dagegen ist leer (dieser Wert wird seit Windows XP nicht mehr benötigt, wird aber aus Gründen der Kompatibilität beibehalten) und tut daher gar nichts. Der Wert Optional gibt optionale Teilsysteme an und ist hier auch leer, da SUA auf Windows 10 nicht mehr zur Verfügung steht. Wäre das noch der Fall, würde der POSIX-Datenwert auf einen anderen Wert zeigen, der wiederum auf Psxss.exe verweist (den Prozess für das POSIX-Teilsystem). Da dieser Wert in Optional stehen würde, so würde er »bei Bedarf geladen«, also bei der ersten Begegnung mit einem POSIX-Abbild. Der Registrierungswert Kmode enthält den Namen der Datei für den Kernelmodusteil des Windows-Teilsystems, Win32k.sys (die weiter hinten in diesem Kapitel noch erklärt wird). Sehen wir uns nun die Windows-Umgebungsteilsysteme genauer an. Windows-Teilsystem Windows wurde zwar dafür ausgelegt, mehrere unabhängige Umgebungsteilsysteme zu unterstützen, doch wenn jedes Teilsystem den gesamten Code zur Handhabung der Fenster und der Anzeige-E/A implementierte, würde das zu einer erheblichen Duplizierung von Systemfunktionen führen und letzten Endes sowohl die Größe als auch die Leistung des Systems beeinträchtigen. Da Windows das primäre Teilsystem sein sollte, brachten die Designer diese Grundfunktionen dort unter, sodass andere Teilsysteme, etwa wie SUA, Dienste des Windows-Teilsystems aufrufen mussten, um eine Anzeige-E/A durchzuführen. Aufgrund dieser Designentscheidung ist das Windows-Teilsystem eine erforderliche Komponente für alle Windows-Systeme, selbst für Serversysteme, auf denen keine Benutzer für interaktive Sitzungen angemeldet sind. Der Prozess ist daher als kritischer Prozess gekennzeichnet, d. h., sollte er aus irgendeinem Grund beendet werden, so stürzt das System ab. 72 Kapitel 2 Systemarchitektur
Das Windows-Teilsystem besteht aus den folgenden Hauptkomponenten: QQ Für jede Sitzung lädt eine Instanz des Umgebungsteilsystemprozesses (Csrss.exe) vier DLLs (Basesrv.dll, Winsrv.dll, Sxssrv.dll und Csrsrv.dll), die Unterstützung für Folgendes bieten: • Verschiedene Ordnungsaufgaben im Zusammenhang mit dem Erstellen und Löschen von Prozessen und Threads • Herunterfahren von Windows-Anwendungen (durch die API ExitWindowsEx) • Bereitstellen von Zuordnungen von .ini-Dateien zu Speicherorten in der Registrierung, um für Rückwärtskompatibilität zu sorgen • Senden bestimmter Kernelbenachrichtigungen (z. B. derjenigen aus dem Plug-&-PlayManager) als Windows-Nachrichten (WL_DEVICECHANGE) an Windows-Anwendungen • Teile der Unterstützung für 16-Bit-VDM-Prozesse (Virtual DOS Machine; nur auf 32-BitWindows) • Unterstützung für Side-by-Side (SxS)/Fusion und Manifestcaches • Verschiedene Unterstützungsfunktionen für natürliche Sprache, um eine Zwischenspeicherung zu ermöglichen Der Kernelmoduscode für den Roheingabethread und den Desktopthread (verantwortlich für den Mauscursor, Tastatureingaben und den Umgang mit dem Desktop-Fenster) wird innerhalb der Threads ausgeführt, die in Winsrv.dll laufen. Daneben enthalten die Csrss.exe-Instanzen im Zusammenhang mit interaktiven Benutzersitzungen eine fünfte DLL namens Cdd.dll (Canonical Display Driver). Sie ist für die Kommunikation mit der DirectX-Unterstützung im Kernel (siehe nachfolgende Erklärung) bei jeder vertikalen Aktualisierung (VSync) zuständig, um den sichtbaren Desktop ohne herkömmliche hardwarebeschleunigte GDI-Unterstützung zu zeichnen. QQ Ein Kernelmodus-Gerätetreiber (Win32k.sys) mit folgendem Inhalt: • Der Fenster-Manager, der die Fensteranzeige steuert, die Bildschirmausgabe verwaltet, Eingaben von der Tastatur, der Maus und anderen Geräten erfasst und Benutzernachrichten an Anwendungen übergibt • Die Grafikgerätschnittstelle (Graphics Device Interface, GDI), bei der es sich um eine Bibliothek von Funktionen für grafische Ausgabegeräte handelt und die Funktionen für das Zeichnen von Linien, Text und Formen sowie zur Bearbeitung von Grafiken enthält • Wrapper für die DirectX-Unterstützung, die in einem anderen Kerneltreiber implementiert ist (Dxgkrnl.sys) QQ Der Konsolenhostprozess (Conhost.exe), der Unterstützung für Konsolenanwendung bietet QQ Der Desktop Window Manager (Dwm.exe), der es ermöglicht, die Darstellung der sichtbaren Fenster mithilfe von CDD und DirectX zu einer einzigen Oberfläche zusammenzufassen QQ Teilsystem-DLLs (wie Kernel32.dll, Advapi32.dll, User32.dll und Gdi32.dll), die dokumentierte Windows-API-Funktionen in die geeigneten und (für den Benutzermodus) nicht dokumentierten Kernelmodus-Systemdienstaufrufe in Ntoskrnl.exe und Win32k.sys übersetzen Hauptsystemkomponenten Kapitel 2 73
QQ Grafikgerätetreiber für hardwareabhängige Grafikanzeigetreiber, Druckertreiber und Video-Miniporttreiber Nach einem Refactoring der Windows-Architektur unter der Bezeichnung MinWin bestehen die Teilsystem-DLLs jetzt generell aus spezifischen Bibliotheken, die APISets implementieren, zu der Teilsystem-DLL verknüpft und mithilfe eines besonderen Umleitungsverfahrens aufgelöst werden. Weitere Informationen über diesen Refactoringvorgang erhalten Sie im Abschnitt »Der Abbildlader« in Kapitel 3. Win32k.sys in Windows 10 Die grundlegenden Anforderungen an die Fensterverwaltung in Windows 10 können je nach vorliegendem Gerät erheblich schwanken. Beispielsweise braucht ein voll ausgestatteter Desktopcomputer alle Möglichkeiten des Fenster-Managers wie veränderliche Fenstergrößen, Besitzerfenster, Kindfenster usw. Windows Mobile 10 auf Smartphones und kleinen Tablets dagegen braucht viele dieser Merkmale nicht, da immer nur ein Fenster im Vordergrund steht und nicht minimiert, vergrößert, verkleinert oder auf ähnliche Weise verändert werden kann. Das Gleiche gilt auch für IoT-Geräte, von denen viele nicht einmal eine Anzeige haben. Aus diesem Grunde wurde der Funktionsumfang von Win32k.sys auf verschiedene Kernelmodule aufgeteilt, wobei auf einem System nicht unbedingt alle diese Module gebraucht werden. Das reduziert die Angriffsfläche des Fenster-Managers erheblich, da der Code an Komplexität verliert und viele seiner älteren Teile entfernt wurden. Betrachten Sie dazu die folgenden Beispiele: QQ Auf Smartphones (Windows Mobile 10) lädt Win32k.sys nur Win32kMin.sys und Win32kBase.sys. QQ Auf Desktopsystemen lädt Win32k.sys nur Win32kBase.sys und Win32kFull.sys. QQ Auf bestimmten IoT-Geräten braucht Win32k.sys lediglich Win32kBase.sys. Anwendungen rufen die Standardfunktion USER auf, um Steuerelemente der Benutzerschnittstelle wie Fenster und Schaltflächen auf der Anzeige zu erstellen. Der Fenster-Manager vermittelt diese Anforderungen an die GDI, die sie wiederum an die Grafikgerätetreiber übergibt, wo sie für das Anzeigegerät formatiert werden. Um die Unterstützung für die Videoanzeige zu vervollständigen, wird ein Anzeigetreiber mit einem Video-Miniporttreiber gekoppelt. Die GDI stellt einen Satz von 2-D-Standardfunktionen bereit, mit denen Anwendungen mit Grafikgeräten kommunizieren können, ohne etwas über sie zu wissen. Die GDI-Funktionen vermitteln zwischen Anwendungen und Grafikgeräten wie Anzeige- und Druckertreibern. Die GDI interpretiert die Anforderungen der Anwendung für Grafikausgabe und sendet die Anforderungen an die Grafikgerätetreiber. Außerdem stellt sie eine Standardschnittstelle bereit, über die die Anwendungen verschiedene Grafikausgabegeräte verwenden können. Diese Schnittstelle macht den Anwendungscode von den Hardwaregeräten und deren Treibern unabhängig. Die GDI schneidet ihre Nachrichten auf die Fähigkeiten der Geräte zu, wobei sie eine Anforderung oft in handhabbare Teile zerlegt. Beispielsweise verstehen manche Geräte die Anweisung, eine Ellipse zu zeichnen, während die GDI bei anderen den Befehl als eine Folge von Pixeln 74 Kapitel 2 Systemarchitektur
deuten muss, die an bestimmten Koordinaten zu platzieren sind. Weitere Informationen über die Architektur von Grafik- und Videotreibern finden Sie im Abschnitt »Design Guide« des Kapitels »Display (Adapters and Monitors)« des WDK. Da ein Großteil des Teilsystems – insbesondere die Funktionen zur Anzeige-E/A – im Kernelmodus läuft, führen nur wenige Windows-Funktionen dazu, dass eine Nachricht an den Windows-Teilsystemprozess gesendet wird, nämlich die Erstellung und Beendigung von Prozessen und Threads und die Zuordnung von DOS-Laufwerksbuchstaben (etwa durch subst.exe). Im Allgemeinen veranlasst eine laufende Windows-Anwendung wenn überhaupt nur wenige Kontextwechsel zum Windows-Teilsystemprozess. Ausnahmen sind lediglich die erforderlichen Kontextwechsel, um die neue Position des Mauscursors zu zeichnen, die Tastatureingaben zu verarbeiten und den Bildschirm über CDD wiederzugeben. Konsolenfensterhost Im ursprünglichen Design des Windows-Teilsystems war der Teilsystemprozess (Csrss.exe) für die Verwaltung des Konsolenfensters verantwortlich, und jede Konsolenanwendung (wie Cmd.exe, die Eingabeaufforderung) kommunizierte mit ihm. Ab Windows 7 wurde für jedes Konsolenfenster im System ein eigener Prozess verwendet, nämlich der Konsolenfensterhost (Conhost.exe). (Ein Konsolenfenster kann von mehreren Konsolenanwendungen gemeinsam genutzt werden. Wenn Sie beispielsweise eine Eingabeaufforderung von einer anderen Eingabeaufforderung aus aufrufen, so verwendet die zweite Eingabeaufforderung das Konsolenfenster der ersten.) Die Einzelheiten des Konsolenhosts von Windows 7 wurden in Kapitel 2 der sechsten Ausgabe dieses Buches erklärt. Mit Windows 8 wurde die Konsolenarchitektur erneut geändert. Den Prozess Conhost. exe gibt es nach wie vor, aber er wird jetzt vom Konsolentreiber (\Windows\System32\Drivers\ConDrv.sys) von dem konsolengestützten Prozess aus ausgelöst (also nicht mehr von Csrss.exe aus wie in Windows 7). Der betreffende Prozess kommuniziert mit Conhost.exe über den Konsolentreiber (ConDrv.sys), indem er Lese-, Schreib-, E/A-Steuerungs- und andere Arten von E/A-Anforderungen sendet. Conhost.exe fungiert als Server und der Prozess, der die Konsole nutzt, als Client. Dank dieser Änderung ist es nicht mehr nötig, dass Csrss. exe Tastatureingaben (im Rahmen des Roheingabethreads) empfängt, durch Win32k.sys an Conhost.exe sendet und dann mit ALPC an Cmd.exe schickt. Stattdessen kann die Anwendung für die Eingabeaufforderung Eingaben über Lese/-Schreib-Eingaben und -Ausgaben direkt vom Konsolentreiber empfangen, wodurch überflüssige Kontextwechsel verhindert werden. Der folgende Screenshot aus Process Explorer zeigt das Handle, den Conhost.exe für das von ConDrv.sys verfügbar gemachte Geräteobjekt \Device\ConDrv geöffnet hält. (Weitere Einzelheiten zu Gerätenamen und E/A erhalten Sie in Kapitel 6.) → Hauptsystemkomponenten Kapitel 2 75
Conhost.exe ist ein Kindprozess des Konsolenprozesses (hier von Cmd.exe). Ausgelöst wird die Erzeugung von Conhost durch den Abbildlader für das Konsolenteilsystemabbild oder auf Anforderung, wenn ein GUI-Teilsystemabbild die Windows-API AllocConsole aufruft. (Natürlich sind das GUI- und das Konsolenteilsystem in dem Sinne identisch, dass beide Varianten des Windows-Teilsystemtyps darstellen.) Die Hauptarbeit übernimmt die DLL, die Conhost lädt (\Windows\SYstem32\ConhostV2.dll). Sie enthält den Großteil des Codes, der mit dem Konsolentreiber kommuniziert. Weitere Teilsysteme Wie bereits erwähnt, unterstützte Windows ursprünglich POSIX- und OS/2-Teilsysteme. Da sie jedoch nicht mehr im Lieferumfang von Windows enthalten sind, behandeln wir sie in diesem Buch auch nicht. Das allgemeine Prinzip der Teilsysteme gibt es jedoch nach wie vor, sodass das System durch neue Teilsysteme erweitert werden kann, sollte in Zukunft Bedarf dafür aufkommen. Pico-Anbieter und das Windows-Teilsystem für Linux Das herkömmliche Teilsystemmodell ist zwar erweiterbar und war offensichtlich leistungsfähig genug, um POSIX und OS/2 ein Jahrzehnt lang zu unterstützen, weist aber zwei erhebliche technische Nachteile auf, weshalb Nicht-Windows-Binärdateien keine breite Anwendung fanden, sondern nur in einigen Sonderfällen genutzt wurden: QQ Da die Teilsysteminformationen aus dem PE-Header (Portable Executable) bezogen werden, muss der Quellcode der ursprünglichen Binärdatei das Teilsystem als ausführbare Windows-PE-Datei (.exe) neu erstellen. Dadurch werden auch jegliche POSIX-Abhängigkeiten und -Systemaufrufe in Windows-Importe der Bibliothek Psxdll.dll geändert. QQ Das Teilsystem ist durch den Funktionsumfang des Win32-Teilsystems (auf dem es manchmal aufsitzt) oder des NT-Kernels eingeschränkt. Daher emuliert es nicht das von der POSIX-Anwendung angeforderte Verhalten, sondern bildet nur einen Wrapper dafür. Das kann manchmal zu leichten Kompatibilitätsstörungen führen. 76 Kapitel 2 Systemarchitektur
Außerdem war das POSIX-Teilsystem/SUA, wie der Name schon sagt, für POSIX/UNIX-Anwendungen ausgelegt, die vor Jahrzehnten den Servermarkt beherrschten, und nicht für die heute gängigen Linux-Anwendungen. Um diese Probleme zu lösen, was eine andere Herangehensweise an den Aufbau eines Teilsystems erforderlich – eine, die keinen herkömmlicher Benutzermodus-Wrapper für die Systemaufrufe der Umgebung und die Ausführung herkömmlicher PE-Abbilder erforderte. Das Projekt Drawbridge von Microsoft Research erwies sich als das ideale Mittel für einen neuen Zugang zum Thema Teilsysteme. Dies führte zur Umsetzung des Pico-Modells. Bei diesem Modell werden Pico-Anbieter verwendet. Dabei handelt es sich um benutzerdefinierte Kernelmodustreiber, die über die API PsRegisterPicoProvider Zugriff auf spezialisierte Kernelschnittstellen erhalten. Diese spezialisierten Schnittstellen bieten die folgenden beiden Vorteile: QQ Sie ermöglichen dem Anbieter, Pico-Prozesse und -Threads zu erstellen und deren Ausführungskontexte, Segmente und Speicherdaten in den zugehörigen EPROCESS- bzw. ETHREAD-Strukturen anzupassen (mehr darüber in Kapitel 3 und 4). QQ Sie ermöglichen dem Anbieter, eine breite Palette von Benachrichtigungen zu empfangen, wenn diese Prozesse oder Threads an bestimmten Systemaktivitäten wie Systemaufrufen, Ausnahmen, APCs, Seitenfehlern, Terminierungen, Kontextwechseln, Anhalten, Wiederaufnahmen usw. beteiligt sind. Ab Windows 10 Version 1607 ist ein solcher Pico-Anbieter vorhanden, nämlich Lxss.sys mit seinem Partner Lxcore.sys. Diese Treiber bilden die Pico-Anbieterschnittstelle für die Komponente WSL (Windows Subsystem for Linux). Der Pico-Anbieter empfängt fast alle möglichen Übergänge zwischen Benutzer- und Kernelmodus (z. B. Systemaufrufe und Ausnahmen). Sofern die darunter laufenden Pico-Prozesse über einen Adressraum verfügen, den der Anbieter erkennen kann, über Code verfügt, der nativ darin ausgeführt werden kann, und die Übergänge komplett transparent erfolgen, spielt der darunter liegende »echte« Kernel keine große Rolle. Wie Sie in Kapitel 3 noch sehen werden, unterscheiden sich Pico-Prozesse, die unter dem WSL-Pico-Anbieter ausgeführt werden, erheblich von normalen Windows-Prozessen. Beispielsweise fehlt ihnen die Ntdll.dll, die in jeden normalen Prozess geladen wird. Stattdessen enthält ihr Arbeitsspeicher Strukturen wie ein vDSO, ein besonderes Abbild, das es nur auf Linux/BSD-Systemen gibt. Wenn die Linux-Prozesse transparent laufen sollen, müssen sie ausgeführt werden können, ohne eine Neukompilierung als ausführbare PE-Windows-Dateien zu erfordern. Da der Windows-Kernel nicht weiß, wie er andere Abbildtypen zuordnen soll, können solche Abbilder nicht von Windows-Prozessen über die API CreateProcess gestartet werden und solche APIs auch nicht selbst aufrufen (denn schließlich wissen sie nicht einmal, dass sie auf Windows laufen). Die Unterstützung für eine solche Art von Interoperabilität bieten der Pico-Anbieter und der LXSS-Manager, ein Dienst des Benutzermodus. Ersterer implementiert eine private Schnittstelle zur Kommunikation mit dem LXSS-Manager, Letzterer eine COM-Schnittstelle zur Kommunikation mit dem besonderen Startprozess Bash.exe und dem Verwaltungsprozess Lxrun. exe. Das folgende Diagramm gibt einen Überblick über die Komponenten von WSL. Hauptsystemkomponenten Kapitel 2 77
Linux-Instanz (Pico-Prozess) Bash.exe LXSS-MangerDienst /bin/bash Benutzermodus LXCore / LXSS Kernelmodus Exekutive und Kernel Unterstützung für eine breite Palette von Linux-Anwendungen zu geben, ist ein erhebliches Unterfangen. Linux umfasst Hunderte von Systemaufrufen, ungefähr so viele wie der Windows-Kernel selbst. Der Pico-Anbieter kann zwar vorhandene Windows-Merkmale nutzen – von denen viele zur Unterstützung des ursprünglichen POSIX-Teilsystems erstellt wurden, z. B. für fork() –, doch manche Funktionalität muss er selbst neu implementieren. Obwohl beispielsweise NTFS zur Speicherung des tatsächlichen Dateisystems genutzt wird (und nicht EXTFS), verfügt der Pico-Anbieter über eine komplette Implementierung des Linux-VFS (Virtual File System) einschließlich Unterstützung von Inodes, inotify() sowie /sys, /dev und ähnlichen Linux-Dateisystem-Namensräumen mit dem entsprechenden Verhalten. Ebenso kann der Pico-Anbieter zwar WSK (Windows Sockets for Kernel) für die Netzwerkanbindung nutzen, verfügt über komplexe Wrapper um das eigentliche Socketverhalten, sodass er UNIX-Domänensockets, Linux-NetLink-Sockets und Internet-Standardsockets unterstützen kann. Manche vorhandenen Windows-Einrichtungen sind auch einfach nicht ausreichend kompatibel, wobei die Abweichungen ganz gering sein können. Beispielsweise verfügt Windows über einen Treiber für benannte Pipes (Npfs.sys), der den herkömmlichen IPC-Pipe-Mechanismus unterstützt. Er unterscheidet sich jedoch nur ein wenig, aber doch ausreichend stark von Linux-Pipes, um eine Anwendung funktionsunfähig zu machen. Das erfordert eine komplette Neuimplementierung von Pipes für Linux-Anwendungen ohne Zuhilfenahme des Kerneltreibers Npfs.sys. Da sich dieses Merkmal zurzeit noch im Beta-Stadium befindet und erheblichen Änderungen unterliegen kann, behandeln wir die internen Mechanismen dieses Teilsystems in diesem Buch nicht. Allerdings werfen wir in Kapitel 3 noch einen weiteren Blick auf Pico-Prozesse. Wenn das Teilsystem ausgereift ist, wird es wahrscheinlich eine offizielle Dokumentation auf MSDN und stabile APIs für die Interaktion mit Linux-Prozessen geben. Ntdll.dll Ntdll.dll ist eine besondere Systemunterstützungsbibliothek, die hauptsächlich für die Verwendung durch Teilsystem-DLLs und native Anwendung da ist. (Nativ bezeichnet in diesem Zusammenhang Abbilder, die nicht an ein bestimmtes Teilsystem gebunden sind.) Sie enthält Funktionen der beiden folgenden Arten: 78 Kapitel 2 Systemarchitektur
QQ Systemdienst-Dispatchstubs zu Systemdiensten der Windows-Exekutive QQ Interne Unterstützungsfunktionen, die von Teilsystemen, Teilsystem-DLLs und anderen nativen Abbilder verwendet werden Die erste Gruppe dieser Funktionen bildet eine Schnittstelle zu den Systemdiensten der Windows-Exekutive, die vom Benutzermodus aus aufgerufen werden können. Es gibt über 450 solcher Funktionen, z. B. NtCreateFile und NtSetEvent. Die meisten Möglichkeiten, die diese Funktionen bieten, sind über die Windows-API zugänglich. (Einige jedoch nicht; sie sind nur zur Verwendung durch bestimmte interne Komponenten des Betriebssystems gedacht.) Für jede dieser Funktionen enthält Ntdll.dll einen Eintrittspunkt mit demselben Namen. Der Code in der Funktion enthält die architekturspezifische Anweisung für den Übergang in den Kernelmodus, um den Systemdienst-Dispatcher aufzurufen. (Dies wird ausführlicher in Kapitel 8 von Band 2 erläutert.) Nach der Überprüfung einiger Parameter ruft der Systemdienst-Dispatcher den Kernelmodus-Systemdienst auf, der den eigentlichen Code innerhalb von Ntoskrnl.exe enthält. Das folgende Experiment zeigt, wie diese Funktionen aussehen. Experiment: Den Systemdienst-Dispatchercode anzeigen Öffnen Sie die Version von WinDbg für Ihre Systemarchitektur (also etwa die x64-Version auf 64-Bit-Windows). Wählen Sie dann im Menü File den Befehl Open Executable. Wechseln Sie zu %SystemRoot%\System32 und markieren Sie Notepad.exe. Daraufhin wird der Editor (Notepad) gestartet. Der Debugger hält am ersten Haltepunkt an. Das geschieht schon sehr früh im Lauf des Prozesses, wovon Sie sich überzeugen können, indem Sie den Befehl k (für den Aufrufstack) ausführen. Dort sehen Sie einige Funktionen, deren Namen mit Ldr beginnen, was auf den Abbildlader hindeutet. Die Hauptfunktion des Editors ist noch nicht ausgeführt worden, weshalb sein Fenster nicht angezeigt wird. Setzen Sie wie folgt einen Haltepunkt in NtCreateFile innerhalb von Ntdll.dll (der Debugger unterscheidet nicht zwischen Groß- und Kleinschreibung): bp ntdll!ntcreatefile Geben Sie den Befehl g (»go«) ein oder drücken Sie (F5), um die Ausführung des Editors fortzusetzen. Der Debugger sollte sofort wieder anhalten und dabei etwas wie das Folgende (auf x64) ausgeben: Breakpoint 0 hit ntdll!NtCreateFile: 00007ffa'9f4e5b10 4c8bd1 mov r10,rcx Möglicherweise wird der Funktionsname als ZwCreateFile angezeigt. Sowohl ZwCreateFile als auch NtCreateFile verweisen auf dasselbe Symbol im Benutzermodus. Geben Sie nun den Befehl u ein (»unassembled«), um einige Anweisungen weiter nach vorn zu schauen: 00007ffa'9f4e5b10 4c8bd1 mov r10,rcx 00007ffa'9f4e5b13 b855000000 mov eax,55h → Hauptsystemkomponenten Kapitel 2 79
00007ffa'9f4e5b18 f604250803fe7f01 test byte ptr [SharedUserData+0x308 jne ntdll!NtCreateFile+0x15 (00000000'7ffe0308)],1 00007ffa'9f4e5b20 7503 (00007ffa'9f4e5b25) 00007ffa'9f4e5b22 0f05 syscall 00007ffa'9f4e5b24 c3 ret 00007ffa'9f4e5b25 cd2e int 00007ffa'9f4e5b27 c3 ret 2Eh Das Register EAX wird auf die Nummer gesetzt, die der Systemdienst in dem vorliegenden Betriebssystem hat (hier bei Windows 10 Pro x64 auf die Hexadezimalzahl 55). Erst später folgt die Anweisung syscall. Sie veranlasst den Prozessor, in den Kernelmodus zu wechseln und zum Systemdienst-Dispatcher zu springen, wo mithilfe von EAX der Exekutivdienst NtCreateFile ausgewählt wird. Am Offset 0x308 wird außerdem das Flag 1 in SharedUserData geprüft (mehr über diese Struktur erfahren Sie in Kapitel 4). Ist es gesetzt, so folgt die Ausführung einem anderen Pfad und nutzt die Anweisung int 2Eh. Wenn Sie das VBS-Sondermerkmal CredentialGuard aktiviert haben (siehe Kapitel 7), wird das Flag auf dem Computer gesetzt, da der Hypervisor effizienter auf die Anweisung int reagieren kann als auf die Anweisung syscall. Dieses Verhalten ist für CredentialGuard von Vorteil. Weitere Einzelheiten über diesen Mechanismus (und über das Verhalten von syscall und int) finden Sie in Kapitel 8 von Band 2. Zunächst einmal aber können Sie versuchen, weitere native Dienste zu finden, z. B. NtReadFile, NtWriteFile und NtClose. Im Abschnitt »Die virtualisierungsgestützte Sicherheitsarchitektur im Überblick« haben Sie bereits gelernt, dass IUM-Anwendungen eine ähnliche Binärdatei wie Ntdll.dll nutzen können, nämlich IumDll.dll. Diese Bibliothek enthält ebenfalls Systemaufrufe, allerdings mit abweichenden Indizes. Wenn auf Ihrem System CredentialGuard aktiviert ist, können Sie das vorstehende Experiment wiederholen, wobei Sie dieses Mal aber im Menü File von WinDbg auf Open Crash Dump klicken und dann die Datei IumDll.dll auswählen. In der folgenden Ausgabe ist das hohe Bit des Systemaufrufindex gesetzt. Die Überprüfung SharedUserData erfolgt jedoch nicht. Für diese Art von Systemaufrufen, die als sichere Systemaufrufe bezeichnet werden, wird stets die Anwendung syscall verwendet. 0:000> u iumdll!IumCrypto iumdll!IumCrypto: 00000001'80001130 4c8bd1 mov r10,rcx 00000001'80001133 b802000008 mov eax,8000002h 00000001'80001138 0f05 syscall 00000001'8000113a c3 ret Ntdll.dll enthält auch viele Unterstützungsfunktionen, z. B. den Abbildlader (Funktionen, deren Namen mit Ldr beginnen), den Heap-Manager und die Kommunikationsfunktionen des Windows-Teilsystemprozesses (Namen mit Csr). Des Weiteren gibt es in Ntdll.dll allgemeine Laufzeitbibliotheksroutinen (Rtl), Unterstützung für Debugging im Benutzermodus (DbgUi), 80 Kapitel 2 Systemarchitektur
Ereignisverfolgung für Windows (Etw) sowie den Dispatcher für asynchrone Prozeduraufrufe (APC) im Benutzermodus und den Ausnahme-Dispatcher. (APCs und Ausnahmen werden kurz in Kapitel 6 und ausführlicher in Kapitel 8 von Band 2 erklärt.) Auch einen kleinen Teil der C-Laufzeitroutinen (CRT) finden Sie in Ntdll.dll. Die Auswahl ist allerdings auf die Routinen beschränkt, die zur String- oder zur Standardbibliothek gehören (wie memcpy, strcpy, sprintf usw.). Diese Routinen sind für die im Folgenden beschriebenen nativen Anwendungen nützlich. Native Abbilder Einige Abbilder (ausführbare Dateien) gehören nicht zu irgendeinem Teilsystem. Sie werden also nicht mit einem Satz von Teilsystem-DLLs verlinkt (etwa Kernel32.dll für das Windows-Teilsystem), sondern nur mit Ntdll.dll. Dies ist der kleinste gemeinsame Nenner für alle Teilsysteme. Da die von Ntdll.dll bereitgestellte native API größtenteils nicht dokumentiert ist, werden Abbilder dieser Art gewöhnlich nur von Microsoft erstellt. Ein Beispiel dafür ist der Prozess des Sitzungs-Managers (Smss.exe; weiter hinten in diesem Kapitel ausführlicher beschrieben). Smss. exe ist der erste Benutzermodusprozess, der erstellt wird (direkt vom Kernel). Er kann daher nicht vom Windows-Teilsystem abhängen, da Csrss.exe (der Prozess des Windows-Teilsystems) noch gar nicht gestartet ist. Tatsächlich ist Smss.exe sogar dafür zuständig, Csrss.exe zu starten. Ein weiteres Beispiel ist das Dienstprogramm Autochk, das manchmal beim Systemstart ausgeführt wird, um Datenträger zu prüfen. Da es relativ im Startprozess läuft (gestartet von Smss. exe), kann es ebenfalls nicht von irgendeinem Teilsystem abhängen. Der folgende Screenshot aus Dependency Walker zeigt, dass Smss.exe nur von Ntdll.dll abhängig ist. Als Teilsystemtyp ist Native angegeben. Exekutive Die Windows-Exekutive bildet die obere Schicht von Ntoskrnl.exe. (Die untere Schicht ist der Kernel.) Sie enthält Funktionen der folgenden Art: QQ Funktionen, die exportiert werden und vom Benutzermodus aus aufgerufen werden können Diese Funktionen werden als Systemdienste bezeichnet und über Ntdll.dll exportiert (z. B. NtCreateFile aus dem vorherigen Experiment). Die meisten dieser Dienste sind über die Windows-API oder die APIs anderer Umgebungsteilsysteme zugänglich. Bei einigen wenigen jedoch ist kein Zugriff über irgendeine dokumentierte Teilsystemfunk tion möglich. (Dazu gehören beispielsweise ALPC und verschiedene Abfragefunktionen wie NtQueryInformationProcess, Sonderfunktionen wie NtCreatePagingFile usw.) Hauptsystemkomponenten Kapitel 2 81
QQ Gerätetreiberfunktionen, die über die Funktion DeviceIoControl aufgerufen werden können Dies ist eine allgemeine Schnittstelle vom Benutzer- zum Kernelmodus, um Funktionen in Gerätetreibern aufzurufen, die nichts mit Lesen oder Schreiben zu tun haben. Die Treiber für Process Explorer und Process Monitor von Sysinternals und der früher erwähnte Konsolentreiber (ConDrv.sys) sind gute Beispiele dafür. QQ Funktionen, die nur vom Kernelmodus aufgerufen werden können, exportiert werden und im WDK dokumentiert sind Dazu gehören mehrere Unterstützungsroutinen, die von Gerätetreiberentwicklern gebraucht werden, z. B. der E/A-Manager (Namen, die mit Io beginnen), allgemeine Exekutivfunktionen (Ex) usw. QQ Funktionen, die exportiert werden und vom Kernelmodus aufgerufen werden können, aber nicht im WDK dokumentiert sind Dazu gehören die Funktionen, die vom Bootvideotreiber aufgerufen werden und deren Namen mit Inbv beginnen. QQ Funktionen, die als globale Symbole definiert sind, aber nicht exportiert werden Dazu gehören interne Unterstützungsfunktionen, die innerhalb von Ntoskrnl.dll aufgerufen werden, z. B. diejenigen, deren Namen mit Iop (interne Unterstützungsfunktionen für den E/A-Manager) oder Mi beginnen (interne Unterstützungsfunktionen für die Speicherverwaltung). QQ Interne Funktionen eines Moduls, die nicht als globale Symbole definiert sind Diese Funktionen werden ausschließlich von der Exekutive und dem Kernel verwendet. Die Exekutive schließt die folgenden Hauptkomponenten ein, die alle in nachfolgenden Kapiteln dieses Buches noch ausführlich behandelt werden: QQ Konfigurations-Manager Der in Kapitel 9 von Band 2 beschriebene Konfigurations-Manager ist für die Implementierung und Verwaltung der Systemregistrierung zuständig. QQ Prozess-Manager Der in Kapitel 3 und 4 erklärte Prozess-Manager erstellt und beendet Prozesse und Threads. Die zugrunde liegende Unterstützung für Prozesse und Threads ist im Windows-Kernel implementiert. Die Exekutive ergänzt diese maschinennahen Objekte um zusätzliche Semantik und Funktionen. QQ Security Reference Monitor (SRM) Der in Kapitel 7 beschriebene SRM setzt Sicherheitsrichtlinien auf dem lokalen Computer durch. Er schützt Betriebssystemressourcen, führt einen Objektschutz zur Laufzeit und eine Überwachung durch. QQ E/A-Manager Der in Kapitel 6 beschriebene E/A-Manager implementiert die geräteunabhängige Ein-/Ausgabe und ist für die Zuteilung zu den passenden Gerätetreibern für die weitere Verarbeitung verantwortlich. QQ Plug-&-Play-Manager Der in Kapitel 6 beschriebene PnP-Manager entscheidet, welche Treiber für ein bestimmtes Gerät erforderlich sind, und lädt sie. Bei der Auflistung ruft er die Hardwareressourcenanforderungen für die einzelnen Geräte ab. Auf deren Grundlage weist er die entsprechenden Ressourcen wie E/A-Ports, IRQs, DMA-Kanäle und Speicherorte zu. Des Weiteren ist er dafür zuständig, ordnungsgemäße Ereignisbenachrichtigungen über Geräteänderungen (Hinzufügen oder Entfernen eines Geräts) im System zu senden. QQ Energie-Manager Der in Kapitel 6 erklärte Energie-Manager, die Prozessorenergieverwaltung (Power Process Management, PPM) und das Energieverwaltungsframework (Pow 82 Kapitel 2 Systemarchitektur
er Management Framework, PoFx) koordinieren Energieereignisse und erstellen Energieverwaltungsbenachrichtigungen für Gerätetreiber. Die PPM kann so eingerichtet werden, dass sie die CPU im Leerlauf in den Ruhezustand versetzt und damit die Leistungsaufnahme senkt. Änderungen im Stromverbrauch einzelner Geräte werden von den Gerätetreibern gehandhabt, aber vom Energie-Manager und dem PoFx koordiniert. Bei Geräten bestimmter Klassen verwaltet der Terminal Timeout Manager auch Zeitüberschreitungen der physischen Anzeige auf der Grundlage der Gerätenutzung und -nähe. QQ WDM-WMI-Routinen (Windows Driver Model / Windows Management Instrumentation) Diese in Kapitel 9 von Band 2 behandelten Routinen ermöglichen den Gerätetreibern, Leistungs- und Konfigurationsinformationen zu veröffentlichen und Befehle von WMI-Diensten im Benutzermodus zu empfangen. Verbraucher von WMI-Informationen können sich auf dem lokalen Computer, aber auch im Netzwerk befinden. QQ Speicher-Manager Der in Kapitel 5 beschriebene Speicher-Manager implementiert virtuellen Arbeitsspeicher. Dieses Speicherverwaltungsverfahren stellt für jeden Prozess einen umfangreichen privaten Adressraum zur Verfügung, der den verfügbaren physischen Arbeitsspeicher überschreiten kann. Des Weiteren bietet der Speicher-Manager auch Unterstützung für den Cache-Manager. Er wird vom Prefetcher und vom Store-Manager unterstützt, die beide ebenfalls in Kapitel 5 erläutert werden. QQ Cache-Manager Der in Kapitel 14 von Band 2 beschriebene Cache-Manager erhöht die Leistung der Datei-E/A, indem er dafür sorgt, dass erst vor Kurzem nachgeschlagene Daten von der Festplatte für den schnellen Zugriff im Hauptarbeitsspeicher abgelegt werden. Als weitere Maßnahme zur Leistungssteigerung verzögert er Schreibvorgänge auf der Festplatte, indem er Aktualisierungen für kurze Zeit im Arbeitsspeicher festhält und sie erst dann an die Festplatte sendet. Dazu nutzt er die Unterstützung des Speicher-Managers für zugeordnete Dateien. Daneben enthält die Exekutive Unterstützungsfunktionen, die von den zuvor genannten Komponenten genutzt werden. Ungefähr ein Drittel dieser Funktionen sind im WDK dokumentiert, da sie auch von den Gerätetreibern genutzt werden. Diese Unterstützungsfunktionen fallen in die folgenden vier Hauptkategorien: QQ Objekt-Manager Der Objekt-Manager erstellt, verwaltet und löscht Windows-Exekutivobjekte und abstrakte Datentypen, die zur Darstellung von Betriebssystemressourcen wie Prozessen, Threads und den verschiedenen Synchronisierungsobjekten dienen. Erklärt wird er in Kapitel 8 von Band 2. QQ ALPC-Einrichtung (Advanced Local Procedure Call) Die in Kapitel 8 von Band 2 er klärte ALPC-Einrichtung übergibt Nachrichten zwischen einem Client- und einem Serverprozess auf demselben Computer. Unter anderem dient sie als lokaler Transport für Remoteprozeduraufrufe (RPCs), die Windows-Implementierung einer standardmäßigen Kommunikationseinrichtung für Client- und Serverprozesse über ein Netzwerk. QQ Laufzeit-Bibliotheksfunktionen Dazu gehören Stringverarbeitung, arithmetische Operationen, Datentypumwandlung und Verarbeitung von Sicherheitsstrukturen. Hauptsystemkomponenten Kapitel 2 83
QQ Unterstützungsroutinen der Exekutive Dazu gehören die Systemspeicherzuweisung (ausgelagerter und nicht ausgelagerter Pool), gesperrter Speicherzugriff sowie besondere Arten von Synchronisierungsmechanismen wie Exekutivressourcen, schnelle Mutexe und Push-Sperren. Die Exekutive enthält auch eine Reihe weiterer Infrastrukturroutinen, von denen einige in diesem Buch nur kurz vorgestellt werden: QQ Kerneldebugger-Bibliothek Sie ermöglicht ein Debugging des Kernels mit einem Debugger, der KD unterstützt, ein portierbares Protokoll, das von verschiedenen Transporten wie USB, Ethernet und IEEE 1394 unterstützt und von WinDbg und Kd.exe implementiert wird. QQ Debugging-Framework für den Benutzermodus Dieses Framework sendet Ereignisse an die Debugging-API des Benutzermodus und ermöglicht Haltepunkte, ein schrittweises Durchlaufen des Codes und Kontextwechsel laufender Threads. QQ Hypervisor- und VBS-Bibliothek Diese Bibliotheken bieten Kernelunterstützung für die sichere VM-Umgebung und optimieren einzelne Teile des Codes, wenn das System weiß, dass es in einer Clientpartition (virtuellen Umgebung) läuft. QQ Errata-Manager Der Errata-Manager stellt Notlösungen für nicht standardmäßige und nicht konforme Hardwaregeräte zur Verfügung. QQ Treiberverifizierer Der Treiberverifizierer implementiert optionale Integritätsprüfungen von Treibern und Code im Kernelmodus (siehe Kapitel 6). QQ Ereignisverfolgung für Windows (ETW) ETW stellt Hilfsroutinen für die systemweite Ereignisverfolgung von Kernel- und Benutzermoduskomponenten bereit. QQ Windows Diagnostic Infrastructure (WDI) Die WDI ermöglicht eine intelligente Verfolgung von Systemaktivitäten auf der Grundlage von Diagnoseszenarien. QQ WHEA-Unterstützungsroutinen (Windows Hardware Error Architecture) Diese Routinen bilden ein allgemeines Framework für die Meldung von Hardwarefehlern. QQ File-System Runtime Library (FSRTL) tinen für Dateisystemtreiber. Die FSRTL bietet allgemeine Unterstützungsrou- QQ Kernel Shim Engine (KSE) Die KSE bietet Shims zur Treiberkompatibilität und zusätzliche Unterstützung für Gerätefehler. Sie nutzt die in Kapitel 8 von Band 2 beschriebene Shim-Infrastruktur und -Datenbank. Der Kernel Der Kernel besteht aus einem Satz Funktionen in Ntoskrnl.exe, die grundlegende Mechanismen bereitstellen. Dazu gehören die Threadplanungs- und Synchronisierungsdienste, die von den Exekutivkomponenten genutzt werden, sowie architekturabhängige, maschinennahe Unterstützung wie Interrupt- und Ausnahme-Dispatching, das auf jeder Prozessorarchitektur anders ausfällt. Der Kernelcode ist hauptsächlich in C geschrieben. Assemblercode ist für die Aufgaben reserviert, die Zugriff auf besondere, über C nicht zugängliche Prozessoranweisungen und Register benötigen. 84 Kapitel 2 Systemarchitektur
Wie bei den Unterstützungsfunktionen der Exekutive aus dem vorherigen Abschnitt sind auch eine Reihe der Funktionen im Kernel im WDK dokumentiert (und können durch eine Suche nach Funktionen gefunden werden, deren Namen mit Ke beginnen), da sie zur Implementierung von Gerätetreibern erforderlich sind. Kernelobjekte Der Kernel bietet eine maschinennahe Grundlage wohldefinierter, vorhersehbarer Betriebssystemprimitiva und -mechanismen, die es den Exekutivkomponenten höherer Ebene ermöglichen, ihre Aufgaben zu erfüllen. Der Kernel trennt sich selbst vom Rest der Exekutive, indem er Betriebssystemmechanismen implementiert und eine Aufstellung von Richtlinien vermeidet. Er überlässt nahezu alle Richtlinienentscheidungen der Exekutive. Die einzige Ausnahme bilden die Threadplanung und das Dispatching, die der Kernel implementiert. Außerhalb des Kernels stellt die Exekutive Threads und andere gemeinsam nutzbare Ressourcen als Objekte dar. Diese Objekte benötigen etwas Richtlinien-Overhead wie Objekthandles zu ihrer Handhabung, Sicherheitsprüfungen zu ihrem Schutz und Ressourcenkontingente, die bei ihrer Erstellung abgerufen werden müssen. Dieser Mehraufwand wird im Kernel vermieden, der einen Satz einfacherer Objekte implementiert, nämlich die Kernelobjekte. Sie helfen dem Kernel, die zentrale Verarbeitung zu steuern, und unterstützen die Erstellung von Exekutivobjekten. Die meisten Exekutivobjekte kapseln ein oder mehrere Kernelobjekte und enthalten deren im Kernel definierte Attribute. Ein Satz von Kernelobjekten, die sogenannten Steuerobjekte, richten die Semantik für die Steuerung verschiedener Betriebssystemfunktionen ein. Dazu gehören das APC-Objekt (Asynchronous Procedure Call), das DPC-Objekt (Deferred Procedure Call) sowie mehrere Objekte, die der E/A-Manager verwendet, z. B. interrupt. Einen weiteren Satz von Kernelobjekten bilden die Dispatcherobjekte. Sie enthalten Synchronisierungsfähigkeiten, die die Threadplanung ändern oder beeinflussen. Zu den Dispatcherobjekten gehören der Kernelthread, Mutexe (in der Kernelterminologie Mutanten genannt), Ereignisse, Kernelereignispaare, Semaphoren, Timer und Timer mit Wartemöglichkeiten. Die Exekutive verwendet Kernelfunktionen, um Instanzen von Kernelobjekten zu erstellen, sie zu bearbeiten und die komplexeren Objekte zu konstruieren, die sie im Benutzermodus bereitstellt. Objekte werden ausführlicher in Kapitel 8 von Band 2 erklärt, Prozesse in Kapitel 3 und Threads in Kapitel 4. Die Steuerregion und der Steuerblock des Kernelprozessors Der Kernel verwendet eine als Steuerregion (Kernel Processor Control Region, KPCR) bezeichnete Datenstruktur, um prozessorspezifische Daten zu speichern. Die KPCR enthält grundlegende Informationen wie die Interruptdispatchtabelle (IDT) des Prozessors, sein Taskstatussegment (TSS) und die globale Deskriptortabelle (GDT). Außerdem schließt sie den Interruptcontroller status ein, den sie mit anderen Modulen teilt, darunter dem ACPI-Treiber und der HAL. Um leichten Zugang zur KPCR zu gewähren, speichert der Kernel einen Zeiger darauf, und zwar auf 32-Bit-Windows im Register fs und auf 64-Bit-Windows in gs. Die KPCR umfasst auch eine eingebettete Datenstruktur, die als Steuerblock des Kernelprozessors bezeichnet wird (Kernel Processor Control Block, KPRCB). Im Gegensatz zur KPCR, die für Drittanbieter-Treiber und andere interne Komponenten des Windows-Kernels dokumentiert ist, Hauptsystemkomponenten Kapitel 2 85
handelt es sich beim KPRCB um eine private Struktur, die nur vom Kernelcode in Ntoskrnl.exe verwendet wird. Sie enthält Folgendes: QQ Planungsinformationen wie den aktuellen, den nächsten sowie Leerlaufthreads, die zur Ausführung auf dem Prozessor geplant sind QQ Die Dispatcherdatenbank für den Prozessor, die wiederum die Bereitschaftswarteschlangen für jede Prioritätsebene enthält QQ Die DPC-Warteschlange QQ Angaben zur CPU und zu ihrem Hersteller wie Modell, Stepping-Geschwindigkeit und Feature-Bits QQ CPU- und NUMA-Topologie wie Knoteninformationen, Kerne pro Gehäuse, logische Prozessoren pro Kern usw. QQ Cachegrößen QQ Zeitabstimmungsinformationen wie die DPC- und die Interruptzeit Der KPRCB enthält auch alle Prozessorstatistiken, darunter die folgenden: QQ E/A-Statistiken QQ Cache-Manager-Statistiken (siehe Kapitel 14 in Band 2) QQ DPC-Statistiken QQ Speicher-Manager-Statistiken (siehe Kapitel 5) Manchmal wird der KPRCB dazu verwendet, am Cache ausgerichtete, prozessorweise Strukturen zur Optimierung des Speicherzugriffs zu speichern, insbesondere auf NUMA-Systemen, beispielsweise die Look-Aside-Listen des Systems für den nicht ausgelagerten und den ausgelagerten Pool. Experiment: KPCR und KPRCB einsehen Mit den Kerneldebuggerbefehlen !pcr und !prcb können Sie sich den Inhalt der KPCR und des KPRCB ansehen. Wenn Sie bei !prcb keine Flags angeben, zeigt der Debugger standardmäßig Informationen für CPU 0 an. Sie können jedoch auch eine bestimmte CPU auswählen, indem Sie eine Zahl an den Befehl anhängen, z. B. !prcb 2. Dagegen zeigt !pcr immer Informationen über den aktuellen Prozessor an. In einer Remotedebuggingsitzung können Sie den Prozessor wechseln. Beim lokalen Debugging können Sie die Adresse der KPCR gewinnen, indem Sie die Erweiterung !pcr gefolgt von der CPU-Nummer verwenden, und dann @$pcr durch diese Adresse ersetzen. Verwenden Sie keine der anderen Ausgaben von !pcr. Diese Erweiterung ist veraltet und zeigt keine korrekten Daten an. Das folgende Beispiel zeigt die Ausgabe von dt nt!_KPCR @$pcr und !pcrb auf Windows 10 x64: lkd> dt nt!_KPCR @$pcr +0x000 NtTib : _NT_TIB +0x000 GdtBase : 0xfffff802'a5f4bfb0 _KGDTENTRY64 +0x008 TssBase : 0xfffff802'a5f4a000 _KTSS64 → 86 Kapitel 2 Systemarchitektur
+0x010 UserRsp : 0x0000009b'1a47b2b8 +0x018 Self : 0xfffff802'a280a000 _KPCR +0x020 CurrentPrcb : 0xfffff802'a280a180 _KPRCB +0x028 LockArray : 0xfffff802'a280a7f0 _KSPIN_LOCK_QUEUE +0x030 Used_Self : 0x0000009b'1a200000 Void +0x038 IdtBase : 0xfffff802'a5f49000 _KIDTENTRY64 +0x040 Unused : [2] 0 +0x050 Irql : 0 '' +0x051 SecondLevelCacheAssociativity : 0x10 '' +0x052 ObsoleteNumber : 0 '' +0x053 Fill0 : 0 '' +0x054 Unused0 : [3] 0 +0x060 MajorVersion : 1 +0x062 MinorVersion : 1 +0x064 StallScaleFactor : 0x8a0 +0x068 Unused1 : [3] (null) +0x080 KernelReserved : [15] 0 +0x0bc SecondLevelCacheSize : 0x400000 +0x0c0 HalReserved : [16] 0x839b6800 +0x100 Unused2 : 0 +0x108 KdVersionBlock : (null) +0x110 Unused3 : (null) +0x118 PcrAlign1 : [24] 0 +0x180 Prcb : _KPRCB lkd> !prcb PRCB for Processor 0 at fffff803c3b23180: Current IRQL -- 0 Threads-- Current ffffe0020535a800 Next 0000000000000000 Idle fffff803c3b99740 Processor Index 0 Number (0, 0) GroupSetMember 1 Interrupt Count -- 0010d637 Times -- Dpc 000000f4 Interrupt 00000119 Kernel 0000d952 User 0000425d Sie können den Befehl dt auch verwenden, um die _KPRCB-Datenstrukturen direkt abzurufen, da Ihnen der Debuggerbefehl die Adresse dieser Strukturen gibt (in der vorstehenden Ausgabe fett hervorgehoben). Wenn Sie beispielsweise die beim Hochfahren ermittelte Prozessorgeschwindigkeit herausfinden wollen, können Sie sich mit dem folgenden Befehl das Feld MHz ansehen: lkd> dt nt!_KPRCB fffff803c3b23180 MHz +0x5f4 MHz : 0x893 lkd> ? 0x893 Evaluate expression: 2195 = 00000000'00000893 Hier lief der Prozessor beim Hochfahren also mit etwa 2,2 GHz. Hauptsystemkomponenten Kapitel 2 87
Hardwareunterstützung Die andere Hauptaufgabe des Kernels besteht darin, die Exekutive und die Gerätetreiber gegen Unterschiede zwischen den Hardwarearchitekturen abzuschirmen. Das schließt es auch ein, solche Abweichungen in Funktionen zu handhaben, z. B. für den Umgang mit Interrupts, das Ausnahme-Dispatching und die Synchronisierung mehrerer Prozessoren. Selbst bei solchen Funktionen, die sich auf die Hardware beziehen, wurde bei der Gestaltung des Kernels versucht, die Menge des gemeinsamen Codes zu maximieren. Der Kernel unterstützt eine Reihe von Schnittstellen, die architekturübergreifend portierbar und semantisch identisch sind. Der Großteil des Codes, der diese portierbaren Schnittstellen implementiert, ist ebenfalls in den verschiedenen Architekturen identisch. Einige dieser Schnittstellen sind auf einigen Architekturen unterschiedlich implementiert und verwenden teilweise architekturspezifischen Code. Diese architektonisch unabhängigen Schnittstellen können auf jedem Computer aufgerufen werden und sind unabhängig von den Codeunterschieden auf den Architekturen semantisch identisch. Manche Kernelschnittstellen, z. B. die in Kapitel 8 von Band 2 beschriebenen Spinlock-Routinen, sind in der HAL implementiert (siehe den nächsten Abschnitt), da ihre Implementierung sich schon zwischen Systemen derselben Architekturfamilie unterscheiden kann. Der Kernel enthält auch etwas Code mit x86-spezifischen Schnittstellen zur Unterstützung alter 16-Bit-MS-DOS-Programme (auf 32-Bit-Systemen). Diese x86-Schnittstellen sind nicht portierbar, allerdings nicht in dem Sinne, dass sie auf einem Computer mit einer anderen Architektur nicht aufgerufen werden können; tatsächlich sind sie dort nicht einmal vorhanden. Dieser x86-spezifische Code unterstützt beispielsweise Aufrufe zur Verwendung des virtuellen 8086-Modus, der zur Emulation von manchem Realmoduscode auf älteren Videokarten erforderlich ist. Ein weiteres Beispiel für architekturspezifischen Code im Kernel bilden die Schnittstellen für die Unterstützung des Übersetzungspuffers und des CPU-Cache. Aufgrund der Art und Weise, wie die Caches realisiert werden, ist dafür auf den einzelnen Architekturen jeweils unterschiedlicher Code erforderlich. Noch ein Beispiel bilden Kontextwechsel. Für Threadauswahl und Kontextwechsel wird zwar unabhängig von der Architektur der gleiche allgemeine Algorithmus verwendet (der Kontext des vorherigen Threads wird gespeichert, der Kontext des neuen Threads wird geladen und der neue Thread wird gestartet), aber bei der Implementierung in den Prozessoren gibt es architektonische Unterschiede. Da der Kontext durch den Prozessorzustand beschrieben wird (Register usw.), hängt es von der Architektur ab, was gespeichert und geladen wird. Die Hardwareabstraktionsschicht Einer der entscheidenden Aspekte des Windows-Designs ist die Portierbarkeit für verschiedene Hardwareplattformen. Angesichts von OneCore und den unzähligen Gerätebauformen ist dies heute wichtiger als je zuvor. Die Hardwareabstraktionsschicht (Hardware Abstraction Layer, HAL) ist die entscheidende Komponente, die diese Portierbarkeit ermöglicht. Es handelt sich dabei um ein ladbares Kernelmodusmodul (Hal.dll), das eine maschinennahe Schnittstelle zu der Hardwareplattform bietet, auf der Windows läuft. Sie deckt hardwareabhängige Einzel- 88 Kapitel 2 Systemarchitektur
heiten wie E/A-Schnittstellen, Interrupt-Controller und Mechanismen zur Kommunikation zwischen Prozessoren zu, also alle architekturspezifischen und maschinenabhängigen Funktionen. Die internen Windows-Komponenten und die Gerätetreiber bewahren diese Portierbarkeit, indem sie nicht direkt auf die Hardware zugreifen, sondern HAL-Routinen aufrufen, wenn sie plattformabhängige Informationen benötigen. Daher sind viele HAL-Routinen im WDK dokumentiert. Um mehr über die HAL und ihre Verwendung durch Gerätetreiber zu erfahren, schlagen Sie im WDK nach. In der Standard-Desktop-Installation von Windows sind zwei x86-HALs enthalten (siehe Tabelle 2–4). Zum Startzeitpunkt kann Windows erkennen, welche es verwenden soll. Dadurch wird das Problem gelöst, das bei früheren Versionen von Windows auftrat, wenn Sie versuchten, eine Windows-Installation auf einer anderen Art von System zu starten. HAL-Datei Unterstützte Systeme Halacpi.dll ACPI-PCs (Advanced Configuration and Power Interface). Damit sind Einprozessorcomputer ohne APIC-Unterstützung gemeint. (Sind mehrere Prozessoren oder ein APIC vorhanden, verwendet das System stattdessen die zweite HAL.) Halmacpi.dll APIC-PCs (Advanced Programmable Interrupt Controller) mit ACPI. Das Vorhandensein eines APIC bedeutet SMP-Unterstützung. Tabelle 2–4 x86-HALs Auf x64- und ARM-Computern gibt es nur ein HAL-Abbild, nämlich Hal.dll. Alle x64-Rechner benötigen sowohl ACPI- als auch APIC-Unterstützung und verfügen daher über dieselbe Motherboardkonfiguration, weshalb es nicht nötig ist, Computer ohne APIC oder mit einem Standard-PIC zu unterstützen. Auch alle ARM-Systeme verfügen über eine ACPI und verwenden Interruptcontroller, die einem Standard-APIC ähneln, weshalb eine einzige HAL für sie ausreicht. Allerdings sind diese Interruptcontroller einem Standard-APIC nur ähnlich, aber nicht identisch damit. Außerdem können sich die Timer und Speicher-/DMA-Controller auf einigen ARM-Systemen von denen auf anderen unterscheiden. Bei IoT-Geräten können sogar gängige PC-Hardwarekomponenten wie der Intel-DMA-Controller fehlen, weshalb sie selbst auf PC-gestützten Systemen Unterstützung für einen anderen Controller benötigen. Bei älteren Windows-Versionen wurden die Hersteller gezwungen, eine eigene HAL für jede mögliche Plattformkombination mitzuliefern. Das führt jedoch zu einer Unmenge von doppeltem Code und ist heute nicht mehr machbar. Stattdessen unterstützt Windows jetzt HAL-Erweiterungen. Dabei handelt es sich um zusätzliche DLLs auf der Festplatte, die der Bootloader laden kann, wenn sie von der Hardware benötigt werden. (Dies erfolgt gewöhnlich durch die ACPI und die Konfiguration in der Registrierung.) Ein Desktop-System mit Windows 10 verfügt gewöhnlich über die Erweiterungen HalExtPL080.dll und HalExtIntcLpioDMA.dll, wobei Letztere auf bestimmten Intel-Plattformen mit geringer Leistungsaufnahme verwendet wird. Die Entwicklung von HAL-Erweiterungen ist nur in Zusammenarbeit mit Microsoft möglich. Die Dateien müssen mit einem besonderen Zertifikat für HAL-Erweiterungen signiert werden, das es nur für Hardwarehersteller gibt. Außerdem ist die Auswahl der APIs, die sie nutzen können, stark eingeschränkt. Auch die Interaktion mit diesen APIs erfolgt durch einen eingeschränkten Import/Export-Tabellenmechanismus, der nicht den herkömmlichen PE- Hauptsystemkomponenten Kapitel 2 89
Abbildmechanismus verwendet. Wenn Sie das folgende Experiment mit einer HAL-Erweiterung ausführen, werden daher keine Funktionen angezeigt. Experiment: Abhängigkeiten von Ntoskrnl.exe und der HAL-Abbilder einsehen Sie können sich die Beziehung zwischen dem Kernel und den HAL-Abbildern mit Dependency Walker (Depends.exe) ansehen und ihre Export- und Importtabellen untersuchen. Öffnen Sie dazu in Dependency Walker das Menü File, klicken Sie auf Open und wählen Sie die gewünschte Abbilddatei aus. Die folgende Beispielausgabe zeigt die Abhängigen von Ntoskrnl.exe. (Ignorieren Sie zunächst die Fehlermeldungen, die dadurch zustande kommen, dass Dependency Walker nicht in der Lage ist, API-Sets zu analysieren.) Wie Sie sehen, ist Ntoskrnl.exe mit der HAL verknüpft und diese wiederum mit Ntoskrnl.exe. (Beide nutzen Funktionen der jeweils anderen Datei.) Außerdem ist Notskrnl.exe mit den folgenden Binärdateien verknüpft: QQ Pshed.dll Der PSHED (Platform-Specific Hardware Error Driver) bietet eine Abstraktion der Einrichtungen, die auf der zugrunde liegenden Plattform für die Meldung von Hardwarefehlern vorhanden sind. Dazu verbirgt er die Einzelheiten der Fehlerbehandlungsmechanismen der Plattform vor dem Betriebssystem und stellt diesem eine einheitliche Schnittstelle bereit. QQ Bootvid.dll Der Bootvideotreiber auf x86-Systemen (Bootvid) bietet Unterstützung für die VGA-Befehle, die erforderlich sind, um beim Hochfahren den Starttext und das Startlogo anzuzeigen. QQ Kdcom.dll Dies ist die Kommunikationsbibliothek des Kerneldebuggerprotokolls (KD). QQ Ci.dll Dies ist die Integritätsbibliothek (siehe Kapitel 8 in Band 2). → 90 Kapitel 2 Systemarchitektur
QQ Msrpc.sys Der Microsoft-RPC-Clienttreiber für den Kernelmodus ermöglicht dem Kernel (und anderen Treibern) die Kommunikation mit Benutzermodusdiensten über Remoteprozeduraufrufe (Remote Procedure Calls, RPCs) und das Marshalling von MES-codierten Daten. Beispielsweise nutzt der Kernel diesen Treiber für das Marshalling von Daten zum und vom Plug-&-Play-Dienst des Benutzermodus. Eine ausführliche Beschreibung der Informationen, die Dependency Walker ausgibt, finden Sie in der Hilfedatei dieses Tools (Depends.hlp). Wir haben Sie gebeten, die Fehlermeldungen zu ignorieren, die zustande kommen, da Dependency Walker keine API-Sets analysieren kann (die im Abschnitt über den Abbildlader in Kapitel 3 beschrieben werden). Das liegt daran, dass der Autor das Programm noch nicht aktualisiert hat, damit es auch diesen Mechanismus korrekt handhaben kann. Allerdings können Sie Dependency Walker immer noch nutzen, um sich anzusehen, welche Abhängigkeiten der Kernel je nach SKU noch haben kann, da die API-Sets auf echte Module verweisen. API-Sets sind keine DLLs oder Bibliotheken, sondern Kontrakte. Es kann durchaus sein, dass einige dieser Kontrakte (oder sogar alle) auf Ihrem Computer nicht vorhanden sind. Ihre Gegenwart hängt von SKU, Plattform und Hersteller ab. QQ Werkernel-Kontrakt Bietet Unterstützung für Windows Error Reporting (WER) im Kernel, z. B. beim Erstellen eines Live-Kernelauszugs. QQ Tm-Kontrakt Dies ist der Kernel-Transaktions-Manager (KTM), der in Kapitel 8 von Band 2 beschrieben wird. QQ Kcminitcfg-Kontrakt Verantwortlich für benutzerdefinierte Erstkonfiguration der Registrierung, die auf bestimmten Plattformen erforderlich sein kann. QQ Ksr-Kontrakt Kümmert sich um Kernel Soft Reboot (KSR) und die erforderliche Persistenz einzelner Speicherbereiche zu dessen Unterstützung, insbesondere auf bestimmten Mobil- und IoT-Plattformen. QQ Ksecurity-Kontrakt Enthält zusätzliche Richtlinien für AppContainer-Prozesse (also Windows-Apps) auf bestimmen Geräten und SKUs, die im Benutzermodus laufen. QQ Ksigningpolicy-Kontrakt Enthält zusätzliche Richtlinien für die Integrität von Benutzermoduscode (User-Mode Code Integrity, UMCI) zur Unterstützung von Nicht-AppContainer-Prozessen auf bestimmten SKUs oder zur ausführlicheren Konfiguration von DeviceGuard- und AppLocker-Sicherheitsmerkmalen auf bestimmten Plattformen und SKUs. QQ Ucode-Kontrakt Dies ist die Mikrocode-Aktualisierungsbibliothek für Plattformen, auf denen solche Aktualisierungen möglich sind, z. B. Intel und AMD. QQ Clfs-Kontrakt Dies ist der CLFS-Treiber (Common Log File System), der unter anderem vom Transaktionsregister (TxR) verwendet wird (siehe Kapitel 8 in Band 2). QQ Ium-Kontrakt Dies sind zusätzliche Richtlinien für IUM-Trustlets, die auf dem System laufen und von bestimmten SKUs gebraucht werden können, z. B. für abgeschirmte VMs auf einem Datacenter-Server. Trustlets werden ausführlicher in Kapitel 3 beschrieben. Hauptsystemkomponenten Kapitel 2 91
Gerätetreiber Gerätetreiber werden noch ausführlich in Kapitel 6 beschrieben. Dieses Kapitel gibt einen kurzen Überblick über die möglichen Arten von Treibern und zeigt, wie Sie die auf Ihrem System installierten und geladenen Treiber auflisten lassen können. Windows unterstützt sowohl Kernel- als auch Benutzermodustreiber, doch in diesem Abschnitt beschäftigen wir uns nur mit den Kerneltreibern. Der Begriff Gerätetreiber scheint auf Hardwaregeräte hinzudeuten, doch gibt es auch Gerätetreiber, die nicht direkt mit der Hardware zu tun haben (siehe die nachfolgende Liste). In diesem Abschnitt geht es hauptsächlich um Gerätetreiber zur Steuerung von Hardwaregeräten. Gerätetreiber sind ladbare Kernelmodusmodule (Dateien, die gewöhnlich die Endung .sys tragen), die als Schnittstelle zwischen dem E/A-Manager und der entsprechenden Hardware dienen. Sie werden im Kernelmodus in einem der drei folgenden Kontexte ausgeführt: QQ Im Kontext des Benutzerthreads, der eine E/A-Funktion (beispielsweise eine Leseoperation) ausgelöst hat QQ Im Kontext eines Kernelmodus-Systemthreads (z. B. einer Anforderung vom Plug-&-PlayManager) QQ Infolge eines Interrupts und daher nicht im Kontext eines bestimmten Threads, sondern des Threads, der beim Auftreten des Interrupts gerade ausgeführt wurde Wie bereits ausgeführt, handhaben Gerätetreiber in Windows die Hardware nicht direkt, sondern rufen Funktionen der HAL auf, um mit der Hardware zu arbeiten. Treiber sind gewöhnlich in C oder C++ geschrieben. Dank der Nutzung der HAL ist ihr Quellcode also über alle von Windows unterstützten CPU-Architekturen hinweg portierbar und ihre Binärdateien innerhalb der Architekturfamilie. Es gibt verschiedene Arten von Gerätetreibern: QQ Hardwaregerätetreiber Sie verwenden die HAL, um die Hardware zu beeinflussen und damit Ausgaben auf ein physisches Gerät oder das Netzwerk zu schreiben oder von dort abzurufen. Es gibt viele verschiedene Arten von Hardwaregerätetreibern, darunter Bustreiber, Benutzerschnittstellentreiber, Massenspeichertreiber usw. QQ Dateisystemtreiber Diese Windows-Treiber nehmen E/A-Anforderungen für Dateien entgegen und übersetzen sie in E/A-Anforderungen für ein bestimmtes Gerät. QQ Dateisystemfiltertreiber Dazu gehören die Treiber für Festplattenspiegelung und -verschlüsselung, die Untersuchung auf lokale Viren, das Abfangen von E/A-Anforderungen und eine Verarbeitung von E/A vor der Übergabe an die nächste Schicht (bzw. in manchen Fällen vor der Ablehnung einer solchen Operation). QQ Netzwerkredirectors und -server Dies sind Dateisystemtreiber, die Dateisystem-E/AAnforderungen zu einem anderen Computer im Netzwerk übertragen bzw. solche Anforderungen von dort empfangen. QQ Protokolltreiber Sie implementieren Netzwerkprotokolle wie TCP/IP, NetBEUI oder IPX/SPX. QQ Kernelstreaming-Filtertreiber Diese Treiber sind verkettet, um eine Signalverarbeitung an Datenströme durchzuführen, z. B. Audio- und Videoaufnahme und -wiedergabe. 92 Kapitel 2 Systemarchitektur
QQ Softwaretreiber Diese Kernelmodule führen im Namen von Benutzermodusprozessen Operationen durch, die nur im Kernelmodus erledigt werden können. Viele Tools von Sys internals wie Process Explorer und Process Monitor verwenden Treiber, um Informationen abzurufen oder Operationen auszuführen, die mit Benutzermodus-APIs nicht zugänglich bzw. nicht möglich sind. Das Treibermodell von Windows Das ursprüngliche Treibermodell wurde in der ersten NT-Version (3.1) erstellt und bot noch keine Unterstützung für Plug & Play, da es dieses Funktionsprinzip damals noch gar nicht gab. Das blieb so bis Windows 2000 (bzw. Windows 95/98 auf der Seite der Windows-Versionen für Endverbraucher). In Windows 2000 kamen Unterstützung für Plug & Play, Energieoptionen und eine Erweiterung des Treibermodells von Windows NT hinzu, die als Windows Driver Model (WDM) bezeichnet wurde. Windows 2000 und höher kann ältere Windows NT 4-Treiber ausführen, aber da diese Treiber keine Unterstützung für Plug & Play und Energieoptionen bieten, sind die Möglichkeiten von Systemen, die diese Treiber nutzen, auf diesen beiden Gebieten eingeschränkt. Ursprünglich bot WDM ein gemeinsames Treibermodell, das zwischen Windows 2000/XP und Windows 98/ME (fast) quellcodekompatibel war. Dadurch wurde es einfacher, Treiber für Hardwaregeräte zu schreiben, da nur ein einziger Codestamm benötigt wurde und nicht zwei. Auf Windows 98/ME wurde WDM simuliert. Als diese Betriebssysteme außer Gebrauch kamen, blieb WDM das Grundmodell zum Schreiben von Hardwaregerätetreibern für Windows 2000 und höhere Versionen. WDM unterscheidet zwischen den drei folgenden Arten von Treibern: QQ Bustreiber Bustreiber sind für Buscontroller, Adapter, Brücken und jegliche anderen Geräte da, die über Kindgeräte verfügen. Diese Treiber sind erforderlich und werden im Allgemeinen von Microsoft bereitgestellt. Für jede Art von Bus auf einem System (wie PCI, PCMCIA und USB) gibt es einen eigenen Bustreiber. Drittanbieter können Bustreiber zur Unterstützung neuer Busse schreiben, z. B. für VMEbus, Multibus und Futurebus. QQ Funktionstreiber Ein Funktionstreiber ist der Hauptgerätetreiber, der die Betriebsschnittstelle für das zugehörige Gerät bereitstellt. Dieser Treiber ist erforderlich, sofern das Gerät nicht »roh« verwendet wird, wobei die E/A über den Bustreiber und Busfiltertreiber verfügt, z. B. einen SCSI-Pass-Thru-Treiber. Der Funktionstreiber ist per definitionem der Treiber, der am meisten über das zugehörige Gerät weiß, und gewöhnlich der einzige Treiber, der auf gerätespezifische Register zugreift. QQ Filtertreiber Filtertreiber dienen dazu, den Funktionsumfang eines Geräts oder eines vorhandenen Treibers zu erweitern oder um E/A-Anforderungen oder -Antworten von anderen Treibern zu ändern. Sie werden gewöhnlich als Korrekturmaßnahme verwendet, wenn Hardware fehlerhafte Informationen über ihre Ressourcenanforderungen bereitstellt. Filtertreiber sind optional. Sie können in beliebiger Anzahl auftreten, oberhalb oder unterhalb eines Funktionstreibers und oberhalb eines Bustreibers platziert werden. Gewöhnlich werden sie von den System-OEMs oder von unabhängigen Hardwareherstellern bereitgestellt. Hauptsystemkomponenten Kapitel 2 93
In der WDM-Treiberumgebung steuert kein einziger Treiber sämtliche Aspekte eines Geräts. Der Bustreiber meldet die Geräte, die sich an seinem Bus befinden, an den PnP-Manager, während der Funktionstreiber das Gerät handhabt. In den meisten Fällen ändern die Filtertreiber niedrigerer Ebene das Verhalten der Gerätehardware. Wenn beispielsweise ein Gerät an seinen Bustreiber meldet, dass es vier E/A-Ports benötigt, obwohl es in Wirklichkeit 16 sein müssten, kann ein gerätespezifischer Filtertreiber auf niedrigerer Ebene die vom Bustreiber an den PnP-Manager gemeldete Liste der Hardwareressourcen abfangen und die Anzahl der E/A-Ports korrigieren. Filtertreiber höherer Ebene dagegen dienen gewöhnlich dazu, den Funktionsumfang eines Geräts zu erweitern. Beispielsweise kann ein solcher Treiber für eine Festplatte zusätzliche Sicherheitsprüfungen erzwingen. Die Interruptverarbeitung wird allgemein in Kapitel 8 von Band 2 und im Zusammenhang mit Gerätetreibern in Kapitel 6 beschrieben. Auch der E/A-Manager, WDM, Plug & Play und die Energieverwaltung sind Thema von Kapitel 6. Windows Driver Foundation Die Windows Driver Foundation (WDF) vereinfacht die Entwicklung von Windows-Treibern, indem sie zwei Frameworks bereitstellt: das Kernel-Mode Driver Framework (KMDF) und das User-Mode Driver Framework (UMDF). Mit dem KMDF können Entwickler Treiber für Windows 2000 SP4 und höher schreiben, während UMDF Windows XP und höher unterstützt. KMDF stellt eine einfache Schnittstelle zum WDM bereit und verbirgt die Komplexität dieses Modells vor den Treiberautoren, ohne das zugrunde liegende Modell der Bus-, Funktionsund Filtertreiber zu ändern. KMDF-Treiber reagieren auf Ereignisse, die sie registrieren, und rufen Funktionen der KMDF-Bibliothek auf, um Arbeiten auszuführen, die nicht auf die von ihnen verwendete verwaltete Hardware zugeschnitten sind, z. B. allgemeine Energieverwaltung oder Synchronisierung. (Früher musste jeder Treiber all dies selbst implementieren.) Dabei kann ein einziger KDMF-Funktionsaufruf über 200 Zeilen WDM-Code einsparen. Mit UMDF ist es möglich, Treiber bestimmter Klassen – vor allem Treiber für Busse auf der Grundlage von USB oder anderen Protokollen mit hoher Latenz, z. B. für Videokameras, MP3-Player, Handys und Drucker – als Benutzermodustreiber zu implementieren. UMDF führt jeden Benutzermodustreiber letzten Endes in einem Benutzermodusdienst aus und verwendet ALPC zur Kommunikation mit einem Kernelmodus-Wrappertreiber, der den eigentlichen Zugang zur Hardware gewährt. Wenn ein UMDF-Treiber abstürzt, wird der Prozess beendet und gewöhnlich neu gestartet. Das System bleibt dabei stabil. Lediglich das Gerät ist vorübergehend nicht erreichbar, während der Dienst für den Treiber neu gestartet wird. Es gibt zwei Hauptversionen von UMDF: Version 1.x ist für alle Betriebssystemversionen verfügbar, die UMDF unterstützen. Die neueste Version ist 1.11 und steht in Windows 10 zur Verfügung. Für Version 1.x wurden C++ und COM zum Schreiben der Treiber verwendet, was für Benutzermodusprogrammierer sehr bequem ist, allerdings dazu führt, dass sich das UMDF-Modell von KMDF unterscheidet. Version 2.0 von UMDF wurde in Windows 8.1 eingeführt und basiert auf demselben Objektmodell wie KMDF, was die Programmiermodelle der beiden Frameworks sehr ähnlich macht. Der Quellcode des WDF wurde von Microsoft schließ- 94 Kapitel 2 Systemarchitektur
lich offengelegt und steht zurzeit auf GitHub unter https://github.com/Microsoft/Windows- Driver-Frameworks zur Verfügung. Universelle Windows-Treiber Mit dem in Windows 10 eingeführten Prinzip der universellen Windows-Treiber ist es möglich, Gerätetreiber zu schreiben, die die vom gemeinsamen Kern von Windows 10 bereitgestellten APIs und Gerätetreiberschnittstellen (Device Driver Interface, DDI) gemeinsam nutzen. Solche Treiber sind innerhalb einer CPU-Architektur (x86, x64 oder ARM) binärkompatibel und können auf Geräten mit unterschiedlicher Bauform verwendet werden, von IoT-Geräten über Smartphones, die HoloLens und die Xbox One bis zu Laptop- und Desktopcomputern. Als Treibermodell können universelle Treiber KMDF, UMDF 2.x und WDM nutzen. Experiment: Die installierten Gerätetreiber einsehen Um die installierten Gerätetreiber aufzulisten, führen Sie das Tool Systeminformationen (Msinfo32.exe) aus. Um es zu starten, klicken Sie auf Start und geben Msinfo32 ein. Erweitern Sie unter Systemübersicht den Knoten Softwareumgebung und darunter Systemtreiber. Die folgende Abbildung zeigt eine Beispielausgabe mit einer Liste der installierten Treiber. Dieses Fenster zeigt eine Liste der in der Registrierung definierten Gerätetreiber, ihres Typs und ihres Zustands (Wird ausgeführt oder Beendet) an. Gerätetreiber und Windows-Dienstprozesse werden beide in HKLM\SYSTEM\CurrentControlSet\Services definiert, aber durch den Typcode unterschieden. Beispielsweise handelt es sich bei Typ 1 um Kernelmodus-Gerätetreiber. Eine vollständige Liste der Informationen, die in der Registrierung über Gerätetreiber gespeichert sind, erhalten Sie in Kapitel 9 von Band 2. → Hauptsystemkomponenten Kapitel 2 95
Alternativ können Sie die zurzeit geladenen Gerätetreiber auch anzeigen lassen, indem Sie in Process Explorer den Prozess System auswählen und die DLL-Ansicht öffnen. Die folgende Abbildung zeigt eine Beispielausgabe. (Um zusätzliche Spalten anzuzeigen, rechtsklicken Sie auf einen Spaltenkopf und klicken auf Select Columns. Dadurch werden alle verfügbaren Spalten für die Module auf der DLL-Registerkarte angezeigt.) Ein Blick in nicht dokumentierte Schnittstellen Es kann sehr erhellend sein, einen genaueren Blick auf die Namen der exportierten oder globalen Symbole in den wichtigsten Systemabbildern (wie Ntoskrnl.exe, Hal.dll oder Ntdll. dll) zu werfen, denn dadurch erhalten Sie eine Vorstellung der Dinge, die Windows über das, was dokumentiert ist und zurzeit unterstützt wird, hinaus tun kann. Natürlich heißt das nicht, dass Sie diese Funktionen aufrufen können oder sollten, nur weil Sie ihre Namen kennen. Da die Schnittstellen nicht dokumentiert sind, können sie Änderungen unterworfen sein. Sehen Sie sich diese Funktionen nur an, um zu erkennen, welche Arten von internen Funktionen Windows ausführt, nicht um die unterstützten Schnittstellen zu umgehen. Wenn Sie sich beispielsweise die Funktionen in Ntdll.dll ansehen, finden Sie alle Systemdienste, die Windows für Teilsystem-DLLs im Benutzermodus bereitstellt – also nicht nur die Teilmenge, die das Teilsystem verfügbar macht. Viele dieser Funktionen sind zwar eindeutig dokumentierten und unterstützten Windows-Funktionen zugeordnet, aber einige von ihnen stehen nicht über die Windows-API zur Verfügung. Es ist allerdings auch interessant, sich die Importe der Windows-Teilsystem-DLLs (wie Kernel32.dll oder Advapi32.dll) und die Funktionen anzusehen, die sie in Ntdll.dll aufrufen. Auch eine Untersuchung von Ntoskrnl.exe ist interessant: Viele der exportierten Routinen, die von Kernelmodus-Gerätetreibern genutzt werden, sind zwar im WDK dokumentiert, doch eine ganze Menge von ihnen nicht. Schauen Sie sich auch die Importtabelle für Ntoskrnl.exe und die HAL an. Sie zeigt die Funktionen in der HAL, die Ntoskrnl.exe nutzt, und umgekehrt. → 96 Kapitel 2 Systemarchitektur
Tabelle 2–5 führt die meisten gängigen Funktionsnamenpräfixe für Komponenten der Exekutive auf. Jede dieser Komponenten verwendet auch eine Variante des Präfixes, um interne Funktionen zu kennzeichnen. Dabei wird an den ersten Buchstaben des Präfix es entweder ein i (für intern) oder an das komplette Präfix ein p (für privat) angehängt. Beispielsweise steht Ki für interne Kernelfunktionen und Psp für interne Prozessunterstützungsfunktionen. Präfix Komponente Alpc Advanced Local Procedure Calls Cc Gemeinsamer Cache (Common Cache) Cm Konfigurations-Manager Dbg Kernel-Debugunterstützung Dbgk Debugging-Framework für den Benutzermodus Em Errata-Manager Etw Ereignisverfolgung für Windows Ex Unterstützungsroutinen der Exekutive FsRtl Dateisystem-Laufzeitbibliothek Hv Hive-Bibliothek Hvl Hypervisor-Bibliothek Io E/A-Manager Kd Kerneldebugger Ke Kernel Kse Kernel Shim Engine Lsa Lokale Sicherheitsautorität Mm Speicher-Manager Nt NT-Systemdienste (vom Benutzermodus aus über Systemaufrufe zugänglich) Ob Objekt-Manager Pf Prefetcher Po Energie-Manager PoFx Energie-Framework Pp PnP-Manager Ppm Prozessor-Energie-Manager Ps Prozessunterstützung Rtl Laufzeitbibliothek Se Security Reference Monitor Sm Store-Manager Tm Transaktions-Manager Ttm Terminal Timeout Manager → Hauptsystemkomponenten Kapitel 2 97
Präfix Komponente Vf Treiberverifizierer Vsl Bibliothek für Virtual Secure Mode Wdi Windows Diagnostic Infrastructure Wfp Windows FingerPrint Whea Windows Hardware Error Architecture Wmi Windows Management Instrumentation Zw Spiegeleintrittspunkt für Systemdienste (die mit Nt beginnen). Er setzt den vorherigen Zugriffsmodus auf den Kernelmodus. Dadurch fällt die Parametervalidierung weg, da Nt-Systemdienste Parameter nur dann überprüfen, wenn der vorherige Zugriffsmodus der Benutzermodus war. Tabelle 2–5 Gängige Präfixe Die Namen der exportierten Funktionen können Sie einfacher deuten, wenn Sie die Namenskonvention für Windows-Systemroutinen kennen. Das allgemeine Format lautet: <Präfix><Operation><Objekt> Präfix gibt die interne Komponente an, die die Routine exportiert, Operation sagt, was mit dem Objekt oder der Ressource geschieht, und Objekt nennt das Objekt, an dem die Operation ausgeführt wird. Beispielsweise ist ExAllocatePoolWithTag die Unterstützungsroutine der Exekutive für die Zuweisung vom ausgelagerten oder nicht ausgelagerten Pool und KeInitializeThread die Routine, die ein Kernelthreadobjekt zuweist und einrichtet. Systemprozesse Die folgenden Systemprozesse erscheinen auf jedem Windows 10-System. Einer davon (der Leerlaufprozess) ist in Wirklichkeit gar kein Prozess, und drei von ihnen – System, Secure System und Speicherkomprimierung – sind keine vollständigen Prozessen, da sie nicht in einer ausführbaren Benutzermodusdatei laufen. Prozesse dieser Art werden minimale Prozesse genannt und werden in Kapitel 3 erklärt. QQ Leerlaufprozess tigen. Enthält einen Thread pro CPU, um CPU-Leerlaufzeiten zu berücksich- QQ Systemprozess Enthält den Großteil der Kernelmodus-Systemthreads und -handles. QQ Secure-System-Prozess ser ausgeführt wird. Enthält den Adressraum des sicheren Kernels in VTL 1, falls die- QQ Speicherkomprimierungsprozess Enthält den komprimierten Arbeitsteil der Benutzermodusprozesse (siehe Kapitel 5). QQ Sitzungs-Manager Smss.exe QQ Windows-Teilsystem Csrss.exe 98 Kapitel 2 Systemarchitektur
QQ Sitzung-0-Initialisierung Wininit.exe QQ Anmeldeprozess Winlogon.exe QQ Dienststeuerungs-Manager Services.exe und die von ihm erzeugten Kindprozesse wie der vom System bereitgestellte, allgemeine Diensthostprozess (Svchhost.exe). QQ Lokaler Sicherheitsauthentifizierungsdienst Lsass.exe; falls CredentialGuard aktiviert ist, auch Lsaiso.exe (Isolated Local Security Authentication Server). In der Prozessstruktur können Sie die Beziehungen zwischen den Prozessen erkennen und erfahren, von welchen Prozessen die einzelnen Prozesse erstellt wurden. Abb. 2–6 zeigt die Prozessstruktur in einer Startablaufverfolgung von Process Monitor. Um eine solche Ablaufverfolgung durchzuführen, öffnen Sie in Process Monitor das Menü Options und wählen Enable Boot Logging. Starten Sie das System dann neu, öffnen Sie Process Monitor wieder und wählen Sie im Menü Tools den Punkt Process Tree oder drücken Sie (Strg) + (T). In Process Monitor können Sie auch die inzwischen beendeten Prozesse erkennen – sie werden durch ein verblasstes Symbol dargestellt. Abbildung 2–6 Die anfängliche Prozessstruktur In den nächsten Abschnitten werden die wichtigsten Systemprozesse erklärt, die Sie in Abb. 2–6 sehen. Dabei wird auch die Reihenfolge erläutert, in der die Prozesse gestartet werden. Eine ausführliche Beschreibung der Schritte, die beim Hochfahren von Windows erfolgen, finden Sie in Kapitel 11 von Band 2. Hauptsystemkomponenten Kapitel 2 99
Der Leerlaufprozess Der erste in Abb. 2–6 aufgeführte Prozess ist der Leerlaufprozess (Idle). Wie in Kapitel 3 erklärt, werden Prozesse anhand ihres Abbildnamens bezeichnet. Allerdings führt dieser Prozess – ebenso wie der Systemprozess, der Secure-System-Prozess und der Speicherkomprimierungsprozess – kein echtes Benutzermodusabbild aus, d. h., im Verzeichnis \Windows gibt es keine Datei namens System Idle Process.exe. Aufgrund der Art der Implementierung wird dieser Prozess (ID 0) auch je nach verwendetem Programm unter einem anderen Namen angezeigt (siehe Tabelle 2–6). Der Leerlaufprozess dient dazu, Leerlaufzeiten zu berücksichtigen. Daher ist die Anzahl der »Threads« in diesem »Prozess« die Anzahl der logischen Prozessoren des Systems. In Kapitel 3 finden Sie eine ausführlichere Erklärung dieses Prozesses. Programme Name für den Prozess mit der ID 0 Task-Manager Systemleerlaufprozess (System Idle process) Process Status (Pstat.exe) Idle process Process Explorer (Procexp.exe) System Idle process Task List (Tasklist.exe) System Idle process Tlist (Tlist.exe) System process Tabelle 2–6 Namen des Prozesses mit der ID 0 in verschiedenen Programmen Als Nächstes sehen wir uns die Systemthreads und den Zweck der einzelnen Systemprozesse an, die echte Abbilder ausführen. Der Systemprozess und die Systemthreads Der Systemprozess (Prozess-ID 4) ist das Zuhause einer besonderen Art von Threads, die nur im Kernelmodus laufen – der Kernelmodus-Systemthreads. Sie haben alle Attribute und Kontexte regulärer Benutzermodusthreads, z. B. Hardwarekontext, Priorität usw., laufen aber ausschließlich im Kernelausführungscode, der in den Systemraum geladen ist, unabhängig davon, ob dies in Ntoskrnl.ee oder einem anderen geladenen Gerätetreiber ist. Außerdem haben Systemthreads keinen Benutzerprozess-Adressraum und müssen zur Zuweisung von dynamischem Speicher daher auf Speicherheaps des Betriebssystems zurückgreifen, z. B. auf den ausgelagerten oder den nicht ausgelagerten Pool. Im Task-Manager von Windows 10 Version 1511 wird der Systemprozess als Systemspeicher und komprimierter Speicher bezeichnet. Der Grund dafür ist ein neues Merkmal von Windows 10, das den Arbeitsspeicher komprimiert, um dort mehr Prozessinformationen unterzubringen, anstatt sie auf die Festplatte auszulagern. Dieser Mechanismus wird ausführlicher in Kapitel 5 beschrieben. Merken Sie sich nur, dass sich der Begriff Systemprozess auf diesen Prozess bezieht, wie auch immer er in irgendeinem Programm angezeigt werden mag. In Windows 10 Version 1607 und Windows Server 2016 heißt der Prozess übrigens wieder einfach System, da für die Speicherkomprimierung hier der Prozess Speicherkomprimierung verwendet wird. Auch dieser Prozess wird in Kapitel 5 ausführlicher beschrieben. 100 Kapitel 2 Systemarchitektur
Systemthreads werden von der Funktion PsCreateSystemThread oder IoCreateSystemThreads erstellt, die beide im WDK dokumentiert sind. Diese Threads können nur vom Kernelmodus aus aufgerufen werden. Windows und verschiedene Gerätetreiber erstellen Systemthreads bei der Systeminitialisierung, um Operationen auszuführen, die einen Threadkontext benötigen, z. B. um Ein-/Ausgaben oder andere Objekte auszugeben und darauf zu warten oder Informationen über ein Gerät abzufragen. So nutzt beispielsweise der Speicher-Manager Systemthreads zur Implementierung von Funktionen, die modifizierte Seiten in die Auslagerungsdatei oder in zugeordnete Dateien schreiben, um Prozesse aus dem Arbeitsspeicher auszulagern und wieder dorthin zu übernehmen usw. Der Kernel erstellt den Systemthread Balance-Set-Manager, der einmal pro Sekunde erwacht, um ggf. verschiedene Planungs- und Speicherverwaltungsereignisse auszulösen. Auch der Cache-Manager verwendet Systemthreads, nämlich für vorausschauendes Lesen und verzögertes Schreiben. Der Dateiserver-Gerätetreiber (Srv2.sys) nutzt Systemthreads, um auf Netzwerk-E/A-Anforderungen für Dateidaten auf Netzwerkfreigaben zu reagieren. Selbst der Plattentreiber hat einen Systemthread, um das Laufwerk abzufragen. (Die Abfrage ist in diesem Fall effizienter, da ein interruptgesteuerter Plattentreiber große Mengen an Systemressourcen verbraucht.) Weitere Informationen über einzelne Systemthreads finden Sie jeweils in den Kapiteln, in denen die zugehörigen Komponenten beschrieben werden. Standardmäßig gehören Systemthreads dem Systemprozess, doch können Gerätetreiber Systemthreads auch in beliebigen Prozessen erstellen. Beispielsweise legt der Gerätetreiber für das Windows-Teilsystem (Win32k.sys) einen Systemthread im kanonischen Displaytreiber (Cdd. dll) des Windows-Teilsystemprozesses (Csrss.exe) an, sodass Daten im Benutzermodus-Adressraum leicht auf diesen Prozess zugreifen können. Bei einer Störungssuche oder bei einer Systemanalyse ist es praktisch, die einzelnen Systemthreads zu den Treibern oder sogar den Subroutinen zurückverfolgen zu können, die den Code enthalten. Beispielsweise verbraucht der Systemprozess auf einem schwer belasteten Dateiserver wahrscheinlich erheblich viel CPU-Zeit. Zu wissen, dass »irgendein Systemthread« läuft, wenn der Systemprozess ausgeführt wird, reicht nicht aus, um zu erkennen, welcher Gerätetreiber oder welche Betriebssystemkomponente läuft. Daher müssen Sie als Erstes herausfinden, welche Systemthreads ausgeführt werden (z. B. mit der Leistungsüberwachung oder mit Process Explorer). Sobald Sie sie gefunden haben, können Sie nachschauen, in welchem Treiber ihre Ausführung begonnen hat. Das verrät Ihnen zumindest, welcher Treiber den Thread wahrscheinlich erstellt hat. Beispielsweise können Sie in Process Explorer auf den Systemprozess rechtsklicken, Properties auswählen und dann auf der Registerkarte Threads auf den Spaltenkopf CPU klicken, damit der aktivste Thread ganz oben angezeigt wird. Markieren Sie diesen Thread und klicken Sie auf die Schaltfläche Module, um sich die Datei mit dem Code anzeigen zu lassen, der auf der Spitze des Stacks ausgeführt wird. Da der Systemprozess in den neuen Versionen von Windows geschützt ist, kann Process Explorer den Aufrufstack nicht anzeigen. Der Secure-System-Prozess Der Secure-System-Prozess (variable Prozess-ID) ist technisch gesehen das Zuhause des Adress raums, der Handles und Systemthreads des sicheren VLT-1-Kernels. Da Planung, Objekt- und Speicherverwaltung jedoch im Besitz des VTL-0-Kernels sind, werden solche Entitäten in Wirk- Hauptsystemkomponenten Kapitel 2 101
lichkeit gar nicht diesem Prozess zugeordnet. Sein einziger tatsächlicher Zweck besteht darin, in Werkzeugen wie dem Task-Manager oder Process Explorer einen sichtbaren Indikator dafür bereitzustellen, dass VBS zurzeit aktiv ist (und mindestens eines der Merkmale bereitstellt, die ihn nutzen). Der Speicherkomprimierungsprozess Der Speicherkomprimierungsprozess verwendet seinen Benutzermodus-Adressraum, um die komprimierten Speicherseiten zu speichern, die dem aus den Arbeitssätzen bestimmter Prozesse getilgtem Standby-Speicher entsprechen (siehe Kapitel 5). Anders als der Secure-System-Prozess beherbergt der Speicherkomprimierungsprozess tatsächlich eine Reihe von Systemthreads, die gewöhnlich als SmKmStoreHelperWorker und SmStReadThread angezeigt werden. Beide gehören zum Store-Manager, der sich um die Speicherkomprimierung kümmert. Im Gegensatz zu den anderen Systemprozessen in der Liste befindet sich der Arbeitsspeicher dieses Prozesses tatsächlich im Benutzermodus-Adressraum. Daher unterliegt er der Beschneidung des Arbeitssatzes und wird in Systemüberwachungswerkzeugen möglicherweise mit einer umfangreichen Speichernutzung angezeigt. Die Registerkarte Leistung des Task-Managers zeigt jetzt sowohl den verwendeten als auch den komprimierten Arbeitsspeicher. Dort können Sie nun erkennen, dass der Arbeitssatz des Speicherkomprimierungsprozesses genauso groß ist wie der komprimierte Arbeitsspeicher. Der Sitzungs-Manager Der Sitzungs-Manager (%SystemRoot%\System32\Smss.exe) ist der erste Benutzermodusprozess, der im System erstellt wird, und zwar von dem Kernelmodus-Systemthread, der die letzte Phase der Initialisierung der Exekutive und des Kernels durchführt. Wie in Kapitel 3 beschrieben, wird er als PPL-Prozess angelegt (Protected Process Light). Bei seinem Start prüft Smss.exe, ob er die erste Instanz (der Master) oder eine Instanz ist, die der Master erzeugt hat, um eine Sitzung zu erstellen. Sind Befehlszeilenargumente vorhanden, so ist Letzteres der Fall. Wenn Smss.exe beim Hochfahren oder beim Erstellen einer Terminaldienste-Sitzung mehrere Instanzen von sich selbst anlegt, kann er dadurch mehrere Sitzungen gleichzeitig erzeugen – bis zu vier gleichzeitige plus eine zusätzliche für jede CPU, die es über die erste hinaus noch gibt. Das verbessert die Anmeldeleistung auf Terminal-Servern, wenn mehrere Benutzer gleichzeitig Verbindung aufnehmen. Wenn eine Sitzung die Initialisierung abgeschlossen hat, wird die Kopie von Smss.exe beendet, sodass nur der ursprüngliche Smss.exe-Prozess aktiv bleibt. (Eine Beschreibung der Terminaldienste finden Sie im Abschnitt »Terminaldienste und mehrere Sitzungen« von Kapitel 1.) Der Smss.exe-Masterprozess führt folgende einmalige Initialisierungsschritte aus: 1. Er kennzeichnet den Prozess und den ursprünglichen Thread als kritisch. Wird ein solcher Prozess oder Thread aus irgendeinem Grunde beendet, stürzt Windows ab. Mehr darüber erfahren Sie in Kapitel 3. 2. Er sorgt dafür, dass der Prozess bestimmte Fehler als kritisch behandelt, z. B. die Verwendung ungültiger Handles oder eine Beschädigung des Heaps, und aktiviert den Prozess ausgleich Disable Dynamic Code Execution. 102 Kapitel 2 Systemarchitektur
3. Er erhöht die Basispriorität des Prozesses auf 11. 4. Wenn das System ein Hinzufügen von Prozessoren im laufenden Betrieb erlaubt, aktiviert er die automatische Aktualisierung der Prozessoraffinität. Wird dann ein neuer Prozessor hinzugefügt, kann er von neuen Sitzungen genutzt werden. Mehr über das dynamische Hinzufügen von Prozessoren erfahren Sie in Kapitel 4. 5. Er initialisiert einen Threadpool zur Handhabung von ALPC-Befehlen und anderen Arbeitselementen. 6. Er erstellt den ALPC-Port \SmApiPort, um Befehle zu empfangen. 7. Er initialisiert eine lokale Kopie der NUMA-Topologie des Systems. 8. Er erstellt den Mutex PendingRenameMutex, um Operationen zum Umbenennen von Dateien zu synchronisieren. 9. Er erstellt den ursprünglichen Prozessumgebungsblock und aktualisiert bei Bedarf die Variable Safe Mode. 10. Er erstellt Sicherheitsdeskriptoren für verschiedene Systemressourcen auf der Grundlage des Wertes von ProtectionMode in HKLM\SYSTEM\CurrentControlSet\Control\Session Manager. 11. Er erstellt die beschriebenen Objekt-Manager-Verzeichnisse auf der Grundlage des Wertes von ObjectDirectories in HKLM\SYSTEM\CurrentControlSet\Control\Session Manager, z. B. \RPC Control und \Windows. Des Weiteren speichert er die in den Werten BootExecute, BootExecuteNoPnpSync und SetupExecute aufgeführten Programme. 12. Er speichert den Programmpfad, der im Wert S0InitialCommand unter HKLM\SYSTEM\ CurrentControlSet\Control\Session Manager angegeben ist. 13. Er liest den Wert NumberOfInitialSessions von HKLM\SYSTEM\CurrentControlSet\Control\ Session Manager, ignoriert ihn aber, wenn sich das System im Fertigungsmodus befindet. 14. Er liest die Dateiumbenennungsoperationen, die in den Werten PendingFileRenameOperations und PendingFileRenameOperations2 von HKLM\SYSTEM\CurrentControlSet\Control\ Session Manager aufgeführt sind. 15. Er liest die Werte von AllowProtectedRenames, ClearTempFiles, TempFileDirectory und D isableWpbtExecution in HKLM\SYSTEM\CurrentControlSet\Control\Session Manager. 16. Er liest die Liste der DLLs im Wert ExcludeFromKnownDllList von HKLM\SYSTEM\Current ControlSet\Control\Session Manager. 17. Er liest die Informationen über die Auslagerungsdatei, die in den Werten PagingFiles und ExistingPageFileslist und den Konfigurationswerten PagefileOnOsVolume und WaitForPagingFiles von HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management gespeichert sind. 18. Er liest und speichert die Werte unter HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\ DOS Devices. 19. Er liest und speichert die Liste aus dem Wert KnownDlls in HKLM\SYSTEM\CurrentControlSet\ Control\Session Manager. 20. Er erstellt systemweite Umgebungsvariablen nach der Definition in HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment. Hauptsystemkomponenten Kapitel 2 103
21. Er erstellt das Verzeichnis \KnownDlls und auf 64-Bit-Systemen mit WoW64 auch \Known Dlls32. 22. Er erstellt symbolische Links für die in HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\DOS Devices definierten Geräte im Verzeichnis \Global?? des Objekt-Manager- Namensraums. 23. Er erstellt das Stammverzeichnis \Sessions im Namensraum des Objekt-Managers. 24. Er erstellt Präfixe für geschützte Mailslots und benannte Pipes, um Dienstanwendungen gegen Spoofing-Angriffe zu schützen, die vorkommen können, wenn schädliche Benutzermodusanwendungen vor einem Dienst ausgeführt werden. 25. Er führt die Programme aus, die auf den zuvor analysierten Listen BootExecute und BootExecuteNoPnpSync aufgeführt sind. (Standardmäßig wird Autochk.exe ausgeführt, das die Festplatte überprüft.) 26. Er initialisiert den Rest der Registrierung (die Hives Software, SAM und Security von HKLM). 27. Sofern sie nicht in der Registrierung deaktiviert ist, führt er die WPBT-Binärdatei (Windows Platform Binary Table) aus, die in der zugehörigen ACPI-Tabelle registriert ist. Dies wird oft von Diebstahlsschutzeinrichtungen genutzt, um sehr früh eine native Windows-Binärdatei auszuführen, die selbst auf frisch installierten Systemen nach Hause telefonieren oder andere Dienste zur Ausführung einrichten kann. Diese Prozesse dürfen nur mit Ntdll.dll verlinkt sein (also zum nativen Teilsystem gehören). 28. Er verarbeitet ausstehende Umbenennungen von Dateien, die in den zuvor untersuchten Registrierungsschlüsseln angegeben wurden, sofern es sich nicht um einen Start in die Windows-Wiederherstellungsumgebung handelt. 29. Er initialisiert Informationen über Auslagerungsdateien und dedizierte Dumpdateien auf der Grundlage von HKLM\System\CurrentControlSet\Control\Session Manager\Memory Management und HKLM\System\CurrentControlSet\Control\CrashControl. 30. Er prüft die Kompatibilität des Systems mit Speicherkühltechniken, wie sie auf NUMA- Systemen verwendet werden. 31. Er speichert die alte Auslagerungsdatei, erstellt die dedizierte Absturzabbilddatei sowie nach Bedarf neue Auslagerungsdateien auf der Grundlage der vorherigen Absturzinformationen. 32. Er erstellt zusätzliche dynamische Umgebungsvariablen wie PROCESSOR_ARCHITECTURE, P ROCESSOR_LEVEL, PROCESSOR_IDENTIFIER und PROCESSOR_REVISION auf der Grundlage von Registrierungsinformationen sowie aus dem Kernel abgerufener Systeminformationen. 33. Er führt die Programme in HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\ SetupExecute aus. Dabei gelten die gleichen Regeln wie für BootExecute in Schritt 11. 34. Er erstellt ein unbenanntes Abschnittsobjekt, das von den Kindprozessen (z. B. Csrss.exe) für die mit Smss.exe ausgetauschten Informationen gemeinsam genutzt wird. Das Handle für diesen Abschnitt wird durch Vererbung an die Kindprozesse übergeben. Mehr über die Vererbung von Handles erfahren Sie in Kapitel 8 von Band 2. 104 Kapitel 2 Systemarchitektur
35. Er öffnet bekannte DLLs und ordnet sie als permanente Abschnitte (zugeordnete Dateien) zu – bis auf diejenigen, die bei früheren Registrierungschecks als Ausnahmen aufgeführt wurden. (Standardmäßig sind keine Ausnahmen vorhanden.) 36. Er erstellt einen Thread, um auf Anforderungen zum Erstellen von Sitzungen zu reagieren. 37. Er erstellt eine Instanz von Smss.exe, um Sitzung 0 (die nicht interaktive Sitzung) zu initialisieren. 38. Er erstellt eine Instanz von Smss.exe, um Sitzung 1 (die interaktive Sitzung) zu initialisieren. Bei entsprechender Konfiguration in der Registrierung erstellt er weitere Smss.exe-Instanzen für zusätzliche interaktive Sitzungen, um sich auf künftige Benutzeranmeldungen vorzubereiten. Wenn Smss.exe diese Instanzen erstellt, fordert er jedes Mal mit dem Flag PROCESS_CREATE_NEW_SESSION in NtCreateUserProcess ausdrücklich eine neue Prozess-ID an. Dadurch wird die interne Speicher-Manager-Funktion MiSessionCreate aufgerufen. Sie erstellt die erforderlichen Kernelmodus-Sitzungsdatenstrukturen (wie das Sitzungsobjekt) und richtet den virtuellen Sitzungsadressraum ein, der vom Kernelmodusteil des Windows-Teilsystems (Win32k.sys) und anderen Sitzungsraum-Gerätetreibern genutzt wird (siehe Kapitel 5). Nach Abschluss dieser Schritte wartet Smss.exe unaufhörlich am Handle für die Instanz der Sitzung 0 auf Csrss.exe. Da Csrss.exe als kritischer Prozess gekennzeichnet ist (also als geschützter Prozess; siehe Kapitel 3), wird dieser Wartevorgang bei einer Beendigung von Csrss.exe niemals abgeschlossen, da das System dann abstürzt. Eine Instanz von Smss.exe für den Start einer Sitzung macht Folgendes: QQ Sie erstellt die Teilsystemprozesse für die Sitzung (standardmäßig den Windows-Teilsystemprozess Csrss.exe). QQ Sie erstellt eine Instanz von Winlogon (interaktive Sitzungen) oder des Anfangsbefehls für Sitzung 0. Bei Letzterem handelt es sich standardmäßig um Wininit, sofern in den zuvor untersuchten Registrierungswerten nichts anderes angegeben ist. Weitere Informationen über diese beiden Prozesse finden Sie in den folgenden Abschnitten. Schließlich wird dieser Smss.exe-Zwischenprozess beendet und lässt die Teilsystem-Prozesse und Winlogon oder Wininit ohne Elternprozess zurück. Der Windows-Initialisierungsprozess Der Prozess Wininit.exe führt die folgenden Systeminitialisierungsfunktionen aus: 1. Er kennzeichnet sich selbst und den Hauptthread als kritisch. Wenn er vorzeitig beendet und das System im Debugmodus gestartet wird, steigt er dadurch in den Debugger ein. (Anderenfalls stürzt das System ab.) 2. Er sorgt dafür, dass der Prozess bestimmte Fehler als kritisch behandelt, z. B. Verwendung ungültiger Handles und Beschädigungen des Heaps. 3. Er initialisiert die Unterstützung für die Statustrennung, falls die SKU dies ermöglicht. Hauptsystemkomponenten Kapitel 2 105
4. Er erstellt das Ereignis Global\FirstLogonCheck (es kann in Process Explorer und WinObj im Verzeichnis \BaseNamedObjects beobachtet werden), das die Windows-Anmeldeprozesse nutzen können, um herauszufinden, welche Anmeldung als Erstes gestartet werden soll. 5. Er erstellt ein WinLogonLogoff-Ereignis im Objekt-Manager-Verzeichnis BaseNamedObjects, das von Winlogon-Instanzen verwendet werden kann. Dieses Ereignis wird gesendet, wenn eine Abmeldeoperation beginnt. 6. Er erhöht seine eigene Basispriorität auf 13 und die des Hauptthreads auf 15. 7. Sofern im Registrierungswert NoDebugThread unter HKLM\Software\Microsoft\Windows NT\ CurrentVersion\Winlogon nichts anderes eingerichtet, erstellt er eine Timerwarteschlange, die nach der Festlegung im Kerneldebugger in jeden Benutzermodusprozess einsteigt. Dadurch können Remotekerneldebugger dafür sorgen, dass sich Winlogon an andere Benutzermodusanwendungen anhängt und darin einsteigt. 8. Er richtet den Computernamen in der Umgebungsvariablen COMPUTERNAME ein und aktualisiert und konfiguriert TCP/IP-Informationen wie den Domänen- und den Hostnamen. 9. Er richtet die Umgebungsvariablen USERPROFILE, ALLUSERPROFILES, PUBLIC und ProgramData für das Standardprofil ein. 10. Er erstellt das temporäre Verzeichnis, indem er %SystemRoot%\Temp erweitert (zum Beispiel C:\Windows\Temp). 11. Er richtet das Laden von Schriftarten und DWM ein, wenn Sitzung 0 interaktiv ist, was von der SKU abhängt. 12. Er erstellt das ursprüngliche Terminal, das aus einer Windows-Station (stets mit dem Namen Winsta0) und zwei Desktops (Winlogon und Default) für Prozesse in Sitzung 0 besteht. 13. Er initialisiert den LSA-Maschinenverschlüsselungsschlüssel in Abhängigkeit davon, ob er lokal gespeichert ist oder interaktiv eingegeben werden muss. Weitere Informationen über die Speicherung lokaler Authentifizierungsschlüssel erhalten Sie in Kapitel 7. 14. Er erstellt den Dienststeuerungs-Manager (Services.exe). Mehr darüber erfahren Sie im folgenden Abschnitt und in Kapitel 9 von Band 2. 15. Er startet den Teilsystemdienst für die lokale Sicherheitsauthentifizierung (Lsass.exe) und, falls CredentialGuard aktiviert ist, auch das isolierte LSA-Trustlet (Lsaiso.exe). Dazu ist es erforderlich, den VBS-Bereitstellungsschlüssel von UEFI abzufragen. Mehr über Lsass.exe und Lsaiso.exe erfahren Sie in Kapitel 7. 16. Wenn Setup aussteht (wenn es sich also um den ersten Start bei einer Neuinstallation oder bei der Aktualisierung auf einen neuen Hauptbuild oder ein Insider-Preview des Betriebssystems handelt), startet er das Setupprogramm. 17. Er wartet unaufhörlich auf eine Anforderung, das System herunterzufahren oder eine der zuvor erwähnten Systemprozesse zu beenden (sofern der Registrierungswert Dont WatchSysProcs in dem in Schritt 7 erwähnten Winlogon-Schlüssel nicht gesetzt ist). In jedem Fall fährt er das System herunter. 106 Kapitel 2 Systemarchitektur
Der Dienststeuerungs-Manager Bei einem Dienst kann es sich in Windows um einen Serverprozess oder einen Gerätetreiber handeln. In diesem Abschnitt beschäftigen wir uns mit Diensten, bei denen es sich um Benutzermodusprozesse handelt. Ebenso wie Daemon-Prozesse von Linux können Dienste so eingerichtet werden, dass sie beim Hochfahren des Systems automatisch gestartet werden, ohne dass eine interaktive Anmeldung erforderlich wäre. Sie können auch manuell gestartet werden, etwa in der Verwaltungskonsole Dienste, mit dem Programm Sc.exe und durch Aufrufen der Windows-Funktion StartService. Gewöhnlich interagieren Dienste nicht mit dem angemeldeten Benutzer, wobei dies jedoch unter besonderen Umständen ebenfalls möglich ist. Die meisten Dienste laufen unter besonderen Dienstkonten (z. B. SYSTEM und LOCAL SERVICE), doch einige können auch im selben Sicherheitskontext wie das Konto des angemeldeten Benutzers ausgeführt werden (siehe Kapitel 9 in Band 2). Der Dienststeuerungs-Manager (Service Control Manager, SCM) ist ein besonderer Systemprozess, der das Abbild %SystemRoot%\System32\Services.exe ausführt. Dieses ist dafür verantwortlich, Dienstprozesse zu starten, zu beenden und mit ihnen zu interagieren. Er ist geschützt, weshalb es nicht leicht ist, ihn zu manipulieren. Programme für Dienste sind im Grunde genommen lediglich Windows-Abbilder, die bestimmte Windows-Funktionen aufrufen, um mit dem SCM zu interagieren und beispielsweise den erfolgreichen Start des Dienstes zu regis trieren, auf Statusanforderungen zu reagieren oder den Dienst anzuhalten oder zu beenden. Dienste werden in der Registrierung unter HKLM\SYSTEM\CurrentControlSet\Services definiert. Denken Sie daran, dass Dienste drei Namen haben: den Prozessnamen, den Sie im System sehen, den internen Namen in der Registrierung und den Anzeigenamen in der Verwaltungskonsole Dienste. (Allerdings haben nicht alle Dienste einen Anzeigenamen. Wenn das der Fall ist, wird in der Konsole der interne Name angegeben.) Dienste können auch ein Beschreibungsfeld mit weiteren Einzelheiten darüber enthalten, was sie jeweils tun. Um einen Dienstprozess den darin enthaltenen Diensten zuzuordnen, verwenden Sie den Befehl tlist /s (aus den Debugging-Tools für Windows) oder takslist /svc (in Windows enthalten). Beachten Sie jedoch, dass es nicht immer eine 1:1-Zuordnung zwischen Dienstprozessen und laufenden Diensten gibt, da sich manche Dienste einen Prozess teilen. In der Registrierung gibt der Wert Type im Schlüssel des Dienstes an, ob der Dienst in seinem eigenem Prozess läuft oder ihn sich mit anderen Diensten in seinem Abbild teilt. Eine Reihe von Windows-Komponenten sind als Dienste implementiert, z. B. der Druckspooler, das Ereignisprotokoll, der Taskplaner und einige Netzwerkkomponenten. Mehr über Dienste erfahren Sie in Kapitel 9 von Band 2. Hauptsystemkomponenten Kapitel 2 107
Experiment: Die installierten Dienste auflisten Um die installierten Dienste aufzulisten, öffnen Sie die Systemsteuerung, wählen Verwaltung und dann Dienste. Alternativ können Sie auch auf Start klicken und services.msc ausführen. Anschließend sehen Sie eine Ausgabe wie die folgende: Um ausführliche Informationen über einen Dienst einzusehen, rechtsklicken Sie darauf und wählen Eigenschaften. Das folgende Beispiel zeigt die Eigenschaften des Dienstes Windows Update: Das Feld Pfad für ausführbare Datei gibt das Programm an, das diesen Dienst und seine Befehlszeile enthält. Denken Sie daran, dass einige Dienste ihren Prozess mit anderen teilen. Die Zuordnung ist nicht immer 1:1. 108 Kapitel 2 Systemarchitektur
Experiment: Einzelheiten von Diensten in Dienstprozessen einsehen Process Explorer hinterlegt die Prozesse, die einen oder mehr Dienste beherbergen, in Rosa (wobei Sie die Kennzeichnung ändern können, indem Sie das Menü Options öffnen und dort Configure Color wählen). Wenn Sie auf einen Diensthostprozess doppelklicken, wird die Registerkarte Services angezeigt, die alle Dienste in dem Prozess, den Namen des Registrierungsschlüssels, in dem der Dienst definiert ist, den Anzeigenamen, den der Administrator sieht, und die Beschreibung des Dienstes (falls vorhanden) angibt. Bei Svchost. exe-Diensten wird auch der Pfad zu der DLL angezeigt, die den Dienst implementiert. Das folgende Beispiel zeigt die Dienste in einem der Svchost.exe-Prozesse, die unter dem Systemkonto laufen: Winlogon, LogonUI und Userinit Der Windows-Anmeldeprozess (%SystemRoot%\System32\Winlogon.exe) kümmert sich um die interaktiven An- und Abmeldungen der Benutzer. Wenn ein Benutzer die sichere Aufmerksamkeits-Tastenfolge (Secure Attention Sequence, SAS) eingibt, wird Winlogon.exe von einer Benutzeranmeldeanforderung unterrichtet. (Strg) + (Alt) + (Entf) ist die Standard-SAS auf Windows. Der Grund für die Verwendung einer SAS besteht darin, Benutzer vor Programmen zu schützen, die den Anmeldeprozess simulieren, um Passwörter abzugreifen: Benutzermodusanwendungen können diese Tastenfolge nicht abfangen. Die Identifizierungs- und Authentifizierungsaspekte des Anmeldeprozesses werden durch DLLs realisiert, die als Anmeldeinformationsanbieter bezeichnet werden. Die Standard-Anmeldeinformationsanbieter von Windows implementieren die Standard-Authentifizierungsmöglichkeiten von Windows: Passwörter und Smartcards. Windows 10 enthält auch einen biometrischen Anmeldeinformationsanbieter zur Gesichtserkennung, Windows Hello genannt. Hauptsystemkomponenten Kapitel 2 109
Entwickler können jedoch ihre eigenen Anmeldeinformationsanbieter bereitstellen, um andere Identifizierungs- und Authentifizierungsmechanismen als die übliche Abfrage von Name und Kennwort umzusetzen, z. B. über Stimmerkennung oder Fingerabdrücke. Da Winlogon.exe ein kritischer Prozess ist, den das System unbedingt braucht, läuft das Anmeldedialogfeld, das die Anmeldeinformationsanbieter und die grafische Benutzeroberfläche anzeigt, nur in einem Kindprozess von Winlogon.exe, nämlich LogonUI.exe. Wenn Winlogon.exe die SAS erkennt, startet er diesen Prozess, der wiederum die Anmeldeinformationsanbieter initialisiert. Gibt der Benutzer dann seine Anmeldeinformationen (wie von dem jeweiligen Anbieter verlangt) ein oder schließt er die Anmeldeoberfläche, wird der Prozess LogonUI.exe beendet. Winlogon.exe kann auch zusätzliche DLLs für Netzwerkanbieter laden, die eine zweite Authentifizierung erfordern. Dadurch können während der normalen Anmeldung mehrere Netzwerkanbieter gleichzeitig Identifizierungs- und Authentifizierungsinformationen erfassen. Nachdem der Name und das Kennwort (oder andere vom Anmeldeinformationsanbieter geforderten Angaben) erfasst wurden, werden sie zur Authentifizierung an den Prozess Lsass. exe (Local Security Authentication Service; siehe Kapitel 7) gesendet. Lsass.exe ruft die DLL für das entsprechende Authentifizierungspaket auf, um die eigentliche Überprüfung durchzuführen, also um beispielsweise zu prüfen, ob das angegebene Kennwort mit dem übereinstimmt, das in Active Directory oder SAM (dem Teil der Registrierung, der die Definitionen der lokalen Benutzer und Gruppen enthält) gespeichert ist. Bei einer Domänenanmeldung und aktiviertem CredentialGuard kommuniziert Lsass.exe mit dem isolierten LSA-Trustlet (Lsaiso.exe, siehe Kapitel 7), um den Maschinenschlüssel abzurufen, der erforderlich ist, um die Gültigkeit der Authentifizierungsanforderung zu überprüfen. Bei erfolgreicher Authentifizierung ruft Lsass.exe eine Funktion im SRM auf (z. B. NtCreateToken), um ein Zugriffstokenobjekt mit dem Sicherheitsprofil des Benutzers zu erstellen. Wenn die Benutzerkontensteuerung verwendet wird und der Benutzer ein Mitglied der Administratorengruppe ist oder Administratorrechte hat, legt Lsass.exe noch eine zweite, eingeschränkte Version dieses Tokens an. Dieses Token wird anschließend von Winlogon verwendet, um die Anfangsprozesse in der Benutzersitzung zu erstellen. Sie werden im Registrierungswert Userinit unter dem Schlüssel HKLM\SOFTWARE\Microsoft\WindowsNT\CurrentVersion\Winlogon gespeichert. Standardmäßig wird dabei der Prozess Userinit.exe erstellt, aber die Liste kann mehr als ein Abbild enthalten. Userinit.exe führt einige Initialisierungsarbeiten an der Benutzerumgebung aus, indem er beispielsweise das Anmeldeskript ausführt und die Netzwerkverbindungen wiederherstellt. Anschließend untersucht er den Registrierungswert Shell (unter dem zuvor erwähnten Winlogon-Schlüssel) und erstellt einen Prozess, um die vom System definierte Shell auszuführen (standardmäßig Explorer.exe). Danach wird Userinit.exe beendet. Aus diesem Grund wird Explorer ohne Elternprozess angezeigt: Sein Elternprozess wurde beendet. Wie in Kapitel 1 erwähnt, werden Prozesse, deren Elternprozesse nicht mehr laufen, in tlist.exe und Process Explorer linksbündig angezeigt. Explorer ist sozusagen ein Enkelprozess von Winlogon.exe. Winlogon.exe ist nicht nur bei der An- und Abmeldung von Benutzern aktiv, sondern auch immer dann, wenn er die SAS an der Tastatur erfasst. Wenn Sie etwa (Strg) + (Alt) + (Entf) drücken, während Sie angemeldet sind, erscheint der Sicherheitsbildschirm von Windows mit den Optionen zur Abmeldung, zum Starten des Task-Managers, zum Sperren der Arbeitsstation, 110 Kapitel 2 Systemarchitektur
zum Herunterfahren des Systems usw. Winlogon.exe und LogonUI.exe sind die Prozesse, die sich um diese Vorgänge kümmern. Eine ausführliche Beschreibung der Schritte, die bei der Anmeldung ablaufen, erhalten Sie in Kapitel 11 von Band 2. Die Authentifizierung wird ausführlicher in Kapitel 7 erklärt. Einzelheiten über die aufrufbaren, mit Lsass.exe gekoppelten Funktionen (deren Namen mit Lsa beginnen) finden Sie in der Dokumentation des Windows SDK. Zusammenfassung Dieses Kapitel hat einen groben Überblick über die Systemarchitektur von Windows gegeben. Sie haben die Schlüsselkomponenten von Windows und deren Beziehungen untereinander kennengelernt. Im nächsten Kapitel werfen wir einen genaueren Blick auf Prozesse. Sie gehören zu den grundlegendsten Dingen in Windows. Zusammenfassung Kapitel 2 111
K apite l 3 Prozesse und Jobs In diesem Kapitel erklären wir die Datenstrukturen und Algorithmen für die Prozesse und Jobs in Windows. Als Erstes sehen wir uns dabei an, wie Prozesse erstellt werden. Anschließend untersuchen wir die internen Strukturen, die einen Prozess ausmachen. Danach werfen wir einen Blick auf geschützte Prozesse und ihre Unterschiede zu nicht geschützten. Wir schauen uns auch die erforderlichen Schritte an, um einen Prozess (und seinen Anfangsthread) zu erstellen. Zum Abschluss des Kapitels befassen wir uns mit Jobs. Da Prozesse so viele Windows-Komponenten berühren, werden in diesem Kapitel eine Reihe von Begriffen und Datenstrukturen erwähnt (wie Arbeitssätze, Threads, Objekte, Handles, Heaps usw.), die an anderen Stellen dieses Buches ausführlich erläutert werden. Um dieses Kapitel richtig verstehen zu können, müssen Sie mit den in Kapitel 1 und 2 vorgestellten Begriffen und Grundprinzipien vertraut sein, also z. B. die Unterschiede zwischen einem Prozess und einem Thread und zwischen Benutzer- und Kernelmodus sowie den Aufbau des virtuellen Adressraums von Windows kennen. Prozesse erstellen Die Windows-API enthält mehrere Funktionen, um Prozesse zu erstellen. Die einfachste ist CreateProcess, die versucht, einen Prozess anzulegen, der dasselbe Zugriffstoken wie der Erstellerprozess hat. Wird ein anderes Token benötigt, bietet sich CreateProcessAsUser an, der als (erstes) zusätzliches Argument ein Handle für ein auf irgendeine Weise (z. B. durch Aufruf der Funktion LogonUser) erworbenes Tokenobjekt entgegennimmt. Weitere Funktionen zum Erstellen von Prozessen sind CreateProcessWithTokenW und CreateProcessWithLogonW (die beide zu Advapi32.dll gehören). CreateProcessWithTokenW ähnelt CreateProcessAsUser, allerdings muss der Aufrufer über andere Rechte verfügen (siehe die Dokumentation des Windows SDK). CreateProcessWithLogonW ist eine praktische Möglichkeit, um sich mit den Anmeldeinformationen eines gegebenen Benutzers anzumelden und gleichzeitig einen Prozess mit dem abgerufenen Token zu erstellen. Um den Prozess tatsächlich zu erstellen, rufen beide Funktionen den Dienst Secondary Logon (seclogon.dll in einem SvcHost.exe-Prozess) über einen Remoteprozeduraufruf auf. SecLogon führt diesen Aufruf in seiner internen Funktion SlrCreateProcessWithLogon aus und ruft, wenn alles gut geht, schließlich CreateProcessAsUser auf. Der Dienst SecLogon ist standardmäßig für den manuellen Start eingerichtet, weshalb er erst beim ersten Aufruf von CreateProcessWithTokenW oder CreateProcessWithLogonW gestartet wird. Schlägt der Dienststart fehl (etwa weil ein Administrator den Dienst deaktiviert hat), so schlagen auch diese Funktionen fehl. Das Befehlszeilenprogramm runas, das Sie wahrscheinlich kennen, nutzt diese Funktionen. Abb. 3–1 stellt die beschriebenen Aufrufe grafisch dar. 113
Prozess erstellen SvcHost.Exe Benutzermodus Kernelmodus Exekutive Abbildung 3–1 Funktionen zum Erstellen eines Prozesses. Interne Funktionen sind durch gestrichelte Kästen gekennzeichnet. Alle zuvor erwähnten, dokumentierten Funktionen erwarten eine ordnungsgemäße PE-Datei (Portable Executive; wobei die Endung .exe jedoch nicht unbedingt erforderlich ist), eine Batchdatei oder eine 16-Bit-COM-Anwendung. Darüber hinaus jedoch wissen sie nicht, welche Zusammenhänge zwischen einzelnen Endungen (wie .txt) und ausführbaren Dateien (wie dem Windows-Editor) bestehen. Solche Kenntnisse werden durch die Windows-Shell in Funktionen wie ShellExecute und ShellExecuteEx bereitgestellt. Sie können beliebige Dateien entgegennehmen (nicht nur ausführbare) und versuchen dann, auf der Grundlage der Dateiendung und der Registrierungseinstellungen in HKEY_CLASSES_ROOT die zugehörige ausführbare Datei zu finden (siehe Kapitel 9 in Band 2). Schließlich ruft ShellExecute(Ex) die Funktion CreateProcess mit einer geeigneten ausführbaren Datei auf und hängt die entsprechenden Argumente an die Befehlszeile an, um das zu erreichen, was der Benutzer tun will (z. B. hängt sie den Dateinamen Notepad.exe an, wenn der Benutzer eine .txt-Datei bearbeiten möchte). Letzten Endes führen all diese Ausführungspfade zu der gemeinsamen internen Funktion CreateProcessInternal, die mit der eigentlichen Arbeit beginnt, um einen Windows-Prozess im Benutzermodus zu erstellen. Wenn alles gut geht, ruft CreateProcessInternal schließlich NtCreateUserProcess in Ntdll.dll auf, um in den Kernelmodus überzugehen und mit den Aspekten der Prozesserstellung weiterzumachen, die im Kernelmodus ablaufen. Dies geschieht in einer Funktion der Exekutive mit demselben Namen (NtCreateUserProcess). Argumente der CreateProcess*-Funktionen Es lohnt sich, einen Blick auf die Argumente der CreateProcess*-Funktionen zu werfen. Einigen davon werden Sie in dem Abschnitt über den Ablauf von CreateProcess wiederbegegnen. Ein Prozess, der im Benutzermodus erstellt wird, enthält immer einen Thread, der schließlich die Hauptfunktion der ausführbaren Datei ausführen wird. Die wichtigsten Argumente für CreateProcess*-Funktionen lauten: 114 Kapitel 3 Prozesse und Jobs
QQ Bei CreateProcessAsUser und CreateProcessWithTokenW das Tokenhandle, unter dem der neue Prozess ausgeführt werden soll. Bei CreateProcessWithLogonW sind der Benutzername, die Domäne und das Kennwort erforderlich. QQ Der Pfad der ausführbaren Datei und die Befehlszeilenargumente. QQ Optionale Sicherheitsattribute für den neuen Prozess und das Threadobjekt. QQ Ein boolesches Flag, das angibt, ob alle als vererbbar gekennzeichneten Handles im aktuellen Prozess (dem Erstellerprozess) vom neuen Prozess geerbt (in ihn kopiert) werden sollen. (Mehr über Handles und die Handlevererbung erfahren Sie in Kapitel 8 von Band 2.) QQ Mehrere Flags, die den Vorgang zum Erstellen des Prozesses steuern. Die Dokumentation des Windows SDK enthält die vollständige Liste. Hier geben wir nur einige Beispiele: • CREATE_SUSPENDED Erstellt den Anfangsthread des neuen Prozesses im angehaltenen Zustand. Der Thread wird ausgeführt, wenn Sie später ResumeThread aufrufen. • DEBUG_PROCESS Der Erstellerprozess deklariert sich selbst als Debugger und erstellt den neuen Prozess unter seiner Kontrolle. • EXTENDED_STARTUPINFO_PRESENT Anstelle von STARTUPINFO wird die erweiterte Struktur STARTUPINFOEX bereitgestellt (siehe weiter unten). QQ Optional ein Umgebungsblock für den neuen Prozess, der die Umgebungsvariablen angibt. Wird er nicht angegeben, so wird er vom Erstellerprozess geerbt. QQ Optional das aktuelle Verzeichnis für den neuen Prozess. Wird es nicht angegeben, so wird es vom Erstellerprozess geerbt. Der neue Prozess kann später SetCurrentDirectory aufrufen, um ein anderes Verzeichnis festzulegen. Das aktuelle Verzeichnis eines Prozesses wird in verschiedenen Suchvorgängen ohne Angabe eines vollständigen Pfades genutzt (z. B. beim Laden einer DLL nur mit dem Dateinamen). QQ Eine STARTUPINFO- oder STARTUPINFOEX-Struktur, die noch mehr Konfigurationseinstellungen für den Erstellungsvorgang eines Prozesses bietet. STARTUPINFOEX enthält ein zusätzliches, undurchsichtiges Feld für einen Satz von Prozess- und Threadattributen, bei denen es sich im Grunde genommen um ein Array aus Schlüssel-Wert-Paaren handelt. Die Attribute werden durch den Aufruf von UpdateProcThreadAttributes für jedes benötigte Attribut festgelegt. Einige von ihnen sind nicht dokumentiert und werden intern verwendet, z. B. beim Erstellen von Store-Apps (siehe den nächsten Abschnitt). QQ Eine PROCESS_INFORMATION-Struktur für die Ausgabe beim erfolgreichen Erstellen des Prozesses. Sie enthält die IDs des neuen Prozesses und des neuen Threads sowie die Handles dafür. Diese Handles kann der Erstellerprozess nutzen, wenn er den neuen Prozess oder Thread anschließend auf irgendeine Weise bearbeiten möchte. Moderne Windows-Prozesse erstellen In Kapitel 1 wurde die neue Art von Anwendungen beschrieben, die ab Windows 8 und Windows Server 2012 zur Verfügung stehen. Die Namen solcher Anwendungen wurden im Laufe der Zeit geändert. Wir nennen sie hier moderne Anwendungen, UWP-Anwendungen oder immersive Prozesse, um sie von den klassischen Desktopanwendungen zu unterscheiden. Prozesse erstellen Kapitel 3 115
Um einen Prozess für eine moderne Anwendung zu erstellen, ist mehr erforderlich, als einfach CreateProcess mit dem Pfad der ausführbaren Datei aufzurufen. Es sind auch einige Befehlszeilenargumente und (mithilfe von UpdateProcThreadAttribute) ein nicht dokumentiertes Prozessattribut mit dem Schlüssel PROC_THREAD_ATTRIBUTE_PACKAGE_FULL_NAME anzugeben. Dieses Attribut ist nicht dokumentiert, aber die API bietet noch andere Möglichkeiten, um eine Store-App auszuführen. Beispielsweise enthält die Windows-API die COM-Schnittstelle IApplicationActivationManager, die von einer COM-Klasse mit der Klassen-ID CLSID_ ApplicationActivationManager implementiert wird. Eine der Methoden in dieser Schnittstelle ist ActivateApplication, und damit können Sie eine Store-App starten, nachdem Sie mit einem Aufruf von GetPackageApplicationsIDs die sogenannte AppUserModelId aus dem vollständigen Paketnamen der App abgerufen haben. (Mehr über diese APIs erfahren Sie im Windows SDK.) Mehr über Paketnamen und darüber, wie eine Store-App erstellt wird, wenn ein Benutzer auf die Kachel einer modernen App tippt (wobei letzten Endes ebenfalls CreateProcess aufgerufen wird), erfahren Sie in Kapitel 9 von Band 2. Andere Arten von Prozessen erstellen Windows-Anwendungen starten entweder klassische oder moderne Anwendungen, doch die Exekutive bietet auch Unterstützung für andere Arten von Prozessen, die durch Umgehen der Windows-API gestartet werden müssen. Dazu gehören native, minimale und Pico-Prozesse. Beispielsweise ist der in Kapitel 2 vorgestellte Sitzungs-Manager (Smss) ein natives Abbild. Da er direkt vom Kernel erstellt wird, nutzt er offensichtlich nicht die API CreateProcess, sondern ruft direkt NtCreateUserProcess auf. Wenn Smss die Prozesse Autochk (das Programm zur Überprüfung der Festplatte) oder Csrss (den Windows-Teilsystemprozess) erstellt, steht die WindowsAPI gar nicht zur Verfügung, weshalb ebenfalls NtCreateUserProcess verwendet werden muss. Des Weiteren können Windows-Anwendungen keine nativen Prozesse erstellen, da die Funk tion CreateProcessInternal Abbilder mit dem Typ des nativen Teilsystems ablehnt. Um all diese Schwierigkeiten abzumildern, enthält die native Bibliothek Ntdll.dll die exportierte Hilfsfunk tion RtlCreateUserProcess, die einen einfachen Wrapper um NtCreateUserProcess darstellt. Wie der Name schon sagt, dient NtCreateUserProcess dazu, Benutzermodusprozesse zu erstellen. Wie Sie in Kapitel 2 gesehen haben, enthält Windows jedoch auch eine Reihe von Kernelmodusprozessen wie den System- und den Speicherkomprimierungsprozess (bei denen es sich um minimale Prozesse handelt). Es können auch Pico-Prozesse vorhanden sein, die von einem Anbieter wie dem Windows-Teilsystem für Linux verwaltet werden. Erstellt werden solche Prozesse durch einen Systemaufruf von NtCreateProcessEx, wobei einige Möglichkeiten (etwa das Erstellen von minimalen Prozessen) ausschließlich für Aufrufer aus dem Kernelmodus reserviert sind. Pico-Anbieter rufen die Hilfsfunktion PspCreatePicoProcess auf, die sich sowohl darum kümmert, den minimalen Prozess zu erstellen, als auch dessen Pico-Anbieterkontext zu initialisieren. Diese Funktion wird nicht exportiert und ist nur über eine besondere Schnittstelle ausschließlich für Pico-Anbieter verfügbar. Wie Sie in dem Abschnitt über den Ablauf weiter hinten in diesem Kapitel noch sehen werden, handelt es sich bei NtCreateProcessEx und NtCreateUserProcess um verschiedene System aufrufe, die aber auf dieselben internen Routinen zurückgreifen, um ihre Arbeit zu erledigen, 116 Kapitel 3 Prozesse und Jobs
nämlich PspAllocateProcess und PspInsertProcess. Alle Möglichkeiten, um einen Prozess zu erstellen, die Sie bisher betrachtet haben, und auch alle weiteren Möglichkeiten, die Sie sich vorstellen können – von einem WMI-PowerShell-Cmdlet bis zu einem Kerneltreiber – landen schließlich dort. Interne Mechanismen von Prozessen Dieser Abschnitt beschreibt die wichtigsten Datenstrukturen von Windows-Prozessen, die von verschiedenen Teilen des Systems unterhalten werden, und die Möglichkeiten und Werkzeuge zu ihrer Untersuchung. Jeder Windows-Prozess wird durch eine Exekutivprozessstruktur (EPROCESS) dargestellt. Neben vielen Prozessattributen enthält eine EPROCESS-Struktur auch weitere zugehörige Daten strukturen bzw. verweist darauf. Beispielsweise hat jeder Prozess einen oder mehrere Threads, die jeweils durch eine Exekutivthreadstruktur (ETHREAD) dargestellt werden. (Threaddatenstrukturen werden in Kapitel 4 erklärt.) Die EPROCESS-Struktur und die meisten ihrer zugehörigen Datenstrukturen befinden sich im Systemadressraum. Eine Ausnahme bildet der Prozessumgebungsblock (Process Environment Block, PEB), der sich im Prozess- (Benutzer-) Adressraum befindet (da er Informationen enthält, auf die Benutzermoduscode zugreifen muss). Außerdem sind einige der Prozessdatenstrukturen für die Speicherverwaltung, z. B. die Liste der Arbeitssätze, nur im Kontext des aktuellen Prozesses gültig, da sie im prozessspezifischen Systemadressraum gespeichert sind. (Mehr über den Prozessadressraum erfahren Sie in Kapitel 5.) Für jeden Prozess, der ein Windows-Programm ausführt, unterhält der Prozess des Windows-Teilsystems (Csrss) eine parallele Struktur namens CSR_PROCESS und der Kernelmodusteil des Windows-Teilsystems (Win32k.sys) die Datenstruktur W32PROCESS, die erstellt wird, wenn ein Thread zum ersten Mal die im Kernelmodus implementierte Windows-Funktion USER oder GDI aufruft. Das geschieht, sobald die Bibliothek User32.dll geladen ist. Typische Funktionen, die das Laden dieser Bibliothek veranlassen, sind CreateWindow(Ex) und GetMessage. Da der Kernelmodusteil des Windows-Teilsystems starken Gebrauch von Grafiken mit DirectX-Hardwarebeschleunigung macht, veranlasst die GDI-Infrastruktur (Graphics Device Interface) den DirectX-Grafikkernel (Dxgkrnl.sys), eine eigene Struktur zu initialisieren, nämlich DXGPROCESS. Sie enthält Informationen für DirectX-Objekte (Oberflächen, Schattierer usw.) und die GPGPU-Zähler und Richtlinieneinstellungen für die Zeitplanung sowohl für Rechenvorgänge als auch für die Speicherverwaltung. Außerdem wird beim Leerlaufprozess jede EPROCESS-Struktur vom Objekt-Manager der Exekutive als ein Prozessobjekt gekapselt (siehe Kapitel 8 von Band 2). Da Prozesse keine benannten Objekte sind, werden sie im Sysinternals-Programm WinObj nicht angezeigt. Sie können sich in WinObj jedoch das Type-Objekt Process im Verzeichnis \ObjectTypes ansehen. Ein Handle für einen Prozess ermöglicht durch die Nutzung der APIs für diesen Prozess den Zugriff auf Daten in der EPROCESS-Struktur und einiger ihrer zugehörigen Strukturen. Durch die Registrierung von Prozesserstellungsbenachrichtigungen können viele andere Treiber und Systemkomponenten ihre eigenen Datenstrukturen erstellen, um die Informa tionen, die sie prozessweise speichern, zu verfolgen. (Das ist durch die Exekutivfunktionen Interne Mechanismen von Prozessen Kapitel 3 117
P sSetCreateProcessNotifyRoutine(Ex, Ex2) möglich, die im WDK dokumentiert sind.) Um den Prozessoverhead zu bestimmen, muss die Größe dieser Datenstrukturen oft berücksichtigt werden, wobei es jedoch praktisch unmöglich ist, genaue Zahlen zu ermitteln. Außerdem erlauben einige dieser Funktionen solchen Komponenten, das Erstellen von Prozessen zu verbieten oder zu blockieren. Dadurch erhalten Antimalware-Anbieter in der Architektur eine Möglichkeit, Sicherheitsverbesserungen am Betriebssystem vorzunehmen, z. B. durch hashgestützte Blacklists oder andere Techniken. Sehen wir uns als Erstes das Prozessobjekt an. Abb. 3–2 zeigt die wichtigsten Felder in einer EProcess-Struktur. Prozesssteuerungsblock (PCB) Schutzsperren ID des Prozesses und des Elternprozesses PsActiveProcessHead Link zum aktiven Prozess Prozessflags Erstellungs- und Beendigungszeit des Prozesses Kontingentinformationen Sitzungsobjekt Sitzungsprozesslink Sicherheits-/Ausnahme-/Debug-Ports Primäres Zugriffstoken Job, zu dem der Prozess gehört Handletabelle Prozessumgebungsblock (PEB) Abbilddateiname Abbildbasisadresse Prozesszähler (Speicher, E/A ...) Threadlistenkopf Laufende Nummer Win32k-Prozessstruktur Wow64-Prozessstruktur (nur 64 Bit) Trustlet-Identität (Win 10 x64) Stromverbrauchswerte (Win 10) Pico-Kontext (Win 10) Schutzebene und Signatur DirectX-Prozess Arbeitssatz Abschnittsobjekt Abbildung 3–2 Wichtige Felder in der EPROCESS-Struktur Die Datenstrukturen für einen Prozess folgen einem ähnlichen Aufbau wie die APIs und Komponenten des Kernels, die auf isolierte und geschichtete Module mit ihren eigenen Namenskonventionen verteilt sind. Wie Abb. 3–2 zeigt, ist das erste Element der EPROCESS-Struktur der Prozesssteuerungsblock (Process Control Block, PCB). Dabei handelt es sich um eine Struktur vom Typ KPROCESS (Kernelprozess). Routinen in der Exekutive speichern Informationen in der EPROCESS-Struktur, doch der Dispatcher, der Scheduler und der Interrupt- und Zeiterfassungscode, die zum Betriebssystemkernel gehören, verwenden stattdessen die KPROCESS-Struktur. Dies ermöglicht eine Abstraktionsebene zwischen der höheren Funktionalität der Exekutive und den zugrunde liegenden Implementierungen einzelner Funktionen und hilft, unerwünschte Abhängigkeiten zwischen den Ebenen zu vermeiden. Abb. 3–3 zeigt die wichtigsten Felder einer KPROCESS-Struktur. 118 Kapitel 3 Prozesse und Jobs
Dispatcher-Header Stamm des Seitenverzeichnisses für den Prozess Seitenverzeichnis Kernelzeit Benutzerzeit Zykluszeit Kontextwechsel Threadlistenkopf KTHREAD Prozesssperre Prozessaffinität Prozessflags Idealer Knoten Thread-Seed Basispriorität KPROCESS Prozesslisteneintrag KPROCESS Instrumentierungs-Callback Sichere Prozess-ID Abbildung 3–3 Wichtige Felder der Kernelprozessstruktur Experiment: Das Format einer EPROCESS-Struktur anzeigen Um sich eine Liste der Felder einer EPROCESS und ihrer Offsets im Hexadezimalformat anzusehen, geben Sie im Kerneldebugger dt nt!_eprocess ein. (Mehr über den Kerneldebugger und seine Verwendung für das Kerneldebugging auf dem lokalen System erfahren Sie in Kapitel 1.) Das folgende Listing zeigt eine Ausgabe auf einem 64-Bit-System mit Windows 10, die allerdings aus Platzgründen gekürzt wurde: lkd> dt nt!_eprocess +0x000 Pcb : _KPROCESS +0x2d8 ProcessLock : _EX_PUSH_LOCK +0x2e0 RundownProtect : _EX_RUNDOWN_REF +0x2e8 UniqueProcessId : Ptr64 Void +0x2f0 ActiveProcessLinks : _LIST_ENTRY ... +0x3a8 Win32Process : Ptr64 Void +0x3b0 Job : Ptr64 _EJOB ... +0x418 ObjectTable : Ptr64 _HANDLE_TABLE +0x420 DebugPort : Ptr64 Void +0x428 WoW64Process : Ptr64 _EWOW64PROCESS ... +0x758 SharedCommitCharge : Uint8B +0x760 SharedCommitLock : _EX_PUSH_LOCK → Interne Mechanismen von Prozessen Kapitel 3 119
+0x768 SharedCommitLinks : _LIST_ENTRY +0x778 AllowedCpuSets : Uint8B +0x780 DefaultCpuSets : Uint8B +0x778 AllowedCpuSetsIndirect : Ptr64 Uint8B +0x780 DefaultCpuSetsIndirect : Ptr64 Uint8B Das erste Element dieser Struktur (Pcb) ist eine eingebettete KPROCESS-Struktur. Dort sind die Scheduling- und Zeiterfassungsdaten gespeichert. Das Format der KPROCESS-Struktur können Sie sich auf die gleiche Weise anzeigen lassen wie das der EPROCESS-Struktur: lkd> dt nt!_kprocess +0x000 Header : _DISPATCHER_HEADER +0x018 ProfileListHead : _LIST_ENTRY +0x028 DirectoryTableBase : Uint8B +0x030 ThreadListHead : _LIST_ENTRY +0x040 ProcessLock : Uint4B ... +0x26c KernelTime : Uint4B +0x270 UserTime : Uint4B +0x274 LdtFreeSelectorHint : Uint2B +0x276 LdtTableLength : Uint2B +0x278 LdtSystemDescriptor : _KGDTENTRY64 +0x288 LdtBaseAddress : Ptr64 Void +0x290 LdtProcessLock : _FAST_MUTEX +0x2c8 InstrumentationCallback : Ptr64 Void +0x2d0 SecurePid : Uint8B Mit dem Befehl dt können Sie sich auch den Inhalt eines oder mehrere Felder anzeigen lassen, indem Sie hinter dem Strukturnamen den Feldnamen angeben. Beispielsweise zeigt dt nt!_eprocess UniqueProcessId das Feld mit der eindeutigen Prozess-ID an. Wenn ein Feld wiederum für eine Struktur steht – wie das Feld Pcb in der EPROCESS-Struktur, das selbst eine KPROCESS-Unterstruktur enthält –, können Sie hinter dem Feldnamen einen Punkt angeben, um den Debugger anzuweisen, die Unterstruktur anzuzeigen. Wenn Sie also beispielsweise die KPROCESS-Struktur ansehen wollen, geben Sie dt nt!_eprocess Pcb. ein. Dahinter können Sie wiederum die Namen von Feldern (innerhalb der KPROCESS-Struktur) angeben usw. Mit dem Schalter -r des Befehls dt können Sie sämtliche Unterstrukturen durchlaufen. Wenn Sie hinter dem Schalter eine Zahl angeben, legen Sie damit die Verschachtelungstiefe vor, bis zu der sich der Befehl vorarbeitet. In der zuvor gezeigten Verwendung zeigt der Befehl dt jedoch das Format der ausgewählten Struktur und nicht die Inhalte irgendwelcher Instanzen dieses Strukturtyps an. Um die Instanz eines Prozesses zu sehen, geben Sie die Adresse einer EPROCESS-Struktur als Argument von dt an. Die Adressen fast aller EPROCESS-Strukturen im System (außer für den Leerlaufprozess) können Sie durch den Befehl !process 0 0 abrufen. Da die KPROCESS-Struktur das erste Element der EPROCESS-Struktur ist, können Sie bei dt _kprocess als Adresse der KPROCESS-Struktur auch die Adresse der EPROCESS-Struktur angeben. 120 Kapitel 3 Prozesse und Jobs
Experiment: Der Befehl !process des Kerneldebuggers Der Befehl !process des Kerneldebuggers zeigt einen Teil der Informationen in einem Prozessobjekt und dessen zugehörigen Strukturen an. Die Ausgabe für einen Prozess besteht jeweils aus zwei Teilen. Als Erstes sehen Sie wie im Folgenden Angaben über den Prozess. Wenn Sie keine Prozessadresse oder -ID angeben, gibt !process Informationen über den Prozess aus, dem der zurzeit auf CPU 0 ausgeführte Thread gehört. Auf einem Einzelprozessorsystem kann das auch der WinDbg-Prozess selbst sein (bzw. livekd, falls Sie diesen Debugger statt WinDbg verwenden). lkd> !process PROCESS ffffe0011c3243c0 SessionId: 2 Cid: 0e38 Peb: 5f2f1de000 ParentCid: 0f08 DirBase: 38b3e000 ObjectTable: ffffc000a2b22200 HandleCount: <Data Not Accessible> Image: windbg.exe VadRoot ffffe0011badae60 Vads 117 Clone 0 Private 3563. Modified 228. Locked 1. DeviceMap ffffc000984e4330 Token ffffc000a13f39a0 ElapsedTime 00:00:20.772 UserTime 00:00:00.000 KernelTime 00:00:00.015 QuotaPoolUsage[PagedPool] 299512 QuotaPoolUsage[NonPagedPool] 16240 Working Set Sizes (now,min,max) (9719, 50, 345) (38876KB, 200KB, 1380KB) PeakWorkingSetSize 9947 VirtualSize 2097319 Mb PeakVirtualSize 2097321 Mb PageFaultCount 13603 MemoryPriority FOREGROUND BasePriority 8 CommitCharge 3994 Job ffffe0011b853690 Auf diese Ausgabe von grundlegenden Prozessinformationen folgt eine Liste der Threads in dem Prozess. Diese Ausgabe wird im Experiment »Der Befehl !thread des Kerneldebuggers« in Kapitel 4 erläutert. Es gibt noch weitere Befehle, die Prozessinformationen anzeigen. Dazu gehört auch !handle, der die Prozesshandletabelle ausgibt (siehe Kapitel 8 von Band 2). Sicherheitsstrukturen von Prozessen und Threads sind Thema von Kapitel 7. → Interne Mechanismen von Prozessen Kapitel 3 121
In dieser Ausgabe erhalten Sie auch die Adresse des PEB. Sie können sie im Befehl !peb einsetzen, der im nächsten Experiment beschrieben wird, um sich den PEB eines beliebigen Prozesses auf übersichtliche Weise anzeigen zu lassen. Es ist auch möglich, den regulären Befehl dt mit der _PEB-Struktur zu verwenden. Da sich der PEB im Benutzeradressraum befindet, ist er jedoch nur im Kontext seines eigenen Prozesses gültig. Um sich den PEB eines anderen Prozesses anzusehen, müssen Sie in WinDbg erst zu diesem Prozess wechseln. Das können Sie mit dem Befehl .process /P gefolgt von dem EPROCESS-Zeiger tun. Die Version von WinDbg im aktuellsten Windows 10 SDK zeigt unterhalb der PEB-Adresse einen Hyperlink an, über den Sie die Befehle .process und !peb automatisch ausführen lassen können. Der PEB befindet sich im Benutzeradressraum des Prozesses, den er beschreibt. Er enthält Informationen, die der Abbildlader, der Heap-Manager und andere Windows-Komponenten benötigen, um vom Benutzermodus aus auf den Prozess zuzugreifen. Es wäre zu aufwendig, all diese Informationen über Systemaufrufe verfügbar zu machen. Die EPROCESS- und die KPROCESS-Struktur sind nur vom Kernelmodus aus zugänglich. Die wichtigsten Felder des PEB sehen Sie in Abb. 3–4 Eine ausführliche Erklärung folgt weiter hinten in diesem Kapitel. Basisadresse des Abbilds Laderdatenbank Lokale Threadspeicherdaten Prozessflags Prozesserstellungsparameter Prozessheap (Zeiger) Prozessheap Globale NT-Flags Gemein genutzte GDI-Handletabelle (Zeiger) Anzahl Prozessoren Prozessheapinformationen Ladersperre Angabe der Betriebssytemversion AppCompat-Daten Shim-Daten Sitzungs-ID Lokale Fiberspeicherdaten Assembly- und Aktivierungskontext Gemeinsam genutzter CSRSS-Speicher (Zeiger) Threadpoolinformationen Abbildung 3–4 Wichtige Felder des Prozessumgebungsblocks 122 Kapitel 3 Prozesse und Jobs Gemein genutzte GDI-Handletabelle
Experiment: Den PEB untersuchen Mit dem Befehl !peb des Kerneldebuggers können Sie die PEB-Struktur des Prozesses ausgeben, dem der zurzeit auf CPU 0 ausgeführte Thread gehört. Sie können auch den im vorherigen Experiment gewonnenen PEB-Zeiger als Argument für den Befehl angeben. lkd> .process /P ffffe0011c3243c0 ; !peb 5f2f1de000 PEB at 0000003561545000 InheritedAddressSpace: No ReadImageFileExecOptions: No BeingDebugged: No ImageBaseAddress: 00007ff64fa70000 Ldr 00007ffdf52f5200 Ldr.Initialized: Yes Ldr.InInitializationOrderModuleList: 000001d3d22b3630 . 000001d3d6cddb60 Ldr.InLoadOrderModuleList: 000001d3d22b3790 . 000001d3d6cddb40 Ldr.InMemoryOrderModuleList: 000001d3d22b37a0 . 000001d3d6cddb50 Base TimeStamp Module 7ff64fa70000 56ccafdd Feb 23 21:15:41 2016 C:\dbg\x64\windbg.exe 7ffdf51b0000 56cbf9dd Feb 23 08:19:09 2016 C:\WINDOWS\SYSTEM32\ntdll.dll 7ffdf2c10000 5632d5aa Oct 30 04:27:54 2015 C:\WINDOWS\system32\KERNEL32.DLL ... Die CSR_PROCESS-Struktur enthält spezifische Prozessinformationen des Windows-Teilsystems (Csrss). Daher ist eine solche Struktur nur mit Windows-Anwendungen verbunden. (Beispielsweise hat Smss keine.) Da jede Sitzung ihre eigene Instanz des Windows-Teilsystems aufweist, werden die CSR_PROCESS-Strukturen jeweils vom Csrss-Prozess der Sitzung gepflegt. Den grundlegenden Aufbau einer SCR_PROCESS-Struktur sehen Sie in Abb. 3–5. Eine ausführliche Erklärung folgt weiter hinten in diesem Kapitel. Interne Mechanismen von Prozessen Kapitel 3 123
Client-ID Prozesslink Threadlistenkopf Clientporthandle Prozesshandle Clientsichtdaten Sitzungsdaten Prozessflags Referenzzähler Nachrichtendaten Shutdown-Ebene Eines pro CSR_SERVER_DLL Flags Prozessgruppendaten Serverdaten Abbildung 3–5 Felder einer CSR_PROCESS-Struktur Experiment: Die CSR_PROCESS-Struktur untersuchen Csrss-Prozesse sind geschützt (mehr darüber weiter hinten in diesem Kapitel), weshalb es nicht möglich ist, einen Benutzermodusdebugger daran anzuhängen (nicht einmal mit erhöhten Rechten oder auf nicht invasive Weise). Stattdessen verwenden wir den Kerneldebugger. Als Erstes listen wir die Csrss-Prozesse auf: lkd> !process 0 0 csrss.exe PROCESS ffffe00077ddf080 SessionId: 0 Cid: 02c0 Peb: c4e3fc0000 ParentCid: 026c DirBase: ObjectTable: ffffc0004d15d040 HandleCount: 543. Image: csrss.exe PROCESS ffffe00078796080 SessionId: 1 Cid: 0338 Peb: d4b4db4000 ParentCid: 0330 DirBase: ObjectTable: ffffc0004ddff040 HandleCount: 514. Image: csrss.exe Als Nächstes ändern wir den Debuggerkontext und lassen ihn auf einen dieser Prozesse verweisen, sodass die Benutzermodusmodule sichtbar werden: lkd> .process /r /P ffffe00078796080 Implicit process is now ffffe000'78796080 Loading User Symbols ............. → 124 Kapitel 3 Prozesse und Jobs
Der Schalter /p ändert den Prozesskontext des Debuggers auf den des angegebenen Prozessobjekts (EPROCESS, was hauptsächlich beim Live-Debugging benötigt wird), und /r fordert, die Benutzermodussymbole zu laden. Anschließend können Sie sich die Module mit dem Befehl lm ansehen oder die CSR_PROCESS-Struktur betrachten: lkd> dt csrss!_csr_process +0x000 ClientId : _CLIENT_ID +0x010 ListLink : _LIST_ENTRY +0x020 ThreadList : _LIST_ENTRY +0x030 NtSession : Ptr64 _CSR_NT_SESSION +0x038 ClientPort : Ptr64 Void +0x040 ClientViewBase : Ptr64 Char +0x048 ClientViewBounds : Ptr64 Char +0x050 ProcessHandle : Ptr64 Void +0x058 SequenceNumber : Uint4B +0x05c Flags : Uint4B +0x060 DebugFlags : Uint4B +0x064 ReferenceCount : Int4B +0x068 ProcessGroupId : Uint4B +0x06c ProcessGroupSequence : Uint4B +0x070 LastMessageSequence : Uint4B +0x074 NumOutstandingMessages : Uint4B +0x078 ShutdownLevel : Uint4B +0x07c ShutdownFlags : Uint4B +0x080 Luid : _LUID +0x088 ServerDllPerProcessData : [1] Ptr64 Void W32PROCESS ist die letzte Systemdatenstruktur im Zusammenhang mit Prozessen, die wir uns ansehen wollen. Sie enthält alle Informationen, die der Grafik- und Fensterverwaltungscode im Windows-Kernel (Win32k) benötigt, um die Statusinformationen über die GUI-Prozesse zu unterhalten (die weiter vorn als Prozesse mit mindestens einem USER/GDI-Systemaufruf definiert worden sind). Den grundlegenden Aufbau einer W32PROCESS-Struktur sehen Sie in Abb. 3–6. Da Typinformationen für Win32k-Strukturen in öffentlichen Symbolen nicht verfügbar sind, können wir Ihnen leider kein einfaches Experiment zeigen, mit dem Sie sich diese Informationen selbst ansehen könnten. Allerdings ginge eine Erörterung von Datenstrukturen und Funktionsprinzipien für die Grafik ohnehin über den Rahmen dieses Buches hinaus. Interne Mechanismen von Prozessen Kapitel 3 125
Prozess Referenzzähler Flags Prozess-ID GDI-Handlezähler und -Spitzenwert USER-Handlezähler und -Spitzenwert Prozesslink GDI-Listen DirectX-Prozessdaten DXGPROCESS (Singleton) DirectComposition-Prozessdaten Abbildung 3–6 Felder der W32PROCESS-Struktur Geschützte Prozesse Im Windows-Sicherheitsmodell kann jeder Prozess, der mit einem Token mit Debugrechten ausgeführt wird (z. B. unter einem Administratorkonto), jegliche Zugriffsrechte für beliebige andere Prozesse auf demselben Computer anfordern. Beispielsweise kann er willkürlich im Prozessspeicher lesen und schreiben, Code injizieren, Threads anhalten und wieder aufnehmen und Informationen über andere Prozesse abfragen. Werkzeuge wie Process Explorer und der Task-Manager benötigen solche Zugriffsrechte, um ihre Aufgabe zu erfüllen. Dieses Verhalten ist sinnvoll, da es dafür sorgt, dass Administratoren stets die volle Kon trolle über die Codeausführung in dem System haben, doch es kollidiert mit dem Systemverhalten für digitale Rechteverwaltung, das die Medienbranche für Computerbetriebssysteme zur Wiedergabe von modernen, hochwertigen digitalen Inhalten wie Blu-Ray-Medien fordert. Um eine zuverlässige und geschützte Wiedergabe solcher Inhalte zu ermöglichen, wurden in Windows Vista und Windows Server 2008 geschützte Prozesse eingeführt. Sie existieren neben den normalen Windows-Prozessen und schränken die Zugriffsrechte, die andere Prozesse auf dem System anfordern können (selbst diejenigen mit Administratorrechten), erheblich ein. Jede Anwendung kann geschützte Prozesse erstellen, allerdings lässt das Betriebssystem den Schutz des Prozesses nur dann zu, wenn die Abbilddatei mit einem besonderen Windows-Medienzertifikat signiert wurde. Der Pfad für geschützte Medien (Protected Media Path, PMP) in Windows nutzt geschützte Prozesse, um wertvolle Medien zu schützen. Entwickler von Anwendungen wie DVD-Playern können über die Media-Foundation-API (MF) geschützte Prozesse verwenden. Der Prozess Audio Device Graph (Audiodg.exe) ist geschützt, da über ihn geschützte Musikinhalte decodiert werden können. Damit verwandt ist die Media Foundation Protected Pipeline (Mfpmp.exe), die aus ähnlichen Gründen ein geschützter Prozess ist. (Er wird nicht standardmäßig ausgeführt.) Der Clientprozess Werfaultsecure.exe der Windows-Fehlerberichterstattung (Windows Error Reporting, WER; siehe Kapitel 8 in Band 2) kann ebenfalls geschützt ausgeführt 126 Kapitel 3 Prozesse und Jobs
werden, da er für den Fall, dass ein geschützter Prozess ausfällt, Zugriff darauf haben muss. Auch der Systemprozess selbst ist geschützt, da einige der Entschlüsselungsinformationen vom Treiber Ksecdd.sys generiert und im Benutzermodusspeicher abgelegt werden. Der Schutz des Systemprozesses erfolgt auch, um die Integrität aller Kernelhandles zu wahren (da die Handletabelle des Systemprozesses alle Kernelhandles des Systems enthält). Dass andere Treiber unter Umständen Arbeitsspeicher im Benutzermodus-Adressraum des Systemprozesses zuordnen können (z. B. das Codeintegritätszertifikat und Katalogdaten), ist ein weiterer Grund, den Prozess zu schützen. Auf Kernelebene gibt es zwei Formen der Unterstützung für geschützte Prozesse. Erstens erfolgt der Großteil der Prozesserstellung im Kernelmodul, um Injektionsangriffe zu vermeiden. (Der Ablauf der Erstellung von normalen und von geschützten Prozessen wird ausführlich im nächsten Abschnitt beschrieben.) Zweitens ist in der EPROCESS-Struktur von geschützten Prozessen (und ihren nächsten Verwandten, den im nächsten Abschnitt beschriebenen PPLs [Protected Processes Light]) ein besonderes Bit gesetzt, das das Verhalten von sicherheitsbezogenen Routinen im Prozess-Manager so ändert, dass bestimmte Zugriffsrechte, die Administratoren normalerweise erhalten, verweigert werden. Die einzigen Zugriffsrechte, die für geschützte Prozesse vergeben werden, sind PROCESS_QUERY/SET_LIMITED_INFORMATION, PROCESS_TERMINATE und PROCESS_SUSPEND_RESUME. Manche Zugriffsrechte werden auch für die Threads innerhalb der geschützten Prozesse deaktiviert. Das sehen wir uns in Kapitel 4 im Abschnitt »Interne Strukturen von Threads« genauer an. Da das Programm Process Explorer reguläre Windows-APIs des Benutzermodus verwendet, um Informationen über Prozessinterna abzurufen, kann es bei geschützten Prozessen bestimmte Operationen nicht durchführen. Dagegen läuft WinDbg im Kerneldebuggingmodus und nutzt daher die Kernelmodus-Infrastruktur, um Informationen zu gewinnen, weshalb es damit möglich ist, sämtliche Informationen anzuzeigen. Das Verhalten von Process Explorer beim Umgang mit einem geschützten Prozess wie Audiodg.exe lernen Sie in dem Experiment im Abschnitt »Innere Mechanismen von Threads« von Kapitel 4 kennen. Wie bereits in Kapitel 1 erwähnt, müssen Sie zum lokalen Kerneldebugging im Debuggingmodus starten (indem Sie bcdedit /debug oder die erweiterten Bootoptionen in Msconfig verwenden). Das dient dazu, die Gefahr von debuggergestützten Angriffen auf geschützte Prozesse und den PMP zu verringern. Beim Start im Debuggingmode können High-Definition-Inhalte nicht wiedergegeben werden. Durch die Einschränkung der Zugriffsrechte kann der Kernel geschützte Prozesse wie in einer Sandbox zuverlässig gegen Zugriff vom Benutzermodus abschirmen. Allerdings sind geschützte Prozesse durch ein Flag in der EPROCESS-Struktur gekennzeichnet, sodass Administratoren immer noch in der Lage sind, einen Kernelmodustreiber zu laden, der dieses Flag ändert. Das allerdings würde eine Verletzung des PMP-Modells darstellen und als schädliches Verhalten eingestuft, weshalb das Laden eines solchen Treibers auf einem 64-Bit-System wahrscheinlich blockiert würde, da die Codesignierungsrichtlinie für den Kernelmodus eine digitale Signierung von Schadcode nicht zulässt. Außerdem würden der Kernelmoduspatchschutz (PatchGuard, siehe Kapitel 7) sowie der Treiber für geschützte Umgebung und Authentifizierung (Protected Geschützte Prozesse Kapitel 3 127
Environment and Authentication, Peauth.sys) solche Versuche erkennen und melden. Selbst auf 32-Bit-Systemen muss der Treiber von der PMP-Richtlinie anerkannt werden, da die Wiedergabe sonst angehalten wird. Diese Richtlinie wird von Microsoft implementiert und nicht von irgendeiner Kernelerkennung. Dieser Block würde ein manuelles Eingreifen von Microsoft erfordern, um die Signatur als bösartig zu erkennen und den Kernel zu aktualisieren. Protected Process Light (PPL) Wie Sie gerade gesehen haben, ging es beim ursprünglichen Modell für geschützte Prozesse vor allem um DRM-geschützte Inhalte. Ab Windows 8.1 und Windows Server 2012 R2 wurde das Modell jedoch um Protected Process Light (PPL), also sozusagen die »Light-Version« von geschützten Prozessen erweitert. PPLs werden auf die gleiche Weise geschützt wie herkömmliche geschützte Prozesse: Benutzermoduscode (selbst solcher mit erhöhten Rechten) kann nicht in sie eindringen und Threads injizieren oder Informationen über die geladenen DLLs gewinnen. Beim PPL-Modell kommen jedoch noch Attributwerte hinzu. Die verschiedenen Signierer haben unterschiedliche Vertrauensebenen, was dazu führt, dass manche PPLs stärker geschützt sind als andere. Da sich die digitale Rechteverwaltung von der reinen Multimedia-DRM auch zur WindowsLizenz-DRM und Windows-Store-DRM entwickelt hat, werden herkömmliche geschützte Prozesse jetzt auch anhand ihres Signiererwerts unterschieden. Die einzelnen anerkannten Signierer legen auch die Zugriffsrechte fest, die weniger geschützten Prozessen verweigert werden. Beispielsweise sind normalerweise nur die Zugriffsmasken PROCESS_QUERY/SET_LIMITED_INFORMATION und PROCESS_SUSPEND_RESUME zugelassen. Dagegen ist PROCESS_TERMINATE für bestimmte PPL-Signierer nicht zugelassen. Tabelle 3–1 zeigt die zulässigen Werte für das Schutzflag in der EPROCESS-Struktur. Internes Symbol für die Schutzebene des Prozesses Schutztyp Signierer PS_PROTECTED_SYSTEM (0x72) Geschützt WinSystem PS_PROTECTED_WINTCB (0x62) Geschützt WinTcb PS_PROTECTED_WINTCB_LIGHT (0x61) PPL WinTcb PS_PROTECTED_WINDOWS (0x52) Geschützt Windows PS_PROTECTED_WINDOWS_LIGHT (0x51) PPL Windows PS_PROTECTED_LSA_LIGHT (0x41) PPL Lsa PS_PROTECTED_ANTIMALWARE_LIGHT (0x31) PPL Anti-malware PS_PROTECTED_AUTHENTICODE (0x21) Geschützt Authenticode PS_PROTECTED_AUTHENTICODE_LIGHT (0x11) PPL Authenticode PS_PROTECTED_NONE (0x00) Keine Keine Tabelle 3–1 Gültige Schutzwerte für Prozesse Wie Sie in Tabelle 3–1 sehen, sind mehrere Signierer definiert, hier in der Reihenfolge absteigender Priorität angegeben. WinSystem ist der Signierer höchster Priorität und wird vom Systemprozess sowie von minimalen Prozessen wie dem Speicherkomprimierungsprozess ver- 128 Kapitel 3 Prozesse und Jobs
wendet. Für Benutzermodusprozesse hat WinTCB (Windows Trusted Computer Base) die höchste Priorität und wird zum Schutz kritischer Prozesse genutzt, die der Kernel genau kennt und für die er seine Sicherheitsmaßnahmen reduzieren mag. Geschützte Prozesse haben stets mehr Befugnisse als PPLs, Prozesse mit einem höherwertigen Signierer haben Zugriff auf Prozesse mit einem Signierer geringerer Priorität, aber nicht umgekehrt. Tabelle 3–2 zeigt die Signierer ebenen ( je höher der Wert, umso mehr Befugnisse hat der Signierer) und einige Beispiele ihrer Nutzung. Sie können sie im Debugger mit dem Typ _PS_PROTECTED_SIGNER ausgeben. Signierer (PS_PROTECTED_SIGNER) Ebene Verwendung PsProtectedSignerWinSystem 7 System- und minimale Prozesse (einschließlich Pico-Prozesse) PsProtectedSignerWinTcb 6 Kritische Windows-Komponenten. PROCESS_TERMINATE wird verweigert. PsProtectedSignerWindows 5 Wichtige Windows-Komponenten, die mit sensiblen Daten umgehen PsProtectedSignerLsa 4 Lsass.exe (falls zur geschützten Ausführung eingerichtet) PsProtectedSignerAntimalware 3 Anti-Malware-Dienste und Prozesse, auch von Dritt anbietern. PROCESS_TERMINATE wird verweigert. PsProtectedSignerCodeGen 2 NGEN (native .NET-Codegenerierung) PsProtectedSignerAuthenticode 1 Hosting von DRM-Inhalten und Laden von Benutzermodus-Schriftarten PsProtectedSignerNone 0 Ungültig (kein Schutz) Tabelle 3–2 Signierer und ihre Ebenen Vielleicht fragen Sie sich an dieser Stelle, was einen Schadprozess daran hindert, sich als geschützten Prozess auszugeben und sich gegen Anti-Malware-Anwendungen abzuschirmen. Da für eine Ausführung als geschützter Prozess das Windows-Media-DRM-Zertifikat nicht mehr erforderlich ist, hat Microsoft sein Codeintegritätsmodul so erweitert, dass es die beiden besonderen EKU-OIDs (Enhanced Key Usage) 1.3.6.1.4.1.311.10.3.22 und 1.3.6.4.1.311.10.3.20 versteht, die in Zertifikaten zur Codesignierung verwendet werden können. Ist eine dieser EKUs vorhanden, werden die im Zertifikat hartcodierten Strings für den Signierer und den Aussteller zusammen mit weiteren möglichen EKUs mit den verschiedenen Werten für geschützte Signierer verknüpft. Beispielsweise kann der Herausgeber Microsoft Windows den geschützten Signiererwert PsProtectedSignerWindows gewähren, aber nur dann, wenn die EKU für Windows System Component Verification (1.3.6.1.4.1.311.10.3.6) ebenfalls vorhanden ist. Als Beispiel zeigt Abb. 3–7 das Zertifikat für Smss.exe, der als WinTcb-Light ausgeführt werden darf. Die Schutzebene eines Prozesses bestimmt auch, welche DLLs er laden darf, denn ansonsten kann ein legitimer geschützter Prozess durch einen Bug oder eine einfache Dateiersetzung dazu gebracht werden, eine Drittanbieter- oder eine Schadbibliothek zu laden, die dann mit derselben Schutzebene ausgeführt würde wie der Prozess. Dazu erhält jeder Prozess eine »Signaturebene«, die im Feld SignatureLevel der EPROCESS-Struktur gespeichert wird. In einer internen Nachschlagetabelle wird dann die zugehörige DLL-Signaturebene gesucht, die als SectionSignatureLevel in der EPROCESS-Struktur steht. Jede in den Prozess geladene DLL wird Geschützte Prozesse Kapitel 3 129
von der Codeintegritätskomponente auf diese Weise überprüft wie die ausführbare Hauptdatei. Beispielsweise kann ein Prozess mit dem Signierer WinTcb nur DLLs mit dem Signierer Windows oder höher laden. Abbildung 3–7 Smss-Zertifikat In Windows 10 und Windows Server 2016 sind die Prozesse smss.exe, csrss.exe, services.exe und wininit.exe mit WinTcb-Lite PPL-signiert. Lsass.exe läuft in Windows für ARM-Geräte (z. B. Windows Mobile 10) als PPL und kann auf x86/x64 als PPL ausgeführt werden, wenn er durch eine Registrierungseinstellung oder eine Richtlinie entsprechend konfiguriert ist (siehe Kapitel 7). Außerdem sind einige Dienste so eingerichtet, dass sie als Windows-PPL oder als geschützter Prozess laufen, z. B. sppsvc.exe (Software Protection Platform). Auch manche Diensthostprozesse (Svchost.exe) werden mit dieser Schutzebene ausgeführt, da viele Dienste ebenfalls geschützt sind, z. B. der AppX-Bereitstellungsdienst und der Dienst des Windows-Teilsystems für Linux. Weitere Informationen über solche geschützten Dienste erhalten Sie in Kapitel 9. Für die Sicherheit des Systems ist es von entscheidender Bedeutung, dass diese Kernbinärdateien als TCB ausgeführt werden. So hat beispielsweise Csrss.exe Zugriff auf private APIs, die vom Windows-Manager (Win32k.sys) implementiert werden und über die ein Angreifer mit Administratorrechten Zugriff auf sensible Teile des Kernels gewinnen könnte. Smss.exe und 130 Kapitel 3 Prozesse und Jobs
Wininit.exe implementieren die Logik für Systemstart und -verwaltung, die unbedingt ausgeführt werden müssen, ohne dass sich ein Administrator daran zu schaffen machen kann. Windows garantiert, dass diese Binärdateien stets als WinTcb-Lite ausgeführt werden. Dadurch ist es beispielsweise nicht möglich, dass jemand sie startet, ohne beim Aufruf von CreateProcess die richtige Prozessschutzebene in den Prozessattributen anzugeben. Diese Garantie wird als Mindest-TCB-Liste bezeichnet und erzwingt für die Prozesse aus Tabelle 3–3, die sich in einem Systempfad befinden, eine Mindestschutz- oder -signierebene unabhängig von der Eingabe des Aufrufers. Prozess Mindestsignierebene Mindestschutzebene Smss.exe Aus der Schutzebene abgeleitet WinTcb-Lite Csrss.exe Aus der Schutzebene abgeleitet WinTcb-Lite Wininit.exe Aus der Schutzebene abgeleitet WinTcb-Lite Services.exe Aus der Schutzebene abgeleitet WinTcb-Lite Werfaultsecure.exe Aus der Schutzebene abgeleitet WinTcb-Full Sppsvc.exe Aus der Schutzebene abgeleitet Windows-Full Genvalobj.exe Aus der Schutzebene abgeleitet Windows-Full Lsass.exe SE_SIGNING_LEVEL_WINDOWS 0 Userinit.exe SE_SIGNING_LEVEL_WINDOWS 0 Winlogon.exe SE_SIGNING_LEVEL_WINDOWS 0 Autochk.exe 1 SE_SIGNING_LEVEL_WINDOWS 0 Tabelle 3–3 Mindest-TCB 1 Nur auf UEFI-Firmwaresystemen Geschützte Prozesse Kapitel 3 131
Experiment: Geschützte Prozesse in Process Explorer anzeigen In diesem Experiment sehen wir uns an, wie Process Explorer geschützte Prozesse (beider Arten) anzeigt. Starten Sie Process Explorer und aktivieren Sie auf der Registerkarte Process Image das Kontrollkästchen Protection, um die gleichnamige Spalte einzublenden: Sortieren Sie nun die Schalter Protection in absteigender Reihenfolge und scrollen Sie bis ganz nach oben. Die geschützten Prozesse werden mit ihrem Schutztyp angezeigt. Der folgende Screenshot wurde auf einem x64-Computer mit Windows 10 angefertigt: Wenn Sie einen geschützten Prozess markieren und einen Blick auf den unteren Teil werfen, sehen Sie in der DLL-Ansicht nichts. Das liegt daran, dass Process Explorer die geladenen Module mithilfe von Benutzermodus-APIs abfragt, wozu ein Zugriff erforderlich ist, der für geschützte Prozesse nicht gewährt wird. Die Ausnahme bildet der Systemprozess, der zwar geschützt ist, für den Process Explorer aber trotzdem die Liste der geladenen Kernelmodule (hauptsächlich Treiber) anzeigt, da Systemprozesse keine DLLs enthalten. Für diese Anzeige verwendet Process Explorer die System-API EnumDeviceDrivers, die kein Prozesshandle benötigt. → 132 Kapitel 3 Prozesse und Jobs
In der Handleansicht werden die gesamten Handleinformationen angezeigt. Dazu verwendet Process Explorer eine nicht dokumentierte API, die ebenfalls kein bestimmtes Prozesshandle benötigt. Das Programm kann die Prozesse identifizieren, da mit den Informationen über die Handles auch die PIDs der zugehörigen Prozesse zurückgegeben werden. Unterstützung für Drittanbieter-PPLs Der PPL-Mechanismus dehnt die Schutzmöglichkeiten für Prozesse auch auf ausführbare Dateien aus, die nicht von Microsoft entwickelt wurden. Ein Beispiel dafür bildet Anti-Malware- Software. Typische AM-Produkte bestehen aus den folgenden drei Hauptkomponenten: QQ Einem Kerneltreiber, der E/A-Anforderungen zum Dateisystem und zum Netzwerk abfängt und über Objekt-, Prozess- und Thread-Callbacks Blockiermöglichkeiten einrichtet QQ Einem Benutzermodusdienst (gewöhnlich unter einem privilegierten Konto), der die Treiberrichtlinien konfiguriert, Benachrichtigungen des Treibers über »interessante« Ereignisse empfängt (z. B. über eine infizierte Datei) und mit einem lokalen Server oder dem Internet kommunizieren kann QQ Einem Benutzermodus-GUI-Prozess, der Informationen an den Benutzer vermittelt und ihn ggf. Entscheidungen treffen lässt Eine Angriffsmöglichkeit für Malware besteht darin, Code in einen Prozess mit erhöhten Rechten zu injizieren – am besten noch in einen Anti-Malware-Dienst, um diesen zu modifizieren oder auszuschalten. Läuft der AM-Dienst aber als PPL, so ist weder eine Codeinjektion noch eine Beendigung des Prozesses erlaubt. Dadurch ist die AM-Software besser gegen Malware geschützt (sofern diese Exploits auf Kernelebene einsetzt). Um das zu ermöglichen, muss der zuvor beschriebene AM-Kerneltreiber über einen zugehörigen ELAM-Treiber (Early-Launch Anti Malware) verfügen. ELAM wird in Kapitel 7 ausführlicher beschrieben. Lassen Sie uns hier nur betonen, dass solche Treiber ein besonderes Anti-Malware-Zertifikat benötigen, das von Microsoft ausgestellt wird (nach eingehender Überprüfung des Softwareherstellers). Ein solcher Treiber kann in seiner ausführbaren Hauptdatei (PE) einen benutzerdefinierten Ressourcenabschnitt namens ELAMCERTIFICATEINFO enthalten, der drei zusätzliche Signierer mit jeweils drei zusätzlichen EKUs beschreibt (wobei die Signierer anhand ihres öffentlichen Schlüssels und die EKUs anhand ihrer OIDs bezeichnet werden). Wenn das Codeintegritätssystem feststellt, dass eine Datei von einem dieser drei Signierer mit einem dieser drei EKUs signiert wurde, erlaubt es dem Prozess, einen PPL von PS_PROTECTED_ANTIMALWARE_LIGHT (0x31) anzufordern. Das Standardbeispiel dafür ist Windows Defender, die eigene AM-Software von Microsoft. In Windows 10 ist ihr Dienst (MsMpEng. exe) mit dem AM-Zertifikat signiert, um ihn besser gegen Malwareangriffe gegen ihn selbst zu schützen. Das Gleiche gilt auch für den Network Inspection Server (NisSvc.exe). Geschützte Prozesse Kapitel 3 133
Minimale und Pico-Prozesse Bei den Prozessen, die wir bisher betrachtet haben, sah es so aus, als seien sie zur Ausführung von Benutzermoduscode da und würden dazu erhebliche Mengen an Datenstrukturen im Arbeitsspeicher unterbringen. Allerdings werden nicht alle Prozesse für diesen Zweck verwendet. Wie wir bereits festgestellt haben, ist der Systemprozess lediglich ein Container für die meisten Systemthreads, damit diese bei ihrer Ausführung keine anderen Benutzermodusprozesse kontaminieren, und für die Treiberhandles (Kernelhandles), damit auch diese nicht in den Besitz irgendwelcher Anwendungen geraten. Minimale Prozesse Wenn die Funktion NtCreateProcessEx vom Kernelmodus aufgerufen wird und über ein bestimmtes Flag verfügt, verhält sie sich anders als sonst und verursacht die Ausführung der API PsCreateMinimalProcess. Dadurch wird ein Prozess erstellt, dem viele der zuvor betrachteten Strukturen fehlen: QQ Es wird kein Benutzermodus-Adressraum eingerichtet, weshalb es auch den PEB und die zugehörigen Strukturen nicht gibt. QQ Dem Prozess wird keine NTDLL und auch keine Loader- oder API-Set-Information zugeordnet. QQ Es wird kein Abschnittsobjekt an den Prozess gebunden. Das heißt, mit der Ausführung oder dem Namen (der leer oder ein willkürlicher String sein kann) wird keine ausführbare Abbilddatei verknüpft. QQ In den EPROCESS-Flags wird das Flag Minimal gesetzt. Dadurch werden auch alle Threads zu Minimalthreads und es werden jegliche Benutzermodus-Zuweisungen wie der TEB (siehe Kapitel 4) oder der Benutzermodusstack vermieden. Wie Sie in Kapitel 2 gesehen haben, gibt es in Windows 10 mindestens zwei minimale Prozesse, nämlich den System- und den Speicherkomprimierungsprozess. Ist die virtualisierungsgestützte Sicherheit aktiviert, kann mit dem Secure-System-Prozess noch ein dritter hinzukommen (siehe Kapitel 2 und 7). Die zweite Möglichkeit, um minimale Prozesse auf einem Windows 10-System ausführen zu lassen, besteht darin, das optionale Windows-Teilsystem für Linux (WSL) zu aktivieren (siehe Kapitel 2). Dadurch wird ein im Lieferumfang von Windows enthaltener Pico-Provider installiert, der aus den Treibern Lxss.sys und LxCore.sys besteht. Pico-Prozesse Minimale Prozesse sind nur von beschränktem Nutzen, um Kernelkomponenten Zugriff auf den virtuellen Benutzermodus-Adressraum zu geben und diesen zu schützen. Pico-Prozesse dagegen spielen eine wichtigere Rolle, da sie einer besonderen Komponente – dem Pico-Anbieter – ermöglichen, die meisten Aspekte ihrer Ausführung zu steuern. Damit kann ein solcher Anbieter letzten Endes das Verhalten eines völlig anderen Betriebssystemkernels emulieren, ohne dass die 134 Kapitel 3 Prozesse und Jobs
zugrunde liegende Benutzermodus-Binärdatei weiß, dass sie auf einem Windows-Betriebssystem läuft. Im Grunde genommen handelt es sich dabei um eine Implementierung des Microsoft Research-Projekts Drawbridge, das auf ähnliche Weise auch SQL Server für Linux unterstützt (wenn auch mit einem Windows-Bibliotheksbetriebssystem oberhalb des Linux-Kernels). Um auf einem System Pico-Prozesse verwenden zu können, muss zunächst ein Anbieter vorhanden sein. Er kann mit der API PsRegisterPicoProvider registriert werden, wobei jedoch eine besondere Regel gilt: Der Pico-Anbieter muss vor allen anderen Drittanbietertreibern geladen werden (einschließlich der Boottreiber). Nur jeweils ein einziger aus einem eingeschränkten Satz von etwa einem Dutzend Kerntreibern darf diese API aufrufen, bevor diese Funktionalität deaktiviert wird. Diese Kerntreiber müssen mit einem Microsoft-Signiererzertifikat und einer Windows-Komponenten-EKU signiert sein. Auf Windows-Systemen, auf denen die optionale WSL-Komponente aktiviert ist, handelt es sich hierbei um den Treiber Lxss.sys, der als Stubtreiber fungiert, bis etwas später der Treiber LxCore.sys geladen wird und die Verantwortung für den Pico-Anbieter übernimmt, indem er die Dispatchtabellen auf sich überträgt. Zurzeit kann sich auch nur ein einziger Kerntreiber als Pico-Anbieter registrieren. Wenn ein Pico-Anbieter die Registrierungs-API aufruft, erhält er eine Reihe von Funktionszeigern, mit denen er Pico-Prozesse erstellen und verwalten kann: QQ Zwei Funktionen, um Pico-Prozesse bzw. Pico-Threads zu erstellen QQ Eine Funktion, um den Kontext (einen willkürlichen Zeiger, mit dem der Anbieter bestimmte Daten speichern kann) eines Pico-Prozesses abzurufen, eine, um ihn festzulegen, und zwei weitere Funktionen, um das Gleiche für Pico-Threads zu tun. Damit wird das Feld PicoContext in der ETHREAD- bzw. EPROCESS-Struktur ausgefüllt. QQ Eine Funktion, um die CPU-Kontextstruktur (CONTEXT) eines Pico-Threads abzurufen, und eine, um ihn festzulegen QQ Eine Funktion, um das FS- und GS-Segment eines Pico-Threads zu ändern. Diese Segmente werden normalerweise von Benutzermoduscode verwendet, um auf eine lokale Threadstruktur zu verweisen (wie den TEB in Windows). QQ Zwei Funktionen, um Pico-Threads bzw. Pico-Prozesse zu beenden QQ Eine Funktion, um einen Pico-Thread anzuhalten, und eine weitere, um ihn wieder aufzunehmen Mithilfe dieser Funktionen kann der Pico-Anbieter maßgeschneiderte Prozesse und Threads erstellen, wobei deren ursprünglicher Startzustand, die Segmentregister und die zugehörigen Daten seiner Kontrolle unterliegen. Das allein macht es aber noch nicht möglich, ein anderes Betriebssystem zu emulieren. Es gibt noch einen zweiten Satz von Funktionszeigern, die jedoch vom Anbieter zum Kernel übertragen werden und als Callbacks fungieren, wenn ein Pico-Thread oder -Prozess bestimmte Aktivitäten ausführt. QQ Ein Callback für den Fall, dass ein Pico-Thread mithilfe der Anweisung SYSCALL einen Systemaufruf vornimmt QQ Ein Callback für den Fall, dass ein Pico-Thread eine Ausnahme auslöst QQ Ein Callback für den Fall, dass in einem Pico-Thread ein Fehler bei einer Sondierungs- und Sperroperation an einer Speicherdeskriptorliste (MDL) auftritt Minimale und Pico-Prozesse Kapitel 3 135
QQ Ein Callback für den Fall, dass ein Aufrufer den Namen eines Pico-Prozesses anfordert QQ Ein Callback für den Fall, dass die Ereignisverfolgung für Windows (ETW) die Benutzermodus-Stackablaufverfolgung eines Pico-Prozesses anfordert QQ Ein Callback für den Fall, dass eine Anwendung versucht, ein Handle für einen Pico-Prozess oder -Thread zu öffnen QQ Ein Callback für den Fall, dass jemand die Beendigung eines Pico-Prozesses anfordert QQ Ein Callback für den Fall, dass ein Pico-Prozess oder -Thread unerwartet beendet wird Der Pico-Anbieter nutzt auch den in Kapitel 7 beschriebenen Kernelpatchschutz (Kernel Patch Protection, KPP), um seine Callbacks und Systemaufrufe zu schützen und um zu verhindern, dass sich gefälschte oder schädliche Pico-Anbieter oberhalb von ihm registrieren. Angesichts dieses beispiellosen Zugriffs auf alle möglichen Benutzer-Kernel-Übergänge und sichtbaren Kernel-Benutzer-Interaktionen zwischen sich selbst und der Welt können Pico-Prozesse und -Threads offensichtlich komplett von einem Pico-Anbieter (und entsprechenden Benutzermodusbibliotheken) gekapselt werden, um als Wrapper für eine ganz andere Kernelimplementierung als die von Windows zu dienen. (Wobei es natürlich Ausnahmen gibt, da nach wie vor die Regeln für Threadplanung und Speicherverwaltung gelten, z. B. für Commits.) Ordnungsgemäß geschriebene Anwendungen sollten nicht von solchen internen Algorithmen beeinflusst werden, da diese sich sogar in dem Betriebssystem ändern können, in dem die Anwendungen normalerweise laufen. Pico-Anbieter sind daher letzten Endes maßgeschneiderte Kernelmodule, die die notwendigen Callbacks für die Reaktion auf die von einem Pico-Prozess hervorgerufenen Ereignisse (siehe weiter vorn) implementieren. Dadurch kann das WSL unveränderte Linux-ELF-Binärdateien im Benutzermodus ausführen. Eingeschränkt wird diese Möglichkeit durch die Vollständigkeit der Systemaufrufemulation und des damit verbundenen Funktionsumfangs. Zur Abrundung des Themas sehen Sie in Abb. 3–8 die Strukturen von NT-, minimalen und Pico-Prozessen. NT-Prozess Minimaler Prozess Pico-Prozess Prozessumgebungs block (PEB) Threadumgebungs block (TEB) Gemeinsame Benutzerdaten NTDLL.DLL System aufrufe System aufrufe System aufrufe Benutzermodus Exekutive und Kernel Pico-Anbieter (Lxss/LXCore) Kernelmodus Abbildung 3–8 Arten von Prozessen 136 Kapitel 3 Prozesse und Jobs
Trustlets (sichere Prozesse) Wie bereits in Kapitel 2 erwähnt, enthält Windows neue VBS-Merkmale (Virtualization-Based Security) wie DeviceGuard und CredentialGuard, die mithilfe eines Hypervisors die Sicherheit des Betriebssystems und der Benutzerdaten verstärken. Eines dieser Merkmale, nämlich CredentialGuard (ausführlich beschrieben in Kapitel 7), läuft in der neuen Umgebung des isolierten Benutzermodus, der zwar immer noch unprivilegiert ist (Ring 3), aber eine virtuelle Vertrauens ebene von 1 (VTL 1) aufweist. Dadurch ist er von der regulären Ebene VTL 0 geschützt, auf der sich sowohl der NT-Kernel (Ring 0) als auch die Anwendungen (Ring 3) befinden. Sehen wir uns nun an, wie der Kernel solche Prozesse zur Ausführung einrichtet und welche Datenstrukturen diese Prozesse verwenden. Der Aufbau von Trustlets Trustlets sind zwar reguläre PE-Dateien (Windows Portable Executables), weisen aber einige IUM-spezifische Eigenschaften auf: QQ Aufgrund der eingeschränkten Anzahl von Systemaufrufen, die für Trustlets zur Verfügung stehen, können sie nur Funktionen aus einer eingeschränkten Menge von Windows- System-DLLs importieren (C/C++ Runtime, KernelBase, Advapi, RPC Runtime, CNG Base Crypto und NTDLL). Mathematische DLLs, die nur Datenstrukturen bearbeiten (wie NTLM, ASN.1 usw.), können jedoch ebenfalls verwendet werden, da sie keine Systemaufrufe durchführen. QQ Sie können Funktionen von einer IUM-spezifischen System-DLL namens Iumbase importieren, die ihnen zur Verfügung gestellt wird. Diese DLL enthält die grundlegende IUMSystem-API mit Unterstützung für Mailslots, Speicherboxen, Kryptografie usw. Diese Bibliothek führt letzten Endes Aufrufe in Iumdll.dll durch, der VTL-1-Version von Ntdll.dll, und enthält sichere Systemaufrufe (also solche, die vom sicheren Kernel implementiert werden und nicht an den normalen VTL-0-Kernel übergeben werden). QQ Sie enthalten den PE-Abschnitt .tPolicy mit der exportierten globalen Variablen s_Ium PolicyMetadata. Damit werden Metadaten für den sicheren Kernel bereitgestellt, um Richtlinieneinstellungen für den VTL-0-Zugriff des Trustlets zu implementieren (z. B. um Debugging zu erlauben, Unterstützung für Absturzabbilder bereitzustellen usw.). QQ Sie werden mit einem Zertifikat signiert, dass die IUM-EKU enthält (1.3.6.1.4.311.10.3.37). Abb. 3–9 zeigt die Zertifikatdaten für LsaIso.exe mit dieser EKU. Außerdem muss beim Start von Trustlets mit CreateProcess ein besonderes Prozessattribut verwendet werden, um die Ausführung im IUM anzufordern und um besondere Starteigenschaften anzugeben. Die Richtlinien-Metadaten und die Prozessattribute beschreiben wir in den folgenden Abschnitten. Trustlets (sichere Prozesse) Kapitel 3 137
Abbildung 3–9 Trustlet-EKU im Zertifikat Richtlinien-Metadaten für Trustlets Zu den Richtlinien-Metadaten gehören verschiedene Optionen, um festzulegen, wie »zugänglich« das Trustlet von VLT 0 aus sein soll. Sie werden durch eine Struktur beschrieben, die sich in der zuvor erwähnten exportierten Variablen s_IumPolicyMetadaten befindet, und enthalten die Versionsnummer (zurzeit 1) und die Trustlet-ID, also eine eindeutige Zahl zur Bezeichnung eines bestimmten der bekannten Trustlets. Beispielsweise hat BioIso.exe die Trustlet-ID 4. Des Weiteren gehört zu den Metadaten ein Array mit Richtlinienoptionen. Zurzeit werden die Optionen aus Tabelle 3–4 unterstützt. Da diese Richtlinien Teil der signierten ausführbaren Daten sind, verletzt jeder Versuch, sie zu ändern, die IUM-Signatur und verhindert damit eine Ausführung. Richtlinie Bedeutung ETW Aktiviert oder deaktiviert ETW Debug Konfiguriert das Debugging Crash Dump Aktiviert oder deaktiviert Absturzabbilder Crash Dump Key Gibt den öffentlichen Schlüssel zur Verschlüsselung von Absturzabbildern an Weitere Informationen Das Debugging kann jederzeit aktiviert sein; nur bei deaktiviertem SecureBoot; oder bei Bedarf über einen Challenge/ Response-Mechanismus. Absturzabbilder können an das Microsoft-Produktteam übermittelt werden, das über den privaten Schlüssel für die Entschlüsselung verfügt. → 138 Kapitel 3 Prozesse und Jobs
Richtlinie Bedeutung Weitere Informationen Crash Dump GUID Gibt den eindeutigen Bezeichner für den Absturzabbildschlüssel an Dadurch kann das Produktteam mehrere Schlüssel verwenden bzw. erkennen. Parent Security Descriptor SDDL-Format Dient zur Validierung, dass der Besitzeroder Elternprozess der erwartete ist. Parent Security Descriptor Revision Revisions-ID des SDDL-Formats Dient zur Validierung, dass der Besitzeroder Elternprozess der erwartete ist. SVN Sicherheitsversion Eine eindeutige Zahl, die das Trustlet (zusammen mit seiner ID) bei der Verschlüsselung von AES256/GCM-Nach richten nutzen kann. Device ID PCI-Bezeichner des sicheren Geräts Das Trustlet kann nur mit einem sicheren Gerät kommunizieren, das den entsprechenden PCI-Bezeichner aufweist. Capability Aktiviert VTL-1-Fähigkeiten Aktiviert den Zugriff auf die API Create Secure Section; DRM- und Benutzermodus-MMIO-Zugriff auf sichere Geräte; und APIs für die sichere Speicherung. Scenario ID Gibt die Szenario-ID für die Binärdatei an Trustlets müssen diesen GUID angeben, wenn sie sichere Abbildabschnitte erstellen, um sicherzustellen, dass es sich um ein bekanntes Szenario handelt. Tabelle 3–4 Trustlet-Richtlinienoptionen Attribute von Trustlets Der Start eines Trustlets erfordert die ordnungsgemäße Verwendung des Attributs PS_CP_ SECURE_PROCESS. Es dient zur Bestätigung, dass der Aufrufer wirklich ein Trustlet erstellen möchte, und zur Überprüfung, dass das Trustlet, das der Aufrufer auszuführen glaubt, auch tatsächlich das Trustlet ist, das ausgeführt wird. Dazu wird eine Trustlet-ID in das Attribut eingebettet. Sie muss mit der Trustlet-ID in den Richtlinien-Metadaten übereinstimmen. Es können auch noch weitere Attribute angegeben werden, die in Tabelle 3–5 aufgeführt sind. Attribut Bedeutung Weitere Informationen Mailbox Key Dient zum Abrufen von Postfachdaten Über Postfächer kann ein Trustlet Daten mit VLT-0-Anwendungen gemeinsam nutzen, sofern der Trustlet-Schlüssel bekannt ist. Collaboration ID Legt die Kollaborations-ID fest, die zur Verwendung der IUMAPI Secure Storage benötigt wird Die Secure-Storage-API ermöglicht Trustlets mit gemeinsamer Kollaborations-ID, Daten untereinander gemeinsam zu nutzen. Ist keine Kollaborations-ID vorhanden, wird stattdessen die ID der Trustlet-Instanz verwendet. TK Session ID Die Sitzungs-ID, die bei kryptografischen Vorgängen verwendet wird Tabelle 3–5 Trustlet-Attribute Trustlets (sichere Prozesse) Kapitel 3 139
Integrierte System-Trustlets Zurzeit enthält Windows 10 die fünf Trustlets, die in Tabelle 3–6 aufgeführt sind. Die Trustlet-ID 0 steht für den sicheren Kernel selbst. Binärdatei mit Trustlet-ID in Klammern Bezeichnung Richtlinienoptionen LsaIso.exe (1) Credential and Key Guard Trustlet Allow ETW, Disable Debugging, Allow Encrypted Crash Dump Vmsp.exe (2) Secure Virtual Machine Worker (vTPM Trustlet) Allow ETW, Disable Debugging, Dis able Crash Dump, Enable Secure Storage Capability, Verify Parent Security Descriptor is S-1-5-83-0 (NT VIRTUAL MACHINE\Virtual Machines) Unbekannt (3) vTPM Key Enrollment Trustlet Unbekannt BioIso.exe (4) Secure Biometrics Trustlet Allow ETW, Disable Debugging, Allow Encrypted Crash Dump FsIso.exe (5) Secure Frame Server Trustlet Disable ETW, Allow Debugging, Enable Create Secure Section Capability, Use Scenario ID { AE53FC6E-8D89-44889D2E-4D008731C5FD} Tabelle 3–6 Integrierte Trustlets Trustlet-Identität Trustlets verfügen über verschiedene Identitätsangaben: QQ Trustlet-ID Dies ist ein Integerwert, der in den Richtlinien-Metadaten des Trustlets hartcodiert ist und in den Prozesserstellungsattributen für das Trustlet verwendet werden muss. Dadurch weiß das System, dass es nur eine Handvoll Trustlets gibt. Anhand der ID wird auch sichergestellt, dass Aufrufer tatsächlich das gewünschte Trustlet starten. QQ Trustlet-Instanz Hierbei handelt es sich um eine kryptografisch sichere 16-Byte-Zufallszahl, die vom sicheren Kernel erstellt wird. Gibt es keine Kollaborations-ID, so wird die Trustlet-Instanz herangezogen, um dafür zu sorgen, dass Secure-Storage-APIs nur dieser einen Instanz des Trustlets erlauben, Daten in ihr Storage-BLOB zu stellen und von dort abzurufen. QQ Kollaborations-ID Diese ID wird verwendet, wenn ein Trustlet anderen Trustlets mit derselben ID oder anderen Instanzen desselben Trustlets den gemeinsamen Zugriff auf ein Secure-Storage-BLOB erlauben möchte. Ist diese ID vorhanden, wird die Instanz-ID beim Aufruf der Get- und Put-APIs ignoriert. QQ Sicherheitsversion (SVN) Sie wird für Trustlets verwendet, die einen starken kryptografischen Beweis für den Ursprung signierter oder verschlüsselter Daten verlangen, etwa bei der Verschlüsselung von AES256/GCM-Daten durch Credential- und KeyGuard und für den Dienst Cryptograph Report. 140 Kapitel 3 Prozesse und Jobs
QQ Szenario-ID Sie wird für Trustlets verwendet, die benannte sichere Kernelobjekte erstellen, z. B. sichere Abschnitte. Diese Objekte werden im Namensraum mit diesem GUID gekennzeichnet, um zu bestätigen, dass das Trustlet sie im Rahmen eines vorherbestimmten Szenarios erstellt hat. Andere Trustlets, die diese benannten Objekte öffnen wollen, müssen über dieselbe Szenario-ID verfügen. Es ist zwar möglich, dass mehr als eine Szenario-ID vorhanden ist, doch zurzeit verwenden Trustlets nicht mehr als eine. IUM-Dienste Die Ausführung als Trustlet bietet nicht nur den Vorteil des Schutzes gegen Angriffe aus der normalen VLT-0-Umgebung, sondern bietet auch Zugriff auf privilegierte und geschützte sichere Systemaufrufe, die der sichere Kernel nur für Trustlets bereitstellt. Dazu gehören die folgenden Dienste: QQ Sichere Geräte (IumCreateSecureDevice, IumDmaMapMemory, IumGetDmaEnabler, IumMapSecureIo, IumProtectSecureIo, IumQuerySecureDeviceInformation, IopUnmapSecureIo, IumUpdateSecureDeviceState) Sie bieten Zugriff auf sichere ACPI- und PCI-Geräte, die nicht von VTL 0 aus zugänglich sind und exklusiv dem sicheren Kernel (und seinen Hilfsdiensten für die sichere HAL und das sichere PCI) gehören. Trustlets mit entsprechenden Fähigkeiten (siehe den Abschnitt »Richtlinien-Metadaten für Trustlets« weiter vorn in diesem Kapitel) können die Register solcher Geräte auf den VTL-1-IUM abbilden und auch Übertragungen mit direktem Speicherzugriff vornehmen (Direct Memory Access, DMA). Außerdem können sie als Benutzermodus-Gerätetreiber für solche Hardware dienen, indem sie das Framework für sichere Geräte (Secure Device Framework, SDF) aus SDFHost.dll verwenden. Diese Funktionalität wird für biometrische Verfahren in Windows Hello verwendet, z. B. für die Verwendung von USB-Smartcards (über PCI) und von Webcams und Fingerabdrucksensoren (über ACPI). QQ Sichere Abschnitte (IumCreateSecureSection, IumFlushSecureSectionBuffers, IumGet ExposedSecureSection, IumOpenSecureSection) Über sichere Abschnitte ist es möglich, physische Seiten gemeinsam mit einem VTL-0-Treiber zu nutzen (der dazu VslCreateSecureSection verwenden muss). Es können auch Daten als benannte sichere Abschnitte innerhalb von VTL 1 mit anderen Trustlets oder anderen Instanzen desselben Trustlets geteilt werden (wobei der zuvor im Abschnitt »Trustlet-Identität« erwähnte Identitätsmechanismus eingesetzt wird). Um dies tun zu können, müssen die Trustlets über die im Abschnitt »RichtlinienMetadaten für Trustlets« erwähnte Fähigkeit Secure Sections verfügen. QQ Postfächer (IumPostMailbox) Damit kann sich ein Trustlet bis zu acht Slots von maximal ca. 4 KB Daten mit einer Komponente im normalen VLT-0-Kernel teilen, der dazu VslRetrieveMailbox aufruft und die Slot-ID und den geheimen Postfachschlüssel angibt. Dies wird unter anderem von Vid.sys in VTL 0 verwendet, um verschiedene Geheimnisse abzurufen, die vTPM im Trustlet Vmsp.exe nutzt. Trustlets (sichere Prozesse) Kapitel 3 141
QQ Identitätsschlüssel (IumGetIdk) Damit kann das Trustlet einen eindeutig kennzeichnenden Entschlüsselungsschlüssel oder einen Signierschlüssel abrufen. Das Schlüssel material bezeichnet eindeutig den Computer und kann nur von einem Trustlet bezogen werden. Dies ist ein entscheidender Aspekt von CredentialGuard, um den Computer zu authentifizieren und zu bestätigen, dass die Anmeldeinformationen tatsächlich aus dem IUM kommen. QQ Kryptografiedienste (IumCrypto) Sie erlauben einem Trustlet, Daten mit einem lokalen oder Bootsitzungsschlüssel zu verschlüsseln und zu entschlüsseln. Dieser Schlüssel wird vom sicheren Kernel erstellt und steht nur im IUM zur Verfügung. Des Weiteren kann ein Trustlet damit ein TPM-Bindungshandle erlangen; den FIPS-Modus des sicheren Kernels abrufen; einen Seed des Zufallszahlengenerators gewinnen, der ausschließlich vom sicheren Kernel für IUM generiert wird; einen Bericht mit IDK-Signatur, SHA-2-Hash, Zeitstempel und der Identität und SVN des Trustlets anlegen; eine Abbilddatei seiner Richtlinien-Metadaten unabhängig davon erstellen, ob sie jemals mit einem Debugger verknüpft wurden oder nicht; und jegliche andere angeforderten Daten generieren, die seiner Kontrolle unterliegen. Das lässt sich als eine Art von TPM-Maßnahme nutzen, mit der das Trustlet beweisen kann, dass es nicht manipuliert worden ist. QQ Sicherer Speicher (IumSecureStorageGet, IumSecureStoragePut) Dadurch bekommt das Trustlet die Fähigkeit Secure Storage (wie zuvor im Abschnitt »Richtlinien-Metadaten für Trustlets« beschrieben), um BLOBs willkürlicher Größe zu speichern und abzurufen. Der Zugriff auf das BLOB kann auf die jeweilige Trustlet-Instanz beschränkt sein oder anderen Trustlets mit derselben Kollaborations-ID offenstehen. Für Trustlets zugängliche Systemaufrufe In dem Bemühen, seine Angriffsfläche zu reduzieren, stellt der sichere Kernel nur eine Teilmenge von weniger als fünfzig der Hunderte von Systemaufrufen zur Verfügung, die eine normale VLT-0-Anwendung nutzen kann. Diese Aufrufe bilden das absolute Minimum für eine Kompatibilität mit den DLLs, die von Trustlets verwendet werden können (welche das sind, erfahren Sie im Abschnitt »Der Aufbau von Trustlets«), und den Diensten, die zur Unterstützung der RPC-Laufzeit (Rpcrt4.dll) und der ETW-Verfolgung erforderlich sind. QQ Worker-Factory- und Thread-APIs Sie unterstützen die von RPCs verwendete Threadpool-API und die vom Lader verwendeten TLS-Slots. QQ Prozessinformations-API Sie unterstützt TLS-Slots und die Threadstackzuweisung. QQ Ereignis-, Semaphoren-, Warte- und Vervollständigungs-APIs Threadpools und Synchronisierung. Sie unterstützten QQ ALPC-APIs (Advanced Local Procedure Calls) Sie unterstützen lokale RPCs über den Transport ncalrpc. QQ Systeminformations-API Sie ermöglicht das Lesen von Informationen über den sicheren Start, grundlegenden und NUMA-Systeminformationen für Kernel32.dll und die Threadpoolskalierung, Leistungsinformationen und einigen Zeitinformationen. QQ Token-API Sie bietet minimale Unterstützung für RPC-Identitätswechsel. 142 Kapitel 3 Prozesse und Jobs
QQ APIs für virtuelle Speicherzuweisung Manager des Benutzermodus. Sie unterstützen Zuweisungen durch den Heap- QQ Abschnitts-APIs Sie unterstützen den Lader (für DLL-Abbilder) und die Funktionalität sicherer Abschnitte (sobald sie durch die zuvor angegebenen sicheren Systemaufrufe erstellt bzw. verfügbar gemacht worden sind). QQ Steuer-API für die Ablaufverfolgung Sie unterstützt ETW. QQ Ausnahme- und Fortsetzungs-API Sie unterstützt die strukturierte Ereignisbehandlung (Structured Exception Handling, SEH). Diese Liste zeigt deutlich, dass es keine Unterstützung für Operationen wie die folgenden gibt: Geräte-E/A, unabhängig davon, ob es sich um Dateien oder physische Geräte handelt (es gibt schon einmal keine CreateFile-API); Registrierungs-E/A; Erstellen anderer Prozesse; jegliche Verwendung von Grafik-APIs (es gibt keinen Win32k.sys-Treiber in VTL 1). Trustlets sind als isolierte Arbeits-Back-Ends in VTL 1 für ihre komplexen Front-Ends in VTL 0 gedacht. Als einzige Kommunikationsmechanismen stehen ihnen ALPC und bereitgestellte sichere Abschnitte zur Verfügung (wobei ihnen die Handles dieser sicheren Abschnitte über ALPC vermittelt werden müssen). In Kapitel 7 schauen wir uns die Implementierung des Trustlets LsaIso.exe genauer an, das für CredentialGuard und KeyGuard zuständig ist. Experiment: Sichere Prozesse identifizieren Abgesehen von ihrem Namen können Sie sichere Prozesse im Kerneldebugger auf zwei verschiedene Weisen erkennen. Erstens verfügt jeder sichere Prozess über eine sichere PID, die in der Handletabelle des sicheren Kernels für sein Handle steht. Sie wird vom normalen VLT-0-Kernel verwendet, wenn er Threads in dem Prozess erstellt oder dessen Beendigung anfordert. Zweitens ist mit den Threads ein Threadcookie verknüpft, das ihren Index in der Threadtabelle des sicheren Kernels angibt. Probieren Sie Folgendes im Kerneldebugger aus: lkd> !for_each_process .if @@(((nt!_EPROCESS*)${@#Process})->Pcb.SecurePid) { .printf "Trustlet: %ma (%p)\n", @@(((nt!_EPROCESS*)${@#Process}) ->ImageFileName), @#Process } Trustlet: Secure System (ffff9b09d8c79080) Trustlet: LsaIso.exe (ffff9b09e2ba9640) Trustlet: BioIso.exe (ffff9b09e61c4640) lkd> dt nt!_EPROCESS ffff9b09d8c79080 Pcb.SecurePid +0x000 Pcb : +0x2d0 SecurePid : 0x00000001'40000004 lkd> dt nt!_EPROCESS ffff9b09e2ba9640 Pcb.SecurePid +0x000 Pcb : +0x2d0 SecurePid : 0x00000001'40000030 → Trustlets (sichere Prozesse) Kapitel 3 143
lkd> dt nt!_EPROCESS ffff9b09e61c4640 Pcb.SecurePid +0x000 Pcb : +0x2d0 SecurePid : 0x00000001'40000080 lkd> !process ffff9b09e2ba9640 4 PROCESS ffff9b09e2ba9640 SessionId: 0 Cid: 0388 Peb: 6cdc62b000 ParentCid: 0328 DirBase: 2f254000 ObjectTable: ffffc607b59b1040 HandleCount: 44. Image: LsaIso.exe THREAD ffff9b09e2ba2080 Cid 0388.038c Teb: 0000006cdc62c000 Win32Thread: 0000000000000000 WAIT lkd> dt nt!_ETHREAD ffff9b09e2ba2080 Tcb.SecureThreadCookie +0x000 Tcb +0x31c SecureThreadCookie : 9 Der Ablauf von CreateProcess Wir haben uns bereits die verschiedenen Datenstrukturen für die Bearbeitung und Verwaltung des Prozesszustands und die Werkzeuge und Debuggerbefehle angesehen, mit denen sie sich untersuchen lassen. In diesem Abschnitt beschäftigen wir uns damit, wie diese Datenstrukturen erstellt und ausgefüllt und wie Prozesse erstellt und beendet werden. Wie bereits erwähnt, rufen alle Funktionen zur Prozesserstellung letzten Endes CreateProcessInternalW auf, weshalb wir damit beginnen. Um einen Windows-Prozess zu erstellen, werden mehrere Schritte in drei Teilen des Betriebssystems ausgeführt: in der clientseitigen Windows-Bibliothek Kernel32.dll (die eigentliche Arbeit beginnt mit CreateProcessInternalW), in der Windows-Exekutive und im Windows-Teilsystemprozess (Csrss). Aufgrund der Teilsystemarchitektur für mehrere Umgebungen wird die Erzeugung eines Exekutivprozessobjekts (das auch andere Teilsysteme nutzen können) von den Arbeiten zum Erstellen eines Windows-Teilsystemprozesses getrennt. Die folgende Beschreibung des Ablaufs der Windows-Funktion CreateProcess ist zwar kompliziert, doch denken Sie daran, dass dieser Teil der Arbeit nur für die zusätzliche Semantik des Windows-Teilsystems erforderlich ist und nicht zu der grundlegenden Arbeit zum Erstellen eines Exekutivprozessobjekts gehört. Die folgende Übersicht zeigt die Hauptphasen beim Erstellen eines Prozesses mit den CreateProcess*-Funktionen von Windows. Die in den jeweiligen Phasen ausgeführten Operationen werden in den anschließenden Abschnitten ausführlich erläutert. Viele Schritte von CreateProcess hängen mit der Einrichtung des virtuellen Adress raums zusammen, weshalb dabei viele Begriffe und Strukturen der Speicherverwaltung erwähnt werden, deren Definition Sie in Kapitel 5 finden. 144 Kapitel 3 Prozesse und Jobs
1. Parameter validieren; Windows-Teilsystemflags und -optionen in ihre nativen Gegenstücke umwandeln; Attributliste analysieren, validieren und in ihr natives Gegenstück umwandeln. 2. Die Abbilddatei (.exe) öffnen, die in dem Prozess ausgeführt werden soll. 3. Das Windows-Exekutivprozessobjekt erstellen. 4. Den ursprünglichen Thread erstellen (Stack, Kontext und Windows-Exekutivthreadobjekt). 5. Spezifische Prozessinitialisierung für das Windows-Teilsystem durchführen. 6. Ausführung des ursprünglichen Threads starten (sofern nicht das Flag CREATE_SUSPENDED angegeben war). 7. Initialisierung des Adressraums im Kontext des neuen Prozesses und Threads abschließen (z. B. durch Laden der erforderlichen DLLs) und die Ausführung des Programmeintrittspunkts beginnen. Abb. 3–10 gibt einen Überblick über die Phasen, die Windows beim Erstellen eines Prozesses durchläuft. Prozess erstellen Phase 1 Parameter und Flags konvertieren und validieren Phase 2 EXE öffnen und Abschnittsobjekt erstellen Phasen 3+4 Prozess- und ThreadObjekte erstellen Phase 5 Spezifische Prozessinitia lisierung für das WindowsTeilsystem durchführen Phase 6 Ausführung des ursprüng lichen Threads starten Windows-Teilsystem Für neuen Prozess und Thread einrichten Neuer Prozess Phase 6 Abschließende Prozessinitialisierung Ausführung des Eintrittspunkts beginnen Fertig Abbildung 3–10 Die Hauptphasen der Prozesserstellung Phase 1: Parameter und Flags konvertieren und validieren Bevor die Funktion CreateProcessInternalW das auszuführende Image öffnet, führt sie folgende Schritte aus: 1. Die Prioritätsklasse für den neuen Prozess wird in Form unabhängiger Bits im Parameter CreationFlags der CreateProcess*-Funktionen angegeben. Daher ist es möglich, bei einem CreateProcess*-Aufruf mehrere Prioritätsklassen anzugeben. Windows weist dem Prozess die Klasse mit der niedrigsten Priorität zu. Der Ablauf von CreateProcess Kapitel 3 145
Es sind sechs Prioritätsklassen definiert, denen jeweils eine Zahl zugeordnet ist: • Leerlauf oder niedrig (4) • Niedriger als normal (6) • Normal (8) • Höher als normal (10) • Hoch (13) • Echtzeit (24) Die Prioritätsklasse wird als Basispriorität für die in dem Prozess erstellten Threads verwendet. Dieser Wert wirkt sich nicht direkt auf den Thread aus, sondern nur auf die enthaltenen Threads. Eine Erläuterung der Prioritätsklassen und ihrer Auswirkung auf die Threadplanung erhalten Sie in Kapitel 4. 2. Wurde für den neuen Prozess keine Prioritätsklasse angegeben, wird Normal verwendet. Wurde die Prioritätsklasse Echtzeit angegeben und hat der Aufrufer des Prozesses nicht das Recht Anheben der Zeitplanungspriorität (SE_INC_BASE_PRIORITY), dann wird stattdessen die Klasse Hoch verwendet. Die Prozesserstellung schlägt also nicht fehl, nur weil der Aufrufer unzureichende Berechtigungen hat, um einen Prozess mit Echtzeitpriorität anzulegen. Stattdessen erhält der neue Prozess einfach eine niedrigere Priorität. 3. Verlangen die Erstellungsflags ein Debugging des Prozesses, so stellt Kernel32 eine Verbindung zum nativen Debuggingcode in Ntdll.dll her, indem er DbgUiConnectToDbg aufruft und ein Handle für das Debugobjekt vom aktuellen Threadumgebungsblock (TEB) abruft. 4. Kernel32.dll richtet den standardmäßigen harten Fehlermodus ein, wenn dies in den Erstellungsflags angegeben ist. 5. Die vom Benutzer definierte Attributliste wird vom Format des Windows-Teilsystems in das native Format umgewandelt und um interne Attribute ergänzt. Die möglichen zusätzlichen Attribute finden Sie in Tabelle 3–7 zusammen mit ihren dokumentierten Windows-API-Gegenstücken, falls vorhanden. Die an CreateProcess*-Aufrufe übergebene Attributliste ermöglicht es, an den Aufrufer Informationen zurückzugeben, die über einen einfachen Statuscode hinausgehen, z. B. die TEB-Adresse des ursprünglichen Threads oder Informationen über den Abbildabschnitt. Das ist für geschützte Prozesse notwendig, da der Elternprozess diese Information nach dem Erstellen des Kindprozesses nicht abfragen kann. Natives Attribut Entsprechendes W32Attribut Typ Beschreibung PS_CP_PARENT_PROCESS PROC_THREAD_ATTRIBUTE_ PARENT_PROCESS. Auch bei Rechteerhöhung verwendet. Eingabe Handle für den Elternprozess PS_CP_DEBUG_OBJECT Nicht verfügbar. Wird bei der Verwendung von DEBUG_ PROCESS als Flag genutzt. Eingabe Debugobjekt, wenn der Prozess im Debuggingmodus gestartet wird 146 Kapitel 3 Prozesse und Jobs →
Natives Attribut Entsprechendes W32Attribut Typ Beschreibung PS_CP_PRIMARY_TOKEN Nicht verfügbar. Wird bei der Verwendung von CreateProcessAsUser/ WithTokenW genutzt. Eingabe Prozesstoken, wenn CreateProcessAsUser verwendet wurde PS_CP_CLIENT_ID Nicht verfügbar. Wird von der Win32-API als Parameter zurückgegeben (PROCESS_ INFORMATION). Ausgabe Gibt die TID und PID des ursprünglichen Threads bzw. Prozesses zurück. PS_CP_TEB_ADDRESS Nicht verfügbar. Intern verwendet und nicht verfügbar gemacht. Ausgabe Gibt die Adresse des TEB für den ursprünglichen Thread zurück. PS_CP_FILENAME Nicht verfügbar. Wird als Parameter in CreateProcess- APIs verwendet. Eingabe Der Name des zu erstellenden Prozesses PS_CP_IMAGE_INFO Nicht verfügbar. Intern verwendet und nicht verfügbar gemacht. Ausgabe Gibt SECTION_IMAGE_ INFORMATION mit Informationen über die Version, die Flags und das Teilsystem der ausführbaren Datei sowie die Stackgröße und den Eintrittspunkt zurück. PS_CP_MEM_RESERVE Nicht verfügbar. Intern von SMSS und CSRSS verwendet. Eingabe Ein Array von Reservierungen für virtuellen Speicher, die beim ursprünglichen Erstellen des Prozessadressraums durchgeführt werden sollen. Die Verfügbarkeit wird garantiert, da noch keine anderen Zuweisungen stattgefunden haben. PS_CP_PRIORITY_CLASS Nicht verfügbar. Wird als Parameter an die CreateProcess-API übergeben. Eingabe Die Prioritätsklasse, die der Prozess haben soll PS_CP_ERROR_MODE Nicht verfügbar. Wird über das Flag CREATE_DEFAULT_ ERROR_MODE übergeben. Eingabe Verarbeitungsmodus des Prozesses für harte Fehler PS_CP_STD_HANDLE_INFO Keines. Wird intern verwendet. Eingabe Gibt an, ob Standardhandles dupliziert oder ob neue Handles erstellt werden sollen. PS_CP_HANDLE_LIST PROC_THREAD_ATTRIBUTE_ HANDLE_LIST Eingabe Die Handles des Elternprozesses, die der neue Prozess erben soll PS_CP_GROUP_AFFINITY PROC_THREAD_ATTRIBUTE_ GROUP_AFFINITY Eingabe Die Prozessorgruppen, auf denen der Thread laufen darf PS_CP_PREFERRED_NODE PROC_THREAD_ATTRIBUTES_ PREFERRED_NODE Eingabe Der bevorzugte (ideale) NUMA- Knoten, der mit dem Prozess verknüpft werden soll. Dies legt den Knoten fest, auf dem der ursprüngliche Prozessheap und der ursprüngliche Threadstack erstellt werden (siehe Kapitel 5). → Der Ablauf von CreateProcess Kapitel 3 147
Natives Attribut Entsprechendes W32Attribut Typ Beschreibung PS_CP_IDEAL_ PROCESSOR PROC_THREAD_ATTTRIBUTE_ IDEAL_PROCESSOR Eingabe Der bevorzugte (ideale) Prozessor, auf dem der Thread eingeplant werden soll PS_CP_UMS_THREAD PROC_THREAD_ATTRIBUTE_ UMS_THREAD Eingabe Enthält die UMS-Attribute, die Vervollständigungsliste und den Kontext. PS_CP_MITIGATION_ OPTIONS PROC_THREAD_MITIGATION_ POLICY Eingabe Enthält Informationen über die vorbeugenden Maßnahmen (SEHOP, ATL-Emulation, NX), die für den Prozess aktiviert oder deaktiviert werden sollen. PS_CP_PROTECTION_LEVEL PROC_THREAD_ATTRIBUTE_ PROTECTION_LEVEL Eingabe Muss auf einen der zulässigen Prozessschutzwerte aus Tabelle 3–1 oder den Wert PROTECT_LEVEL_SAME zeigen. In letzterem Fall erhält der Prozess dieselbe Schutzebene wie sein Elternprozess. PS_CP_SECURE_PROCESS Keines. Wird intern verwendet. Eingabe Gibt an, dass der Prozess als IUM-Trustlet (Isolated User Mode) ausgeführt werden soll (siehe Kapitel 8 in Band 2). PS_CP_JOB_LIST Keines. Wird intern verwendet. Eingabe Weist den Prozess einer Jobliste zu. PS_CP_CHILD_PROCESS_ POLICY PROC_THREAD_ATTRIBUTE_ CHILD_PROCESS_POLICY Eingabe Gibt an, ob der neue Prozess direkt oder indirekt Kindprozesse erstellen darf (z. B. durch WMI). PS_CP_ALL_APPLICATION_PACKAGES_POLICY PROC_THREAD_ATTRIBUTE_ ALL_APPLICATION_PACKAGES_ POLICY Eingabe Gibt an, ob das AppContainer- Token von Überprüfungen von Zugriffssteuerungslisten ausgenommen werden soll, die die Gruppe ALL APPLICATION PACKAGES enthalten. Stattdessen wird die Gruppe ALL RESTRICTED APPLICATION PACKAGES verwendet. PS_CP_WIN32K_FILTER PROC_THREAD_ATTRIBUTE_ WIN32K_FILTER Eingabe Gibt an, ob viele GDI/USER-Systemaufrufe des Prozesses an Win32k.sys gefiltert (blockiert) oder ob sie zugelassen, aber überwacht werden sollen. Wird vom Browser Microsoft Edge verwendet, um die Angriffsfläche zu verringern. PS_CP_SAFE_OPEN_ PROMPT_ORIGIN_CLAIM Keines. Wird intern verwendet. Eingabe Wird von »Mark of the Web« verwendet, um anzuzeigen, dass die Datei aus einer nicht vertrauenswürdigen Quelle stammt. 148 Kapitel 3 Prozesse und Jobs →
Natives Attribut Entsprechendes W32Attribut Typ Beschreibung PS_CP_BNO_ISOLATION PROC_THREAD_ATTRIBUTE_ BNO_ISOLATION Eingabe Sorgt dafür, dass das Primärtoken des Prozesses mit einem isolierten BaseNamedObjects-Verzeichnis verknüpft wird (siehe Kapitel 8 in Band 2). PS_CP_DESKTOP_APP_ POLICY PROC_THREAD_ATTRIBUTE_ DESKTOP_APP_POLICY Eingabe Gibt an, ob die moderne Anwendung ältere Desktopanwendungen starten darf und wenn ja, in welcher Weise. PROC_THREAD_ ATTRIBUTE_SECURITY_ CAPABILITIES Keines. Wird intern verwendet. Eingabe Gibt einen Zeiger auf eine SECURITY_CAPABILITIES- Struktur an, die dazu dient, ein AppContainer-Token für den Prozess zu erstellen, bevor NtCreateUserProcess aufgerufen wird. Tabelle 3–7 Prozessattribute 6. Fordert das Erstellungsflag eine getrennte virtuelle DOS-Maschine (VDM) an, obwohl der Prozess Teil eines Jobobjekts ist, so wird das Flag ignoriert. 7. Die an die CreateProcess-Funktion übergebenen Sicherheitsattribute für den Prozess und den ursprünglichen Thread werden in ihre interne Darstellung umgewandelt (OBJECT_ ATTRIBUTES-Strukturen; dokumentiert im WDK). 8. CreateProcessInternalW prüft, ob der Prozess als moderner Prozess erstellt werden soll. Das ist der Fall, wenn es in dem Attribut PROC_THREAD_ATTRIBUTE_PACKAGE_FULL_NAME mit dem vollständigen Paketnamen so verlangt wird oder wenn der Ersteller selbst ein moderner Prozess ist (und mit dem Attribut PROC_THREAD_ATTRIBUTE_PARENT_PROCESS nicht ausdrücklich ein Elternprozess angegeben wurde). In diesem Fall wird die interne Funktion BasepAppXExtension aufgerufen, um mehr Kontextinformationen über die Parameter der modernen App zu ermitteln. Sie befinden sich in der Struktur APPX_PROCESS_CONTEXT, die Informationen wie den Paketnamen (intern als »package moniker« bezeichnet), die Fähigkeiten der Anwendung, das aktuelle Verzeichnis für den Prozess und die Angabe enthält, ob der App volles Vertrauen geschenkt werden soll. Die Möglichkeit, eine voll vertrauenswürdige moderne App zu erstellen, ist nicht öffentlich verfügbar, sondern für Anwendungen reserviert, die zwar über ein modernes Erscheinungsbild verfügen, aber Operationen auf Systemebene durchführen. Ein wichtiges Beispiel ist die App Einstellungen in Windows 10 (SystemSettings.exe). 9. Wird ein moderner Prozess erstellt, so wird die interne Funktion BasepCreateLowBox aufgerufen, um die Sicherheitsfähigkeiten (sofern in PROC_THREAD_ATTRIBUTE_SECURITY_ CAPABILITIES bereitgestellt) für die Erstellung des ursprünglichen Tokens aufzuzeichnen. Die Bezeichnung LowBox bezieht sich auf die Sandbox (den Anwendungscontainer), in der der Prozess ausgeführt wird. Es ist zwar nicht möglich, moderne Prozesse direkt durch einen Aufruf von CreateProcess zu erstellen (stattdessen müssen die zuvor beschriebenen COM-Schnittstellen verwendet werden), doch im Windows SDK und auf MSDN wird die Der Ablauf von CreateProcess Kapitel 3 149
Möglichkeit beschrieben, ältere AppContainer-Desktopanwendungen durch die Übergabe dieses Attributs zu erstellen. 10. Soll ein moderner Prozess erstellt werden, so wird ein Flag gesetzt, um anzuzeigen, dass der Kernel die Erkennung des eingebetteten Manifests überspringen soll. Moderne Prozesse sollten kein eingebettetes Manifest haben, da es nicht benötigt wird. (Eine moderne App hat ihr eigenes Manifest, das aber nichts mit dem hier gemeinten eingebetteten Manifest zu tun hat.) 11. Wurde das Debugflag DEBUG_PROCESS angegeben, wird der Debugger-Wert im Registrierungsschlüssel Image File Execution Options für die ausführbare Datei (siehe den nächsten Abschnitt) so markiert, dass er übersprungen wird. Anderenfalls könnte ein Debugger niemals den zu debuggenden Prozess erstellen, da der Vorgang in eine Endlosschleife eintritt (es wird immer und immer wieder versucht, den Debuggerprozess anzulegen). 12. Alle Fenster werden mit Desktops verknüpft, den grafischen Darstellungen eines Arbeitsbereichs. Ist in der STARTUPINFO-Struktur kein Desktop angegeben, so wird der Prozess mit dem aktuellen Desktop des Aufrufers verknüpft. Die Windows 10-Funktion der virtuellen Desktops verwendet in Wirklichkeit nicht mehrere Desktopobjekte (im Sinne von Kernelobjekten). Es gibt immer noch lediglich einen Desktop, auf dem aber die Fenster nach Bedarf angezeigt bzw. ausgeblendet werden. Anders ist es beim Sysinternals-Tools Desktops.exe, das wirklich bis zu vier Desktopobjekte erstellt. Den Unterschied können Sie erkennen, wenn Sie versuchen, ein Fenster von einem Desktop zu einem anderen zu verschieben. Bei Desktops.exe geht das nicht, da eine solche Operation in Windows nicht unterstützt wird. In den virtuellen Desktops von Windows 10 ist es jedoch möglich, da dabei in Wirklichkeit gar nichts verschoben wird. 13. Die an CreateProcessInternalW übergebene Anwendung und die Befehlszeilenargumente werden analysiert. Der Pfad der ausführbaren Datei wird in den internen NT-Namen umgewandelt (beispielsweise wird aus c:\temp\a.exe etwas wie \device\harddiskvolume1\ temp\a.exe), da einige Funktionen auf dieses Format angewiesen sind. 14. Die meisten erfassten Informationen werden in eine einzige große Struktur vom Typ RTL_ USER_PROCESS_PARAMETERS umgewandelt. Nach Abschluss dieser Schritte ruft CreateProcessInternalW die Funktion NtCreateUserProcess auf, um zu versuchen, den Prozess zu erstellen. Da Kernel32.dll zu diesem Zeitpunkt noch nicht weiß, ob der Name des Abbilds zu einer echten Windows-Anwendung oder zu einer Batchdatei (.bat oder .cmd), einer 16-Bit- oder DOS-Anwendung gehört, kann dieser Aufruf fehlschlagen. In diesem Fall schaut sich CreateProcessInternalW den Grund für den Fehler an und versucht, ihn zu korrigieren. 150 Kapitel 3 Prozesse und Jobs
Phase 2: Das auszuführende Abbild öffnen An dieser Stelle ist der erstellende Thread in den Kernelmodus gewechselt und fährt mit seiner Arbeit in der Implementierung des Systemaufrufs NtCreateUserProcess fort. 1. Als Erstes validiert NtCreateUserProcess die Argumente und baut eine interne Struktur für alle Informationen zu dem Erstellungsvorgang auf. Validiert werden die Argumente, um sicherzugehen, dass der Aufruf an die Exekutive nicht von einem Hack ausging, der den Übergang von Ntdll.dll zum Kernel mit gefälschten oder schädlichen Argumenten simuliert. 2. Wie Abb. 3–11 zeigt, besteht der nächste Schritt darin, das geeignete Windows-Abbild zu finden, um die vom Aufrufer angegebene ausführbare Datei auszuführen und ein Abschnittsobjekt zu erstellen, das später im Adressraum des neuen Prozesses abgebildet wird. Wenn dieser Aufruf aus irgendeinem Grund fehlschlägt, gibt er einen Fehlerstatus an CreateProcessInternalW zurück (siehe Tabelle 3–8). Mithilfe dieser Informationen versucht CreateProcessInternalW es dann noch einmal. Führt Cmd.exe aus Führt NtVdm.exe aus BAT- oder CMDDatei von DOS Win16 Führt die EXE-Datei direkt aus Windows Um was für eine Anwendung handelt es sich? EXE-, COM- oder PIF-Datei von DOS Führt NtVdm.exe aus Abbildung 3–11 Auswahl des zu verwendenden Windows-Abbilds 3. Soll ein geschützter Prozess erstellt werden, so wird auch die Signierrichtlinie geprüft. 4. Soll ein moderner Prozess erstellt werden, so erfolgt eine Lizenzierungsprüfung, um sicherzugehen, dass der Prozess lizenziert ist und ausgeführt werden darf. Anwendungen, die zusammen mit Windows vorinstalliert wurden, dürfen unabhängig von der Lizenz immer ausgeführt werden. Ist das Querladen zugelassen (was über Einstellungen festgelegt wird), dann können alle signierten Anwendungen ausgeführt werden, nicht nur diejenigen aus dem Store. 5. Handelt es sich bei dem Prozess um ein Trustlet, so muss das Abschnittsobjekt mit einem besonderen Flag erstellt werden, das die Verwendung durch den sicheren Kernel erlaubt. 6. Wenn es sich bei der angegebenen ausführbaren Datei um eine Windows-EXE handelt, versucht NtCreateUserProcess, sie zu öffnen und ein Abschnittsobjekt dafür zu erstellen. Der Ablauf von CreateProcess Kapitel 3 151
Dieses Objekt wird noch nicht auf den Arbeitsspeicher abgebildet, aber es ist geöffnet. Die Tatsache, dass ein Abschnittsobjekt erstellt werden konnte, bedeutet jedoch nicht zwangsläufig, dass es sich bei der Datei um ein gültiges Windows-Abbild handelt, denn es könnte auch eine DLL oder eine ausführbare POSIX-Datei sein. In letzterem Fall schlägt der Aufruf fehl, da POSIX nicht mehr unterstützt wird. Bei einer DLL schlägt CreateProcessInternalW ebenfalls fehl. 7. Nachdem NtCreateUserProcess ein gültiges Windows-Abbild gefunden hat, schaut sie in der Registrierung unter HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options nach, ob es einen Unterschlüssel mit dem Dateinamen und der Erweiterung des Abbilds gibt (allerdings ohne Verzeichnis- und Pfadangaben, also z. B. Notepad. exe). Wenn ja, sucht PspAllocateProcess nach dem Debugger-Wert für diesen Schlüssel. Ist er vorhanden, wird der Name des auszuführenden Abbilds zum String für diesen Wert. CreateProcessInternalW startet dann erneut in Phase 1. Sie können dieses Verhalten nutzen, um den Startcode von Windows-Dienstprozessen zu debuggen, bevor sie gestartet werden. Wenn Sie den Debugger erst nach dem Start einklinken, ist ein Debugging des Startcodes nicht mehr möglich. 8. Handelt es sich bei dem Abbild dagegen nicht um eine Windows-EXE (sondern z. B. um eine MS-DOS- oder eine Win16-Anwendung), durchläuft CreateProcessInternalW eine Reihe von Schritten, um ein Windows-Unterstützungsabbild zur Ausführung dieser Datei zu finden. Dieser Vorgang ist notwendig, da Nicht-Windows-Anwendungen nicht direkt ausgeführt werden. Stattdessen verwendet Windows dazu besondere Unterstützungsabbilder, die letzten Endes für die Ausführung des Nicht-Windows-Programms verantwortlich sind. Wenn Sie beispielsweise (auf 32-Bit-Windows) versuchen, eine ausführbare MS-DOS- oder Win16-Datei zu öffnen, wird das Abbild Ntvdm.exe ausgeführt. Kurz: Es ist nicht möglich, einen Nicht-Windows-Prozess direkt zu erstellen. Wenn Windows keine Möglichkeit findet, das angegebene Abbild auszuführen, schlägt CreateProcessInternalW fehlt (siehe Tabelle 3–8). Abbild Statuscode Ausgeführtes Abbild Ergebnis MS-DOS-Anwendung mit der Erweiterung .exe, .com oder .pif PsCreateFailOnSectionCreate Ntvdm.exe CreateProcessInternalW startet neu in Phase 1. Win16-Anwendung PsCreateFailOnSectionCreate Ntvdm.exe CreateProcessInternalW startet neu in Phase 1. Win64-Anwendung auf einem 32-Bit-System (oder eine PPC-, MIPS- oder Alpha-Binärdatei) PsCreateFailMachineMismatch – CreateProcessInternalW schlägt fehl. 152 Kapitel 3 → Prozesse und Jobs
Abbild Statuscode Ausgeführtes Abbild Ergebnis Hat einen Debugger-Wert mit einem anderen Abbildnamen PsCreateFailExeName Im Debugger-Wert angegebener Name CreateProcessInternalW startet neu in Phase 1. Ungültige oder beschädigte Windows-EXE PsCreateFailExeFormat – CreateProcessInternalW schlägt fehl. Kann nicht geöffnet werden PsCreateFailOnFileOpen – CreateProcessInternalW schlägt fehl. Befehlsprozedur (Anwendung mit der Erweiterung .bat oder .com) PsCreateFailOnSectionCreate Cmd.exe CreateProcessInternalW startet neu in Phase 1. Tabelle 3–8 Umgang mit verschiedenen Abbildern CreateProcessInternalW entscheidet wie folgt, welches Abbild sie ausführen soll: QQ Wenn x86-32-Bit-Windows ausgeführt wird und es sich bei dem Abbild um eine MS-DOS-Anwendung mit der Erweiterung .exe, .com oder .pif handelt, wird eine Nachricht an das Windows-Teilsystem gesendet, um zu prüfen, ob für die Sitzung bereits ein MS-DOS-Unterstützungsprozess erstellt worden ist (nämlich Ntvdm.exe, wie im Registrierungswert HKLM\SYSTEM\CurrentControlSet\Control\WOW\cmdline angegeben). Wenn ja, wird dieser Unterstützungsprozess verwendet, um die MS-DOS-Anwendung auszuführen. (Das Windows-Teilsystem sendet die Nachricht an den VDM-Prozess [virtuelle DOS-Maschine], um das neue Abbild auszuführen.) Danach gibt CreateProcessInternalW die Steuerung zurück. Wurde dagegen noch kein Unterstützungsprozess erstellt, wird als auszuführendes Image Ntvdm.exe angegeben, woraufhin CreateProcessInternalW mit Phase 1 neu beginnt. QQ Trägt die auszuführende Datei die Endung .bat oder .cmd, wird die Windows-Eingabeaufforderung Cmd.com zum auszuführenden Abbild, woraufhin CreateProcessInternalW mit Phase 1 neu beginnt. Der Name der Batchdatei wird als zweiter Parameter nach dem Schalter /c an Cmd.exe übergeben. QQ Findet der Vorgang auf einem x86-Windows-System statt und handelt es sich bei dem Abbild um eine ausführbare Win16-Datei (Windows 3.1), muss CreateProcessInternalW entscheiden, ob ein neuer VDM-Prozess dafür erstellt oder der standardmäßige sitzungsweit gemeinsam genutzte VDM-Prozess verwendet werden soll (der möglicherweise noch nicht erstellt wurde). Für diese Entscheidung werden die CreateProcess-Flags CREATE_SEPARATE_ WOW_VDM und CREATE_SHARED_WOW_VDM herangezogen. Wenn sie nicht angegeben sind, gibt der Registrierungswert HKLM\SYSTEM\CurrentControlSet\Control\WOW\DefaultSeparateVDM das Standardverhalten vor. Soll die Anwendung in einer eigenen VDM ausgeführt werden, wird das auszuführende Abbild in Ntvdm.exe gefolgt von einigen Konfigurationsparametern und dem Namen des 16-Bit-Prozesses geändert, woraufhin CreateProcessInternalW in Phase 1 neu startet. Anderenfalls sendet das Windows-Teilsystem eine Nachricht, um zu prüfen, ob der gemeinsam genutzte VDM-Prozess vorhanden ist und verwendet werden Der Ablauf von CreateProcess Kapitel 3 153
kann. Läuft der VDM-Prozess auf einem anderen Desktop oder in einem anderen Sicherheitskontext als dem des Aufrufers, dann kann er nicht genutzt werden. In diesem Fall muss ein neuer VDM-Prozess erstellt werden. Kann der gemeinsame VDM-Prozess dagegen verwendet werden, dann sendet das Windows-Teilsystem ihm eine Nachricht, damit er das neue Abbild ausführt. CreateProcessInternalW gibt dann die Steuerung zurück. Wenn der VDM-Prozess noch nicht gestartet wurde (oder nicht genutzt werden kann), wird das auszuführende Abbild in das VDM-Unterstützungsabbild geändert, woraufhin CreateProcessInternalW in Phase 1 neu startet. Phase 3: Das Windows-Exekutivprozessobjekt erstellen NtCreateUserProcess hat jetzt eine gültige ausführbare Windows-Datei geöffnet und ein Ab- schnittsobjekt erstellt, um sie auf den Adressraum des neuen Prozesses abzubilden. Als Nächstes erstellt die Funktion ein Windows-Exekutivprozessobjekt, um das Abbild auszuführen. Dazu ruft sie die interne Systemfunktion PspAllocateProcess auf. Die Erstellung des Exekutivprozessobjekts (die vom Erstellerthread erledigt wird) umfasst die folgenden Teilphasen: a. Das EPROCESS-Objekt einrichten b. Den ursprünglichen Prozessadressraum erstellen c. Die KPROCESS-Struktur initialisieren d. Die Einrichtung des Prozessadressraums abschließen e. Den PEB einrichten f. Die Einrichtung des Exekutivprozessobjekts abschließen Nur während des Systeminitialisierens (beim Erstellen des Systemprozesses) gibt es keinen Elternprozess. Danach ist immer ein Elternprozess erforderlich, um den Sicherheitskontext für den neuen Prozess bereitzustellen. Phase 3a: Das EPROCESS-Objekt einrichten Diese Teilphase umfasst die folgenden Schritte: 1. Der neue Prozess erbt die Affinität des Elternprozesses, es sei denn, während der Prozess erstellung wurde in der Attributliste ausdrücklich eine Affinität angegeben. 2. Gegebenenfalls wird der NUMA-Knoten ausgewählt, der als idealer Knoten in der Attributliste angegeben wurde. 3. Der neue Prozess erbt die E/A- und Seitenpriorität vom Elternprozess. Gibt es keinen Elternprozess, werden die Standardprioritäten verwendet, also 5 für die Seiten- und Normal für die E/A-Priorität. 4. Der neue Prozessbeendigungsstatus wird auf STATUS_PENDING gesetzt. 5. Es wird der Verarbeitungsmodus für harte Fehler ausgewählt, der in der Attributliste angegeben ist. Wird dort keiner genannt, so erbt der Prozess den Modus des Elternprozesses. 154 Kapitel 3 Prozesse und Jobs
Gibt es keinen Elternprozess, wird der Standardverarbeitungsmodus genommen, bei dem alle Fehler angezeigt werden. 6. Die ID des Elternprozesses wird im Feld InheritedFromUniqueProcessID des neuen Prozessobjekts gespeichert. 7. Der Schlüssel Image File Execution Objects (IFEO) wird abgefragt, um herauszufinden, ob der Prozess auf großen Seiten abgebildet werden soll (Wert UseLargePages). Wenn der Prozess unter Wow64 läuft, werden jedoch grundsätzlich keine großen Seiten verwendet. Es wird auch geprüft, ob in diesem Schlüssel NTDLL als die DLL angegeben ist, die in dem Prozess auf großen Seiten abgebildet werden soll. 8. Der Leistungsoptionsschlüssel PerfOptions in IFEO wird abgefragt (falls vorhanden). Er kann mehrere der folgenden Werte enthalten: IoPriority, PagePriority, CpuPriorityClass und WorkingSetLimitInKB. 9. Soll der Prozess unter Wow64 laufen, dann wird die Wow64-Hilfsstruktur EWOW64PROCESS zugewiesen und im Element WoW64Process der EPROCESS-Struktur angegeben. 10. Soll der Prozess in einem Anwendungscontainer laufen (was bei einer modernen App meistens der Fall ist), wird das mit LowBox erstellte Token validiert (siehe Kapitel 7). 11. Es wird versucht, alle erforderlichen Rechte zum Erstellen des Prozesses zu erwerben. Die Auswahl der Prioritätsklasse Echtzeit, die Zuweisung eines Tokens zu dem neuen Prozess, die Abbildung des Prozesses auf großen Seiten und das Erstellen des Prozesses in einer neuen Sitzung erfordern jeweils entsprechende Rechte. 12. Das primäre Zugriffstoken des Prozesses (ein Duplikat des Primärtokens des Elternprozesses) wird erstellt. Neue Prozesse erben das Sicherheitsprofil ihrer Elternprozesse. Wird mit CreateProcessAsUser ein anderes Zugriffstoken für den neuen Prozess angegeben, so wird das Token entsprechend geändert. Das kann jedoch nur dann geschehen, wenn das Token des übergeordneten Prozesses eine höhere Integritätsebene aufweist als das Zugriffstoken des neuen Prozesses und wenn dieses Zugriffstoken ein echtes Kind oder Geschwister des Elterntokens ist. Hat der Elternprozess das Recht SeAssignPrimaryToken, wird diese Überprüfung übersprungen. 13. Die Sitzungs-ID des neuen Prozesstokens wird geprüft, um zu bestimmen, ob der neue Prozess sitzungsübergreifend erstellt wird. Wenn ja, so wird der Elternprozess vorübergehend an die Zielsitzung angehängt, um die Kontingente und die Adressraumerstellung korrekt zu verarbeiten. 14. Der Kontingentblock des neuen Prozesses wird auf die Adresse des Elternkontingentblocks gesetzt. Der Referenzzähler für den Elternkontingentblock wird heraufgesetzt. Wurde der Prozess mit CreateProcessAsUser erstellt, wird dieser Schritt nicht ausgeführt. Stattdessen wird das Standardkontingent erstellt oder das Kontingent aus dem Benutzerprofil ausgewählt. 15. Die Mindest- und Höchstgröße des Arbeitssatzes für den Prozess werden auf die Werte von PspMinimumWorkingSet bzw. PspMaximumWorkingSet gesetzt. Diese Werte können überschrieben werden, wenn im IFEO-Schlüssel PerfOptions entsprechende Leistungsoptionen angegeben wurden. In diesem Fall wird die maximale Arbeitssatzgröße aus diesem Schlüs- Der Ablauf von CreateProcess Kapitel 3 155
sel bezogen. Die Standardgrenzwerte für den Arbeitssatz sind im Grunde genommen nur Empfehlungen, während es sich bei dem Maximalwert in PerfOptions um eine feste Grenze handelt, die der Arbeitssatz nicht überschreiten kann. 16. Der Adressraum des Prozesses wird initialisiert (siehe Phase 3b). Wird der neue Prozess in einer anderen Sitzung erstellt, so wird der Elternprozess jetzt von der Zielsitzung getrennt. 17. Wurde die Gruppenaffinität für den Prozess nicht geerbt, so wird sie jetzt ausgewählt. Die Standardgruppenaffinität wird entweder vom Elternprozess geerbt, wenn zuvor die NUMA-Knotenweiterleitung eingerichtet wurde (dabei wird die Gruppe verwendet, die den NUMA-Knoten besitzt), oder mit einem Umlaufverfahren zugewiesen. Befindet sich das System in einem erzwungenen Gruppenkenntnismodus und hat sich der Auswahlalgorithmus für Gruppe 0 entschieden, so wird stattdessen Gruppe 1 ausgewählt, falls sie vorhanden ist. 18. Der KPROCESS-Teil des Prozessobjekts wird initialisiert (siehe Phase 3c). 19. Das Token für den Prozess wird jetzt eingerichtet. 20. Die Prioritätsklasse des Prozesses wird auf Normal gesetzt. Hatte der Elternprozess die Klasse Leerlauf oder Niedriger als normal, so wird stattdessen diese Priorität verwendet. 21. Die Prozesshandletabelle wird initialisiert. Ist das Flag zur Handlevererbung für den Elternprozess gesetzt, werden alle vererbbaren Handles von dessen Objekthandletabelle in die des neuen Prozesses kopiert (mehr darüber siehe Kapitel 8 in Band 2). Mit einem Prozessattribut kann jedoch auch nur eine Teilmenge der Handles angegeben werden. Das ist praktisch, wenn Sie mit CreateProcessAsUser einschränken wollen, welche Objekte der Kindprozess erben soll. 22. Wurden im Schlüssel PerfOptions Leistungsoptionen angegeben, so werden diese jetzt angewendet. Dieser Schlüssel enthält Werte, mit denen die Grenzwerte für den Arbeitssatz, die E/A-, die Seiten- und die CPU-Priorität des Prozesses überschrieben werden können. 23. Die endgültige Prioritätsklasse für den Prozess und das Standardquantum für seine Threads werden berechnet und festgelegt. 24. Die verschiedenen Vorbeugungsoptionen im IFEO-Schlüssel (ein einzelner 64-Bit-Wert namens Mitigation) werden gelesen und festgelegt. Läuft der Prozess in einem Anwendungscontainer, wird das Abwehrflag TreatAsAppContainer hinzugefügt. 25. Alle anderen Vorbeugungsflags werden angewendet. Phase 3b: Den ursprünglichen Prozessadressraum erstellen Der ursprüngliche Prozessadressraum besteht aus den folgenden Seiten: QQ Seitenverzeichnis (auf Systemen mit Seitentabellen, die mehr als zwei Ebenen umfassen, kann es mehr als ein Verzeichnis geben, beispielsweise auf x86-Systemen im PAE-Modus und auf 64-Bit-Systemen) QQ Hyperspace-Seite QQ VAD-Bitmap-Seite QQ Arbeitssatzliste 156 Kapitel 3 Prozesse und Jobs
Um diese Seiten zu erstellen, werden die folgenden Schritte ausgeführt: 1. 2. In den entsprechenden Seitentabellen werden Einträge vorgenommen, um die ursprünglichen Seiten abzubilden. Die Anzahl der Seiten wird aus der Kernelvariablen MmTotalCommittedPages ermittelt und zu MmProcessCommit addiert. 3. Die systemweite Mindestgröße für Arbeitssätze von Standardprozessen (PsMinimumWorkingSet) wird aus MmResidentAvailablePages ermittelt. 4. Die Seitentabellenseiten für den globalen Systemraum (also alle außer den gerade beschriebenen prozessspezifischen Seiten und dem sitzungsspezifischen Arbeitsspeicher) werden erstellt. Phase 3c: Die KPROCESS-Struktur initialisieren Die nächste Phase von PspAllocateProcess besteht darin, die KPROCESS-Struktur zu initialisieren (das Element Pcb in EPROCESS). Diese Aufgabe erledigt KeInitializeProcess wie folgt: 1. Die doppelt verlinkte Liste, die alle zu dem Prozess gehörenden Threads verbindet und ursprünglich leer ist, wird initialisiert. 2. Der Anfangswert (oder Rücksetzwert) des Standardquantums für den Prozess (siehe den Abschnitt »Threadplanung« in Kapitel 4) wird als 6 hartcodiert, bis er später von PspComputeQuantumAndPriority initialisiert wird. Das ursprüngliche Standardquantum ist in den Client- und den Serverversionen von Windows unterschiedlich. Weitere Angaben dazu erhalten Sie im Abschnitt »Threadplanung« in Kapitel 4. 3. Die Basispriorität des Prozesses wird auf den in Phase 3a berechneten Wert gesetzt. 4. Die Standardprozessoraffinität für die Threads in dem Prozess wird festgelegt. Die Gruppenaffinität wird auf den in Phase 3a berechneten oder auf den vom Elternprozess geerbten Wert gesetzt. 5. Der Swapping-Status für den Prozess wird auf resident gesetzt. 6. Der Thread-Seed wird anhand des idealen Prozessors bestimmt, den der Kernel für den Prozess ausgewählt hat (und der wiederum auf dem idealen Prozessor des zuvor erstellten Prozesses basiert, was letzten Endes zu einer Randomisierung in der Art eines Umlaufverfahrens führt). Beim Erstellen eines neuen Prozesses wird der Seed in KeNodeBlock (dem ursprünglichen NUMA-Knotenblock) aktualisiert, sodass der nächste neue Prozessor einen anderen idealen Prozessor für den Seed erhält. 7. Bei einem sicheren Prozess (Windows 10 und Windows Server 2016) wird jetzt die sichere ID durch einen Aufruf von HvlCreateSecureProcess erstellt. Der Ablauf von CreateProcess Kapitel 3 157
Phase 3d: Die Einrichtung des Prozessadressraums abschließen Die Einrichtung des Adressraums für einen neuen Prozess ist recht kompliziert, weshalb wir uns diesen Vorgang Schritt für Schritt ansehen. Für das Verständnis dieses Abschnitts sind Kenntnisse des in Kapitel 5 beschriebenen Speicher-Managers von Nutzen. Den Großteil der Arbeit zum Einrichten des Adressraums erledigt die Routine MmInitializeProcessAddressSpace. Sie ermöglicht es auch, den Adressraum eines anderen Prozesses zu klonen. Diese Möglichkeit war früher praktisch, um den POSIX-Systemaufruf fork zu implementieren, und kann sich in Zukunft vielleicht auch wieder als nützlich erweisen, um andere fork-Aufrufe nach Art von Unix zu unterstützen. (Auf diese Weise wird fork im Windows-Teilsystem für Linux in Redstone 1 implementiert.) In den folgenden Schritten wird dieser Klonvorgang jedoch nicht beschrieben, sondern die normale Initialisierung des Prozessadressraums: 1. Der Manager für den virtuellen Speicher setzt den Wert für den letzten Kürzungszeitpunkt des Prozesses auf die aktuelle Zeit. Der Arbeitssatz-Manager (der im Kontext des Systemthreads für den Ausgleichssatz-Manager läuft) bestimmt anhand dieses Wertes, wann er die Kürzung des Arbeitssatzes auslösen soll. 2. Der Speicher-Manager initialisiert die Arbeitssatzliste des Prozesses. Jetzt können Seitenfehler entgegengenommen werden. 3. Der Abschnitt (der beim Öffnen der Abbilddatei erstellt wurde) wird jetzt im Adressraum des neuen Prozesses abgebildet. Die Basisadresse des Prozessabschnitts wird auf die Basis adresse des Abbilds gesetzt. 4. Der Prozessumgebungsblock (PEB) wird erstellt und initialisiert (siehe Phase 3e). 5. Ntdll.dll wird dem Prozess zugeordnet, und bei einem Wow64-Prozess auch die 32-Bit-Version von Ntdll.dll. 6. Falls verlangt, wird eine neue Sitzung für den Prozess erstellt. Dieser Schritt ist hauptsächlich für den Sitzungs-Manager (Smss) bei der Initialisierung einer neuen Sitzung da. 7. Die Standardhandles werden dupliziert und die neuen Werte werden in die Prozessparameterstruktur geschrieben. 8. Alle in der Attributliste aufgeführten Speicherreservierungen werden jetzt verarbeitet. Zwei Flags erlauben auch eine Massenreservierung der ersten 1 oder 16 MB des Adressraums. Diese Flags werden intern verwendet, um beispielsweise Realmodusvektoren und ROMCode zuzuordnen (die sich im unteren Bereich des virtuellen Adressraums befinden müssen, wo normalerweise der Heap oder andere Prozessstrukturen angeordnet werden). 9. Die Benutzerprozessparameter werden in den Prozess geschrieben, kopiert und von der absoluten in eine relative Form umgewandelt, sodass nur ein einziger Speicherblock dafür erforderlich ist. 10. Die Affinitätsinformationen werden in den PEB geschrieben. 11. Der API-Umleitungssatz MinWin wird in dem Prozess abgebildet. Sein Zeiger wird im PEB gespeichert. 12. Die eindeutige Prozess-ID wird ermittelt und gespeichert. Der Kernel unterscheidet nicht zwischen eindeutigen Prozess- und Thread-IDs und Handles. Prozess- und Thread-IDs 158 Kapitel 3 Prozesse und Jobs
(Handles) werden in der globalen Handletabelle PspCidTable gespeichert, die nicht mit einem Prozess verknüpft ist. 13. Wenn es sich um einen sicheren Prozess handelt (wenn er also im IUM läuft), so wird er initialisiert und mit dem Kernelprozessobjekt verknüpft. Phase 3e: Den PEB einrichten NtCreateUserProcess ruft MmCreatePeb auf, die als Erstes die systemweiten Sprachunterstüt- zungstabellen (National Language Support, NLS) in den Prozessadressraum abbildet. Anschließend ruft sie MiCreatePeb=rTeb auf, um eine Seite für den PEB zuzuweisen, und initialisiert dann eine Reihe von Feldern, größtenteils auf der Grundlage von internen Variablen, die über die Registrierung eingestellt wurden, z. B. MmHeap*-Werte, MmCriticalSectionTimeout und MmMinimumStackCommitInBytes. Einige dieser Felder können durch Einstellungen im verlinkten ausführbaren Image überschrieben werden, z. B. die Windows-Version im PE-Header und die Affinitätsmaske in dessen Ladekonfigurationsverzeichnis. Ist das Abbildheader-Flag IMAGE_FILE_UP_SYSTEM_ONLY gesetzt (was bedeutet, dass das Abbild nur auf einem Einzelprozessorsystem ausgeführt werden kann), so wird für alle Threads in diesem Prozess eine einzige CPU ausgewählt (MmRotatingUniprocessorNumber). Die Auswahl erfolgt einfach durch Durchlaufen aller verfügbaren Prozessoren. Jedes Mal, wenn ein Abbild dieser Art ausgeführt wird, kommt der nächste Prozessor an die Reihe. Dadurch werden solche Abbilder gleichmäßig auf die Prozessoren verteilt. Phase 3f: Die Einrichtung des Exekutivprozessobjekts abschließen Bevor das Handle für den neuen Prozess zurückgegeben werden kann, müssen noch einige letzte Einrichtungsschritte abgeschlossen werden. Ausgeführt werden sie von PspInsertProcess und ihren Hilfsfunktionen: 1. Ist die systemweite Prozessüberwachung aktiviert (aufgrund einer lokalen Richtlinieneinstellung oder einer Gruppenrichtlinieneinstellung auf einem Domänencontroller), so wird das Erstellen des Prozesses im Sicherheitsprotokoll vermerkt. 2. War der Elternprozess in einem Job enthalten, so wird dieser Job aus dem Jobsatz des Elternprozesses wiederhergestellt und an die Sitzung des neuen Prozesses gebunden. Schließlich wird der neue Prozess zu dem Job hinzugefügt. 3. Das neue Prozessobjekt wird am Ende der Windows-Liste aktiver Prozesse eingefügt (PsActiveProcessHead). Jetzt ist der Prozess für Funktionen wie EnumProcess und OpenProcess zugänglich. 4. Der Prozessdebugport des Elternprozesses wird in den neuen Kindprozess kopiert, es sei denn, das Flag NoDebugInherit ist gesetzt (das beim Erstellen des Prozesses angefordert werden kann). Wurde ein Debugport angegeben, so wird dieser mit dem neuen Prozess verknüpft. 5. Jobobjekte können die möglichen Gruppen beschränken, in denen die Threads in den Prozessen des Jobs ausgeführt werden können. Daher vergewissert sich PspInsertProcess, dass die Gruppenaffinität des Prozesses nicht die Gruppenaffinität des Jobs verletzt. Ein Der Ablauf von CreateProcess Kapitel 3 159
weiteres interessantes Problem liegt vor, wenn die Berechtigungen des Jobs es erlauben, die Affinitätsberechtigungen des Prozesses zu ändern, da ein Jobobjekt mit geringeren Rechten dann die Affinitätsanforderungen eines höher privilegierten Prozesses manipulieren könnte. 6. Schließlich ruft PspInsertProcess die Funktion ObOpenObjectByPointer auf, um ein Handle für den neuen Prozess zu erstellen, und gibt dieses Handle dann an den Aufrufer zurück. Es wird erst dann ein Prozesserstellungscallback gesendet, wenn der erste Thread in dem Prozess angelegt wurde. Der Code sendet Prozesscallbacks immer vor Callbacks zur Objektverwaltung. Phase 4: Den ursprünglichen Thread mit seinem Stack und Kontext erstellen Das Windows-Exekutivprozessobjekt ist jetzt fast vollständig eingerichtet. Es hat jedoch noch keinen Thread und kann daher nichts tun. Für alle Aspekte der Threaderstellung ist normalerweise die Routine PspCreateThread zuständig, die von NtCreateThread aufgerufen wird, um einen neuen Thread anzulegen. Da der ursprüngliche Thread jedoch intern vom Kernel ohne Eingaben vom Benutzermodus erstellt wird, kommen stattdessen die beiden Hilfsroutinen PspAllocateThread und PspInsertThread zum Einsatz, auf die sich auch PspCreateThread stützt. Dabei kümmert sich PspAllocateThread darum, das Exekutivthreadobjekt zu erstellen und zu initialisieren, während PspInsertHandle den Threadhandle und die Sicherheitsattribute anlegt und KeStartThread aufruft, um aus dem Exekutivobjekt einen planbaren Thread zu machen. Allerdings tut der Thread danach immer noch nichts, da er in angehaltenem Zustand erstellt wird. Er wird erst wieder aufgenommen, wenn der Prozess vollständig initialisiert ist (siehe Phase 5). Der Threadparameter (der nicht in CreateProcess, sondern in CreateThread gesetzt werden kann) gibt die Adresse des PEB an. Verwendet wird dieser Parameter von dem Initialisierungscode, der im Kontext des neuen Threads ausgeführt wird (siehe Phase 6). PspAllocateThread führt die folgenden Schritte aus: 1. Sie verhindert, dass in Wow64-Prozessen UMS-Threads (User-Mode Scheduling) erstellt werden und dass Aufrufer aus dem Benutzermodus Threads in Systemprozessen anlegen. 2. Sie erstellt und initialisiert ein Exekutivthreadobjekt. 3. Sind für das System Energieabschätzungen aktiviert (auf der Xbox sind sie immer deaktiviert), weist sie die THREAD_ENERGY_VALUES-Struktur zu, auf die das ETHREAD-Objekt zeigt, und initialisiert sie. 4. Sie initialisiert die Listen, die von LPC, der E/A-Verwaltung und der Exekutive verwendet werden. 5. Sie legt die Threaderstellungszeit fest und richtet die Thread-ID ein. 160 Kapitel 3 Prozesse und Jobs
6. Bevor der Thread ausgeführt werden kann, braucht er einen Stack und einen Ausführungskontext, die jetzt von der Routine eingerichtet werden. Die Stackgröße für den ursprünglichen Thread wird aus dem Abbild bezogen. Es gibt keine Möglichkeit, eine andere Größe anzugeben. Bei einem Wow64-Prozess wird auch der Wow64-Threadkontext initialisiert. 7. Sie weist den Threadumgebungsblock (TEB) für den neuen Thread zu. 8. Die Benutzermodus-Startadresse für den Thread wird im Feld StartAddress der ETHREAD-Struktur gespeichert. Dies ist die vom System bereitgestellte Threadstartfunktion RtlUserThreadStart aus Ntdll.dll. Die vom Benutzer angegebene Windows-Startadresse wird ebenfalls in der ETHREAD-Struktur gespeichert, aber an anderer Stelle, nämlich im Feld Win32StartAddress. Dadurch sind Debuggingwerkzeuge wie Process Explorer in der Lage, diese Information anzuzeigen. 9. Um die KTHREAD-Struktur einzurichten, wird KeInitThread aufgerufen. Die ursprüngliche und die aktuelle Basispriorität des Threads, seine Affinität und sein Quantum werden auf die Basispriorität des Prozesses gesetzt. Danach weist KeInitThread einen Kernelstack für den Thread zu und initialisiert den maschinenabhängigen Hardwarekontext für den Thread einschließlich Kontext-, Trap- und Ausnahmeframes. Der Threadkontext wird so eingerichtet, dass der Thread in KiThreadStartup im Kernelmodus gestartet wird. Schließlich setzt KeInitThread den Threadstatus auf Initialized und gibt die Steuerung an PspAllocateThread zurück. 10. Bei einem UMS-Thread wird PspUmsInitThread aufgerufen, um den UMS-Status zu initialisieren. Nachdem diese Aufgaben erledigt sind, ruft NtCreateUserProcess die Funktion PspInsertThread auf, um die folgenden Schritte auszuführen: 1. Sofern in einem Attribut ein idealer Prozessor für den Thread angegeben wurde, wird er jetzt initialisiert. 2. Sofern in einem Attribut eine Gruppenaffinität für den Thread angegeben wurde, wird sie jetzt initialisiert. 3. Gehört der Prozess zu einem Job, wird eine Überprüfung durchgeführt, um sicherzustellen, dass die Gruppenaffinität des Threads die Einschränkungen des Jobs nicht verletzt (siehe die Beschreibung weiter vorn). 4. Es wird geprüft, ob der Prozess oder der Thread bereits beendet wurde und ob der Thread schon lauffähig war. Wenn irgendetwas davon zutrifft, schlägt die Threaderstellung fehl. 5. Wenn der Thread zu einem sicheren Prozess (IUM) gehört, wird ein sicheres Threadobjekt erstellt und initialisiert. 6. Der KTHREAD-Teil des Threadobjekts wird durch einen Aufruf von KeStartThread initialisiert. Dazu werden die Schedulereinstellungen vom Besitzerprozess geerbt, der ideale Knoten und Prozessor festgelegt, die Gruppenaffinität aktualisiert, die Basis- und die dynamische Priorität gesetzt (durch Kopieren vom Prozess), das Threadquantum bestimmt und der Thread in die von KPROCESS gepflegte Prozessliste eingefügt (wobei es sich um eine andere Liste als die in EPROCESS handelt). Der Ablauf von CreateProcess Kapitel 3 161
7. Ist der Prozess »tiefgefroren« (dürfen in ihm also keine Threads ausgeführt werden, auch keine neuen), so wird auch der Thread eingefroren. 8. Wenn es sich auf einem Nicht-x86-System um den ersten Thread eines Prozesses handelt (und dieser wiederum nicht der Leerlaufprozess ist), wird der Prozess in eine weitere systemweite Prozessliste eingefügt, die von der globalen Variablen PiProcessListHead gepflegt wird. 9. Der Threadzähler des Prozessobjekts wird erhöht und die E/A- und Seitenpriorität des Besitzerprozesses werden geerbt. Ist die neue Threadanzahl die höchste, die es in dem Prozess bisher gegeben hat, wird auch der Threadspitzenzähler aktualisiert. Bei dem zweiten Thread eines Prozesses wird das Primärtoken eingefroren, sodass es nicht mehr geändert werden kann. 10. Der Thread wird in die Threadliste des Prozesses eingefügt und angehalten, wenn der Erstellerprozess dies verlangt hat. 11. Das Threadobjekt wird in die Prozesshandletabelle eingefügt. 12. Handelt es sich um den ersten Thread des Prozesses (wurde er also im Rahmen eines CreateProcess*-Aufrufs erstellt), werden alle registrierten Callbacks für die Prozesserstellung und danach alle registrierten Callbacks für die Threaderstellung aufgerufen. Wenn irgendeiner dieser Callbacks ein Veto gegen den Erstellungsvorgang einlegt, schlägt er fehl, woraufhin ein entsprechender Status an den Aufrufer zurückgegeben wird. 13. Wurde über ein Attribut eine Jobliste angegeben und ist dies der erste Thread in dem Prozess, so wird der Prozess allen Jobs auf der Liste zugewiesen. 14. Der Thread wird durch einen Aufruf von KeReadyThread zur Ausführung bereit gemacht und tritt in den verzögerten Bereitschaftsstatus ein (siehe Kapitel 4). Phase 5: Spezifische Prozessinitialisierung für das WindowsTeilsystem durchführen Wenn NtCreateUserProcess einen Erfolgscode zurückgibt, sind die erforderlichen Exekutivprozess- und Exekutivthreadobjekte erstellt worden. Anschließend führt CreateProcessInternalW einige spezifische Operationen für das Windows-Teilsystem aus, um die Prozessinitialisierung abzuschließen. 1. Es wird geprüft, ob Windows die Ausführung der ausführbaren Datei erlauben soll. Dazu wird die Abbildversion im Header validiert und nachgeforscht, ob die Windows-Anwendungszertifizierung den Prozess durch eine Gruppenrichtlinie blockiert hat. Bei besonderen Ausgaben von Windows Server 2012 R2, z. B. Windows Storage Server 2012 R2, erfolgen noch weitere Prüfungen, um zu sehen, ob die Anwendung unzulässige APIs importiert. 2. Wenn es von Softwareeinschränkungsrichtlinien vorgeschrieben ist, wird für den neuen Prozess ein eingeschränktes Token erstellt. Anschließend wird die Anwendungskompatibilitätsdatenbank danach abgefragt, ob es für den Prozess einen Eintrag in der Registrierung oder der Anwendungsdatenbank des Systems gibt. Zu diesem Zeitpunkt werden noch 162 Kapitel 3 Prozesse und Jobs
keine Kompatibilitätsshims angewendet. Die Informationen werden im PEB gespeichert, sobald der ursprüngliche Start mit der Ausführung beginnt (Phase 6). 3. CreateProcessInternalW ruft (für nicht geschützte Prozesse) einige interne Funktionen auf, um SxS-Informationen wie Manifestdateien und DLL-Umleitungspfade (siehe den Abschnitt »DLL-Namensauflösung und -Umleitung« weiter hinten in diesem Kapitel) sowie weitere Informationen zu gewinnen, z. B. ob sich die EXE-Datei auf einem Wechselmedium befindet und welche Installererkennungsflags gesetzt sind. Für immersive Prozesse werden auch Versionsinformationen und die Angabe der Zielplattform aus dem Paketmanifest zurückgegeben. 4. Aus den folgenden gesammelten Informationen wird eine Nachricht an das Windows-Teilsystem (Csrss) zusammengestellt: • Name des Pfades und des SxS-Pfades • Prozess- und Threadhandles • Abschnittshandle • Zugriffstokenhandle • Medieninformationen • AppCompat- und Shim-Daten • Informationen über immersive Prozesse • PEB-Adresse • Verschiedene Flags, z. B. zur Angabe, ob der Prozess geschützt ist oder mit erhöhten Rechten ausgeführt werden soll • Ein Flag zur Angabe, ob der Prozess zu einer Windows-Anwendung gehört (damit Csrss bestimmen kann, ob der Startcursor angezeigt werden soll) • Sprachinformationen für die Benutzeroberfläche • DLL-Umleitungs- und .local-Flags (siehe den Abschnitt »Der Abbildlader« weiter hinten in diesem Kapitel) • Informationen aus der Manifestdatei Wenn das Windows-Teilsystem diese Nachricht erhält, führt es die folgenden Schritte aus: 1. CsrCreateProcess dupliziert ein Handle für den Prozess und den Thread. In diesem Schritt wird der Verwendungszähler des Prozesses und des Threads von dem bei der Erstellung festgelegten Wert 1 auf 2 hochgesetzt. 2. Die CSR_PROCESS-Struktur wird zugewiesen. 3. Der Ausnahmeport des neuen Prozesses wird auf den allgemeinen Funktionsport für das Windows-Teilsystem gesetzt, sodass das Teilsystem eine Nachricht empfängt, wenn in dem Prozess eine Zweite-Chance-Ausnahme auftritt (zur Ausnahmebehandlung siehe Kapitel 8 in Band 2). 4. Wird eine neue Prozessgruppe mit dem neuen Prozess als Stammprozess erstellt (Flag CREATE_NEW_PROCESS_GROUP in CreateProcess), dann wird sie in CSR_PROCESS eingerichtet. Prozessgruppen sind praktisch, um Steuerereignisse an mehrere Prozesse zu senden, die Der Ablauf von CreateProcess Kapitel 3 163
sich eine Konsole teilen. Weitere Informationen finden Sie in der Dokumentation des Windows SDK über CreateProcess und GenerateConsoleCtrlEvent. 5. Die CSR_THREAD-Struktur wird zugewiesen und initialisiert. 6. CsrCreateThread fügt den Thread in die Threadliste für den Prozess ein. 7. Der Prozesszähler der Sitzung wird heraufgesetzt. 8. Die Prozessbeendigungsebene wird auf den Standardwert 0x280 (siehe SetProcessShut downParameters in der Dokumentation des Windows SDK). 9. Die neue CSR_PROCESS-Struktur wird in die Liste der Prozesse für das gesamte Windows-Teilsystem eingefügt. Nachdem Csrss diese Schritte durchgeführt hat, prüft CreateProcessInternalW, ob der Prozess mit erhöhten Rechten ausgeführt wurde (was bedeutet, dass er über ShellExecute ausgeführt wurde und vom AppInfo-Dienst höhere Rechte erhalten hat, nachdem dem Benutzer das Bestätigungsdialogfeld angezeigt wurde). Dazu wird unter anderem nachgesehen, ob es sich bei dem Prozess um ein Setupprogramm handelt. Wenn ja, wird das Prozesstoken geöffnet und das Virtualisierungsflag eingeschaltet, damit die Anwendung virtualisiert wird (siehe die Informationen zur Benutzerkontensteuerung und Virtualisierung in Kapitel 7). Enthält die Anwendung Shims zur Rechteerhöhung oder hat sie in ihrem Manifest eine höhere Rechte ebene angefordert, wird der Prozess zerstört und eine Anforderung zur Rechteerhöhung an den AppInfo-Dienst gesendet. Für geschützte Prozesse entfallen die meisten dieser Überprüfungen. Da diese Prozesse für Windows Vista oder höher entworfen sein müssen, besteht keinerlei Grund für eine Überprüfung auf Rechteerhöhung, Virtualisierung und Anwendungskompatibilität. Außerdem könnte es zu Sicherheitslücken führen, wenn Mechanismen wie die Shim-Engine ihre üblichen Hookund Speicherpatch-Techniken bei geschützten Prozessen anwenden dürften, denn dann wäre es Angreifern möglich, eigene Shims einzufügen, die das Verhalten der geschützten Prozesse ändern. Da die Shim-Engine vom Elternprozess installiert wird, der möglicherweise keinen Zugriff auf seinen geschützten Kindprozess hat, würde selbst eine legitime Verwendung von Shims unter Umständen nicht funktionieren. Phase 6: Ausführung des ursprünglichen Threads starten Jetzt ist die Prozessumgebung festgelegt, die Ressourcen für den Thread sind zugewiesen, der Prozess hat einen Thread und das Windows-Teilsystem weiß über den neuen Prozess Bescheid. Sofern der Aufrufer nicht das Flag CREATE_SUSPENDED angegeben hat, wird der ursprüngliche Thread jetzt wieder aufgenommen, sodass er mit der Ausführung beginnen und die restlichen Arbeiten zur Prozessinitialisierung ausführen kann, die im Kontext des neuen Prozesses nötig sind (Phase 7). 164 Kapitel 3 Prozesse und Jobs
Phase 7: Prozessinitialisierung im Kontext des neuen Prozesses durchführen Als Erstes führt der neue Thread die Kernelmodus-Threadstartroutine KiStartUserThread aus. Sie senkt die IRQL-Ebene des Threads von DPC (Deferred Procedure Call, verzögerter Prozeduraufruf) in APC und ruft dann die ursprüngliche Threadroutine des Systems auf, PspUserThreadStartup. Dieser Routine wird die vom Benutzer angegebene Threadstartadresse als Parameter übergeben. PspUserThreadStartup führt dann die folgenden Aktionen aus: 1. Auf einer x86-Architektur installiert sie eine Ausnahmekette. (Auf anderen Architekturen weicht die Vorgehensweise ab; siehe Kapitel 8 in Band 2.) 2. Sie senkt die IRQL-Ebene auf PASSIVE_LEVEL (Ebene 0, was die einzige IRQL-Ebene ist, auf der Benutzercode ausgeführt werden darf). 3. Sie deaktiviert die Möglichkeit, das primäre Prozesstoken zur Laufzeit auszutauschen. 4. Wurde der Thread beim Start aus irgendeinem Grund abgebrochen, so wird er beendet. Es werden dann keine weiteren Maßnahmen unternommen. 5. Sie legt aufgrund der Informationen in den Kernelmodus-Datenstrukturen die Gebietsschema-ID und den idealen Prozessor im TEB fest und prüft, ob die Threaderstellung fehlgeschlagen ist. 6. Sie ruft DbgkCreateThread auf, die prüft, ob Abbildbenachrichtigungen für den neuen Prozess gesendet wurden. Wenn das nicht der Fall ist, Benachrichtigungen aber aktiviert sind, wird eine Abbildbenachrichtigung erst für den Prozess und dann für den Abbildladevorgang von Ntdll.dll gesendet. Dieser Vorgang erfolgt in dieser Phase und nicht bei der ursprünglichen Zuordnung der Abbilder, da die für Kernel-Callouts notwendige Prozess-ID dann noch gar nicht zugewiesen ist. 7. Danach folgt eine weitere Überprüfung, um zu sehen, ob es sich um einen zu debuggenden Prozess handelt. Wenn das der Fall ist, aber noch keine Debuggerbenachrichtigungen gesendet wurden, wird durch das Debugobjekt (falls vorhanden) eine Prozesserstellungsnachricht gesendet, sodass das Prozessstart-Debugereignis (CREATE_PROCESS_DEBUG_INFO) an den entsprechenden Debuggerprozess gesendet werden kann. Darauf folgt ein ähnliches Threadstart-Debugereignis sowie ein weiteres Debugereignis für den Abbildladevorgang von Ntdll.dll. Anschließend wartet DbgkCreateThread auf eine Antwort des Debuggers (über die Funktion ContinueDebugEvent). 8. Sie prüft, ob das Anwendungs-Prefetching auf dem System aktiviert ist. Wenn ja, ruft sie den Prefetcher (und SuperFetch) auf, um die Prefetch-Anweisungsdatei (falls vorhanden) und die während der ersten zehn Sekunden der letzten Prozessausführung referenzierten Prefetchseiten zu verarbeiten. (Mehr über den Prefetcher und SuperFetch erfahren Sie in Kapitel 5.) Der Ablauf von CreateProcess Kapitel 3 165
9. Sie prüft, ob das systemweite Cookie in der Struktur SharedUserData eingerichtet wurde. Wenn nicht, generiert sie es auf der Grundlage eines Hashs von Systeminformationen wie der Anzahl der verarbeiteten Interrupts, der DPC-Auslieferungen, der Seitenfehler, der Interruptzeit und einer Zufallszahl. Dieses systemweite Cookie wird als Schutz gegen bestimmte Arten von Exploits bei der internen Codierung und Decodierung von Zeigern verwendet, u. a. im Heap-Manager. (Mehr über die Sicherheit des Heap-Managers erfahren Sie in Kapitel 5.) 10. Bei einem sicheren Prozess (IUM-Prozess) wird HvlStartSecureThread aufgerufen, um die Steuerung für den Start der Threadausführung an den sicheren Kernel zu übertragen. Diese Funktion gibt die Steuerung nur zurück, wenn der Thread vorhanden ist. 11. Sie richtet den ursprünglichen Thunkkontext zur Ausführung der Initialisierungsroutine des Abbildladers (LdrInitializeThunk in Ntdll.dll) und den systemweiten Threadstartstub ein (RtlUserThreadStart in Ntdll.dll). Dazu wird der Kontext des Threads direkt bearbeitet und dann eine Beendigung des Systemdienstbetriebs verlangt, wodurch dieser spezielle Benutzerkontext geladen wird. Die Routine LdrInitializeThunk initialisiert den Lader, den Heap-Manager, die NLS-Tabellen, die Arrays für den lokalen Threadspeicher (TLS) und den lokalen Fiberspeicher (FLS) sowie kritische Abschnittsstrukturen. Anschließend lädt sie alle erforderlichen DLLs und ruft die DLL-Eintrittspunkte mit dem Funktionscode von DLL_PROCESS_ATTACH auf. Sobald die Funktion die Steuerung zurückgibt, stellt NtContinue den neuen Benutzerkontext wieder her und kehrt in den Benutzermodus zurück. Jetzt beginnt die echte Threadausführung. RtlUserThreadStart verwendet die Adresse des eigentlichen Abbildeintrittspunktes und den Startparameter und ruft den Eintrittspunkt der Anwendung auf. Diese beiden Parameter wurden bereits vom Kernel auf den Stack gelegt. Für diesen komplizierten Ablauf gibt es zwei Gründe: QQ Der Abbildlader in Ntdll.dll wird dadurch in die Lage versetzt, den Prozess intern und hinter den Kulissen einzurichten, sodass anderer Benutzermoduscode ordnungsgemäß ausgeführt werden kann. (Andernfalls gäbe es keinen Heap, keinen lokalen Threadspeicher usw.) QQ Wenn alle Threads in einer gemeinsamen Routine beginnen, können sie mit einer Ausnahmebehandlung versehen werden. Dadurch kann Ntdll.dll es erkennen, wenn sie abstürzen, und den Filter für nicht behandelte Ausnahmen in Kernel32.dll aufrufen. Außerdem kann Ntdll.dll die Threadbeendigung beim Rücksprung von der Startroutine des Threads koordinieren und verschiedene Aufräumarbeiten durchführen. Anwendungsentwickler können SetUnhandledExceptionFilter aufrufen, um ihren eigenen Code für den Umgang mit nicht behandelten Ausnahmen hinzuzufügen. 166 Kapitel 3 Prozesse und Jobs
Experiment: Den Prozessstart verfolgen Sie haben jetzt in allen Einzelheiten gesehen, wie ein Prozess startet und welche verschiedenen Operationen erforderlich sind, um mit der Ausführung einer Anwendung zu beginnen. Nun wollen wir uns mit Process Monitor einige der Dateieingaben und -ausgaben ansehen, die bei diesen Vorgängen erfolgen, sowie einige der Registrierungsschlüssel, auf die dabei zugegriffen wird. Dieses Experiment vermittelt zwar keine Gesamtansicht aller internen Schritte, die wir zuvor beschrieben haben, doch Sie können dabei verschiedene Teile des Systems in Aktion erleben, vor allem den Prefetcher und SuperFetch, die Überprüfung der Optionen zur Ausführung der Abbilddatei, weitere Kompatibilitätsprüfungen sowie die Zuordnung der DLL des Abbildladers. Als Beispiel verwenden wir eine sehr einfache ausführbare Datei, nämlich Notepad.exe (den Windows-Editor), die wir an einer Eingabeaufforderung (Cmd.exe) starten. Beachten Sie, dass wir uns dabei sowohl die Operationen innerhalb von Cmd.exe als auch die in Notepad.exe ansehen. Wie Sie bereits wissen, wird ein Großteil der Arbeiten im Benutzermodus von der Funktion CreateProcessInternalW erledigt, die von dem Elternprozess aufgerufen wird, bevor der Kernel das neue Prozessobjekt erstellt hat. Gehen Sie zur Vorbereitung wie folgt vor: 1. Fügen Sie im Process Monitor zwei Filter hinzu, einen für Cmd.exe und einen für Notepad.exe. Dies sind die beiden einzigen Prozesse, die Sie einschließen sollten. Sorgen Sie dafür, dass zurzeit keine Instanzen dieser beiden Prozesse laufen, damit sichergestellt ist, dass Sie sich auch die richtigen Ereignisse ansehen. Das Filterfenster sollte wie folgt aussehen: 2. Vergewissern Sie sich, dass die Ereignisprotokollierung zurzeit deaktiviert ist (öffnen Sie File und deaktivieren Sie Capture Events). Starten Sie dann die Eingabeaufforderung. 3. Aktivieren Sie die Ereignisprotokollierung (öffnen Sie File und wählen Sie Event Logging, drücken Sie (Strg) + (E) oder klicken Sie auf das Lupensymbol auf der Symbolleiste). Geben Sie dann Notepad.exe ein und drücken Sie die Eingabetaste. Auf einem typischen Windows-System erscheinen nun etwa 500 bis 3500 Ereignisse. → Der Ablauf von CreateProcess Kapitel 3 167
4. Stoppen Sie die Erfassung und blenden Sie die Spalten Sequence und Time of Day aus, sodass Sie sich besser auf die für unser Experiment interessanten Spalten konzentrieren können. Das Fenster sollte jetzt so ähnlich wie in dem folgenden Screenshot aussehen. Wie in Phase 1 des Ablaufs von CreateProcess beschrieben, liest Cmd.exe, kurz bevor der Prozess gestartet und der erste Thread erstellt wird, in der Registrierung, und zwar in HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\ Notepad.exe. Da mit Notepad.exe keine Optionen zur Ausführung des Abbilds verbunden sind, wird der Prozess regulär erstellt. Wie bei anderen Ereignissen im Protokoll von Process Monitor können Sie auch hier durch Untersuchung des Ereignisstacks erkennen, welche Teile des Ablaufs zur Prozesserstellung im Benutzer- und welche im Kernelmodus durchgeführt wurden und durch welche Routinen das geschah. Dazu doppelklicken Sie auf das Ereignis RegOpenKey und wechseln zur Registerkarte Stack. Der folgende Screenshot zeigt den Standardstack auf einem 64-Bit-Computer mit Windows 10. → 168 Kapitel 3 Prozesse und Jobs
Der Stack zeigt, dass Sie bereits den Teil der Prozesserstellung erreicht haben, der im Kernelmodus ausgeführt wird (durch NtCreateUserProcess) und dass die Hilfsroutine PspAllocateProcess für die vorliegende Überprüfung verantwortlich ist. Wenn Sie sich die Liste der Ereignisse nach dem Erstellen des Threads und des Prozesses ansehen, werden Ihnen drei Gruppen von Ereignissen auffallen: QQ Eine einfache Überprüfung von Flags zur Anwendungskompatibilität, durch die der Prozesserstellungscode des Benutzermodus erkennen kann, ob eine Überprüfung in der Anwendungskompatibilitätsdatenbank durch die Shim-Engine erforderlich ist. QQ Mehrere Lesevorgänge in den SxS- (Side-By-Side), Manifest- und MUI-/Sprachschlüsseln, die zu dem zuvor erwähnten Assemblyframework gehören. QQ Datei-E/A für eine oder mehrere .sdb-Dateien, bei denen es sich um die Anwendungskompatibilitätsdatenbanken auf dem System handelt. Bei diesen Ein-/Ausgaben finden zusätzliche Überprüfungen statt, um herauszufinden, ob für die Shim-Engine diese Anwendung aufgerufen werden muss. Da der Editor ein gutwilliges Microsoft-Programm ist, sind hier keine Shims nötig. → Der Ablauf von CreateProcess Kapitel 3 169
Der folgende Screenshot zeigt die nächsten Ereignisse, die innerhalb des Notepad-Prozesses selbst auftreten. Dies sind die Aktionen, die der Benutzermodus-Threadstartwrapper im Kernelmodus ausgelöst hat. Die ersten beiden sind Debugbenachrichtigungen über das Laden von Abbildern von Notepad.exe und Ntdll.dll. Diese Nachrichten können erst jetzt generiert werden, da der Code im Prozesskontext des Editors läuft und nicht mehr im Kontext der Eingabeaufforderung. Als Nächstes greift der Prefetcher ein und sucht nach einer Prefetch-Datenbankdatei, die bereits für den Editor generiert wurde. (Weitere Informationen über den Prefetcher erhalten Sie in Kapitel 5.) Wurde der Editor auf dem System schon mindestens einmal ausgeführt, so ist diese Datenbank vorhanden, sodass der Prefetcher nun beginnt, die darin angegebenen Befehle auszuführen. In diesem Fall sehen Sie, wenn Sie weiter nach unten scrollen, wie verschiedene DLLs gelesen und abgefragt werden. Anders als beim gewöhnlichen Laden von DLLs – durch den Abbildlader des Benutzermodus, der sich die Importtabellen ansieht, oder manuell durch Anwendungen – werden die Ereignisse hier vom Prefetcher hervorgerufen, der die vom Editor benötigten Bibliotheken kennt. Das gewöhnliche Laden von DLLs folgt jedoch als Nächstes, und dabei werden Sie ähnliche Ereignisse wie hier sehen. → 170 Kapitel 3 Prozesse und Jobs
Die Ereignisse werden jetzt von Benutzermoduscode generiert, der aufgerufen wurde, als die Kernelmodus-Wrapperfunktion ihre Arbeit beendet hatte. Daher stammen die ersten Ereignisse von LdrpInitializeProcess, die von LdrInitializeThunk für den ersten Thread des Prozesses aufgerufen wurde. Das können Sie bestätigen, indem Sie sich den Ereignisstack ansehen – beispielsweise das Abbildladeereignis für Kernel32.dll, das im folgenden Screenshot zu sehen ist. → Der Ablauf von CreateProcess Kapitel 3 171
Es folgen weitere Ereignisse, die von dieser Routine und ihren Hilfsfunktionen hervorgerufen wurden. Schließlich gelangen Sie zu den Ereignissen der Funktion WinMain des Editors, in der der Code ausgeführt wird, der der Kontrolle des Entwicklers unterliegt. Die ausführliche Beschreibung aller Ereignisse und Benutzermoduskomponenten, die bei der Prozessausführung auftreten, würde ein ganzes Kapitel füllen. Die Untersuchung weiterer Ereignisse überlassen wir Ihnen daher als Übungsaufgabe. Einen Prozess beenden Ein Prozess ist ein Container und eine Grenze. Das bedeutet, dass Ressourcen, die von einem Prozess verwendet werden, in anderen Prozessen nicht unbedingt sichtbar sind. Um Informa tionen zwischen Prozessen auszutauschen, ist daher ein Kommunikationsmechanismus erforderlich. Es ist auch nicht möglich, dass ein Prozess versehentlich willkürliche Bytes im Arbeitsspeicher eines anderen Prozesses überschreibt, denn das würde den ausdrücklichen Aufruf einer Funktion wie WriteProcessMemory erfordern. Dazu wiederum müsste ausdrücklich ein Handle mit der korrekten Zugriffsmaske (PROCESS_VM_WRITE) geöffnet werden, wobei nicht einmal sichergestellt ist, dass dieser Zugriff auch gewährt wird. Diese natürliche Isolation der Prozesse bedeutet auch, dass eine Ausnahme in einem Prozess keine Auswirkungen auf andere Prozesse hat. Schlimmstenfalls stürzt der Prozess ab, aber der Rest des Systems bleibt intakt. Ein Prozess kann durch den Aufruf der Funktion ExitProcess ordnungsgemäß beendet werden. Für viele Prozesse – je nach Linkereinstellung – ruft der Prozessstartcode für den ersten Thread ExitProcess im Namen des Prozesses auf, wenn der Thread von der Hauptfunktion zurückspringt. Ordnungsgemäß bedeutet hier, dass die in den Prozess geladenen DLLs über die anstehende Beendigung des Prozesses benachrichtigt werden, damit sie die Gele- 172 Kapitel 3 Prozesse und Jobs
genheit bekommen, noch etwas zu tun. Diese Benachrichtigung erfolgt durch den Aufruf ihrer D llMain-Funktion mit DLL_PROCESS_DETACH. ExitProcess kann nur von dem zu beendenden Prozess selbst aufgerufen werden. Eine nicht ordnungsgemäße Beendigung eines Prozesses ist mit der Funktion TerminateProcess möglich, die von außerhalb des Prozesses aufgerufen werden kann. (Process Explorer und der Task-Manager verwenden diese Funktion, wenn sie dazu aufgefordert werden.) TerminateProcess benötigt ein Handle für den Prozess, der mit der Zugriffsmaske PROCESS_TERMINATE geöffnet wird. Allerdings wird dieser Zugriff nicht zwangsläufig gewährt. Daher ist es nicht einfach und manchmal sogar unmöglich, bestimmte Prozesse zu beenden (z. B. Csrss). Nicht ordnungsgemäß bedeutet hier, dass die DLLs keine Chance mehr haben, irgendwelchen Code auszuführen (DLL_PROCESS_DETACH wird nicht gesendet), und alle Threads abrupt abgebrochen werden. Das kann zu Datenverlusten führen, etwa wenn ein Dateicache keine Möglichkeit mehr hat, seine Daten in die Zieldatei zu übertragen. Wie auch immer ein Prozess beendet wird, es kann keine Lecks geben. Der gesamte private Arbeitsspeicher des Prozesses wird automatisch vom Kernel freigegeben, der Adressraum wird zerstört, alle Handles zu Kernelobjekten werden geschlossen usw. Solange es noch offene Handles zu dem Prozess gibt (weil die EPROCESS-Struktur nach wie vor existiert), können andere Prozesse Zugriff auf einige der Prozessverwaltungsinformationen bekommen, z. B. auf den Prozessbeendigungscode (GetExitCodeProcess). Nach dem Schließen dieser Handles wird die EPROCESS-Struktur jedoch zerstört, sodass wirklich nichts mehr von dem Prozess übrig ist. Allerdings können Drittanbietertreiber im Namen des Prozesses Zuweisungen im Kernelspeicher vornehmen, etwa aufgrund einer IOCTL (siehe Kapitel 6) oder einfach einer Prozessbenachrichtigung. Es liegt dann in ihrer Verantwortung, diesen Poolarbeitsspeicher freizugeben. Windows hält sich nicht über den Kernelspeicher auf dem Laufenden, den ein Prozess besitzt, und räumt ihn auch nicht auf (abgesehen von Arbeitsspeicher, der aufgrund vom Prozess erstellter Handles mit Objekten belegt ist). Damit der Treiber seinen Freigabepflichten nachkommen kann, wird er gewöhnlich über eine IRP_MJ_CLOSE-, IRP_MJ_CLEANUP- oder Prozessbeendigungsnachricht darüber informiert, dass das Handle für das Geräteobjekt geschlossen wurde. Der Abbildlader Wenn auf dem System ein Prozess gestartet wird, erstellt der Kernel ein Prozessobjekt dafür und führt verschiedene Initialisierungstätigkeiten aus. Dies führt jedoch nicht zur Ausführung der Anwendung, sondern bereitet lediglich den Kontext und die Umgebung vor. Im Gegensatz zu Treibern, bei denen es sich um Kernelmoduscode handelt, werden Anwendungen im Benutzermodus ausgeführt. Der Großteil der eigentlichen Initialisierungsarbeit erfolgt außerhalb des Kernels. Sie wird durch den Abbildlader erledigt, der intern als Ldr bezeichnet wird. Der Abbildlader befindet sich in der Benutzermodus-System-DLL Ntdll.dll und nicht in der Kernelbibliothek. Daher verhält er sich wie Standardcode aus einer DLL und unterliegt denselben Einschränkungen für den Speicherzugriff und die Sicherheitsrechte. Das Besondere an diesem Code ist, dass er im laufenden Prozess garantiert vorhanden ist (da Ntdll.dll stets geladen wird) und als erster Benutzermoduscode eines neuen Prozesses ausgeführt wird. Der Abbildlader Kapitel 3 173
Da der Lader vor dem eigentlichen Anwendungscode ausgeführt wird, ist er für Benutzer und Entwickler gewöhnlich nicht sichtbar. Obwohl seine Initialisierungsaufgaben verborgen sind, kann ein Programm zur Laufzeit mit den Schnittstellen des Laders kommunizieren, z. B. beim Laden oder Entladen einer DLL oder beim Abfragen der Basisadresse einer DLL. Der Lader ist unter anderem für die folgenden Aufgaben zuständig: QQ Initialisierung des Benutzermodusstatus der Anwendung, z. B. durch Erstellen des ursprünglichen Heaps und Einrichten der Slots für den lokalen Thread- und Fiberspeicher (TLS bzw. FLS). QQ Analyse der Importtabelle (IAT) der Anwendung, um nach allen erforderlichen DLLs zu suchen (mit anschließender rekursiver Analyse der IATs der einzelnen DLLs), gefolgt von der Analyse der Exporttabellen der DLLs, um sicherzustellen, dass die Funktion tatsächlich vorhanden ist. (Mit besonderen Forwarder-Einträgen kann ein Export zu einer anderen DLL umgeleitet werden.) QQ Laden und Entladen von DLLs zur Laufzeit und bei Bedarf und Pflege einer Liste aller geladenen Module (Moduldatenbank). QQ Handhabung von Manifestdateien für die SxS-Unterstützung (Side-by-Side) sowie von MUI-Dateien und -Ressourcen (Multiple Language User Interface). QQ Durchsuchen der Anwendungskompatibilitätsdatenbank nach Shims und ggf. Laden der DLL für die Shim-Engine. QQ Aktivieren der Unterstützung für API-Sets und API-Umleitung, einem Kernbestandteil der One Core-Funktionalität, die es ermöglicht, UWP-Anwendungen zu erstellen (Universal Windows Platform). QQ Aktivieren von Vorbeugungsmaßnahmen zur dynamischen Laufzeitkompatibilität durch SwitchBack-Mechanismen sowie Interaktion mit der Shim-Engine und den Anwendungsverifizierungsmechanismen. Die meisten dieser Aufgaben sind unverzichtbar, um es der Anwendung zu ermöglichen, ihren eigenen Code auszuführen. Ohne sie würden alle Vorgänge vom Aufruf externer Funktionen bis zur Verwendung des Heaps sofort fehlschlagen. Nachdem der Prozess erstellt ist, ruft der Lader die native API NtContinue auf, um die Ausführung gemäß dem Ausnahmeframe fortzusetzen, der auf dem Stack liegt, wie es auch jeder andere Ausnahmehandler tun würde. Dieser Ausnahmeframe wurde vom Kernel erstellt und enthält den eigentlichen Eintrittspunkt der Anwendung. Da der Lader keinen Standardaufruf ausführt oder in die laufende Anwendung hineinspringt, sind die Initialisierungsfunktionen des Laders im Aufrufbaum der Stackablaufverfolgung für einen Thread niemals zu sehen. 174 Kapitel 3 Prozesse und Jobs
Experiment: Den Abbildlader beobachten In diesem Experiment verwenden Sie globale Flags, um die Debuggerfunktion der Lader snapshots zu aktivieren. Damit können Sie beim Debuggen des Anwendungsstarts die Debugausgabe des Abbildladers einsehen. 1. Starten Sie in dem Verzeichnis, in dem Sie WinDbg installiert haben, die Anwendung Gflags.exe und klicken Sie auf die Registerkarte Image File. 2. Geben Sie Notepad.exe in das Feld Image ein und drücken Sie (Tab). Dadurch werden die verschiedenen Optionen aktiviert. Wählen Sie Show Loader Snaps aus und klicken Sie auf OK oder Apply. 3. Starten Sie WinDbg, öffnen Sie das Menü File, wählen Sie Open Executable und suchen Sie C:\Windows\System32\Notepad.exe, um sie zu starten. Es werden jetzt Debuginformationen wie die folgenden angezeigt: 0f64:2090 @ 02405218 - LdrpInitializeProcess - INFO: Beginning execution of notepad.exe (C:\WINDOWS\notepad.exe) Current directory: C:\Program Files (x86)\Window Kits\10\Debuggers\ Package directories: (null) 0f64:2090 @ 02405218 - LdrLoadDll - ENTER: DLL name: KERNEL32.DLL 0f64:2090 @ 02405218 - LdrpLoadDllInternal - ENTER: DLL name: KERNEL32.DLL 0f64:2090 @ 02405218 - LdrpFindKnownDll - ENTER: DLL name: KERNEL32.DLL 0f64:2090 @ 02405218 - LdrpFindKnownDll - RETURN: Status: 0x00000000 0f64:2090 @ 02405218 - LdrpMinimalMapModule - ENTER: DLL name: C:\WINDOWS\ System32\KERNEL32.DLL ModLoad: 00007fff'5b4b0000 00007fff'5b55d000 C:\WINDOWS\System32\KERNEL32. DLL 0f64:2090 @ 02405218 - LdrpMinimalMapModule - RETURN: Status: 0x00000000 0f64:2090 @ 02405218 - LdrpPreprocessDllName - INFO: DLL api-ms-wincorertlsupportl1-2-0.dll was redirected to C:\WINDOWS\SYSTEM32\ntdll.dll by API set 0f64:2090 @ 02405218 - LdrpFindKnownDll - ENTER: DLL name: KERNELBASE.dll 0f64:2090 @ 02405218 - LdrpFindKnownDll - RETURN: Status: 0x00000000 0f64:2090 @ 02405218 - LdrpMinimalMapModule - ENTER: DLL name: C:\WINDOWS\ System32\KERNELBASE.dll ModLoad: 00007fff'58b90000 00007fff'58dc6000 C:\WINDOWS\System32\KERNELBASE. dll 0f64:2090 @ 02405218 - LdrpMinimalMapModule - RETURN: Status: 0x00000000 0f64:2090 @ 02405218 - LdrpPreprocessDllName - INFO: DLL api-ms-wineventingprovider-l1-1-0.dll was redirected to C:\WINDOWS\SYSTEM32\ kernelbase.dll by API set 0f64:2090 @ 02405218 - LdrpPreprocessDllName - INFO: DLL api-ms-wincoreapiqueryl1-1-0.dll was redirected to C:\WINDOWS\SYSTEM32\ntdll.dll by API set → Der Abbildlader Kapitel 3 175
4. Der Debugger hält schließlich mitten im Ladercode an. Dies geschieht an der Stelle, an der der Imagelader prüft, ob ein Debugger angeschlossen ist, und einen Haltepunkt auslöst. Wenn Sie (g) drücken, um die Ausführung fortzusetzen, werden weitere Meldungen des Laders angezeigt. Schließlich erscheint der Windows-Editor. 5. Arbeiten Sie mit dem Editor. Dabei können Sie beobachten, dass bei bestimmten Operationen der Lader aufgerufen wird, z. B. wenn Sie das Dialogfeld Speichern oder Öffnen aufrufen. Das zeigt, dass der Lader nicht nur beim Start ausgeführt wird, sondern weiterhin auf Threadanforderungen reagiert, die das verzögerte Laden weiterer Module verursachen können (die dann nach Gebrauch wieder entladen werden). Frühe Prozessinitialisierung Der Lader befindet sich in Ntdll.dll, also einer nativen DLL, die nicht mit einem bestimmten Teilsystem verbunden ist. Daher unterliegen alle Prozesse demselben Laderverhalten (mit einigen kleinen Unterschieden). Wir haben uns bereits ausführlich die Schritte angesehen, die zum Erstellen eines Prozesses im Kernelmodus führen, und einige der Aufgaben beschrieben, die die Windows-Funktion CreateProcess erledigt. Jetzt wollen wir uns mit den weiteren Arbeiten beschäftigen, die unabhängig vom Teilsystem im Benutzermodus stattfinden, sobald die Ausführung der ersten Benutzermodusanweisung beginnt. Beim Start eines Prozesses führt der Lader die folgenden Schritte aus: 1. Er prüft, ob LdrpProcessInitialized bereits auf 1 eingestellt oder das Flag SkipLoaderInit im TEB gesetzt wurde. Wenn ja, überspringt er die gesamte Initialisierung und wartet drei Sekunden lang darauf, dass LdrpProcessInitializationComplete aufgerufen wird. Dies geschieht etwa, wenn die Windows-Fehlerberichterstattung Prozessreflexion einsetzt, oder bei anderen Prozessabspaltungen, bei denen keine Laderinitialisierung benötigt wird. 2. Er setzt LdrInitState auf 0, was bedeutet, dass der Lader nicht initialisiert ist, sowie das PEB-Flag ProcessInitializing und das TEB-Flag RanProcessInit auf 1. 3. Er initialisiert die Ladersperre im PEB. 4. Er initialisiert die dynamische Funktionstabelle für die Abwicklungs- und Ausnahmeunterstützung in JIT-Code. 5. Er initialisiert den veränderlichen, schreibgeschützten Heap-Abschnitt MRDATA (Mutable Read Only), in dem sicherheitsrelevante globale Variablen gespeichert werden, die von Exploits nicht bearbeitet werden dürfen (siehe Kap. 7). 6. Er initialisiert die Laderdatenbank im PEB. 7. Er unterstützt die Sprachunterstützungstabellen (National Language Support, NLS) für den Prozess. 8. Er stellt den Abbildpfadnamen für die Anwendung zusammen. 9. Er erfasst die SEH-Ausnahmehandler aus dem Abschnitt .pdata und erstellt die internen Ausnahmetabellen. 176 Kapitel 3 Prozesse und Jobs
10. Er erfasst die Systemaufrufthunks für die fünf kritischen Laderfunktionen NtCreateSection, NtOpenFile, NtQueryAttributesFile, NtOpenSection und NtMapViewOfSection. 11. Er liest die Vorbeugeoptionen für die Anwendung (die vom Kernel über die exportierte Variable LdrSystemDllInitBlock übergeben wurden). Sie werden ausführlich in Kapitel 7 beschrieben. 12. Er fragt den Registrierungsschlüssel Image File Execution Options für die Anwendung ab. Dazu gehören Optionen wie die globalen Flags (gespeichert in GlobalFlags), Debuggingoptionen für den Heap (DisableHeapLookaside, ShutdownFlags und FrontEndHeapDebugOptions), Ladereinstellungen (UnloadEventTraceDepth, MaxLoaderThreads, UseImpersonatedDeviceMap), ETW-Einstellungen (TracingFlags) sowie weitere Optionen wie MinimumStackCommitInBytes und MaxDeadActivationContexts. Im Rahmen dieser Arbeiten werden auch das Paket Application Verifier und die zugehörigen Verifizierungs-DLLs initialisiert und die ControlFlow Guard-Optionen (CFG) von CFGOptions gelesen. 13. Er schaut im Header der ausführbaren Datei nach, ob es sich um eine .NET-Anwendung handelt (was durch das Vorhandensein eines .NET-spezifischen Abbildverzeichnisses angezeigt wird) und ob das Abbild im 32-Bit-Format vorliegt. Außerdem fragt er den Kernel ab, um zu ermitteln, ob der Prozess ein Wow64-Prozess ist. Bei Bedarf kann er auch mit einem reinen Abbildlader-Abbild von 32 Bit umgehen, für das Wow64 nicht benötigt wird. 14. Er lädt die im Ladekonfigurationsverzeichnis der ausführbaren Datei angegebenen Konfigurationsoptionen. Diese Optionen werden vom Entwickler bei der Kompilierung der Anwendung definiert und können vom Compiler und vom Linker genutzt werden, um bestimmte Sicherheits- und Vorbeugemerkmale wie CFG einzurichten. Sie steuern das Verhalten der ausführbaren Datei. 15. Er nimmt eine Minimalinitialisierung des FLS und TLS vor. 16. Er richtet die Debuggingoptionen für kritische Abschnitte ein, erstellt eine Datenbank für die Ablaufverfolgung des Benutzermodusstacks (sofern das entsprechende globale Flag gesetzt war) und ruft StackTraceDatabaseSizeInMb von Image File Execution Options ab. 17. Er initialisiert den Heap-Manager für den Prozess und erstellt den ersten Prozessheap. Um die erforderlichen Parameter einzurichten, zieht dieser Heap verschiedene Optionen für die Ladekonfiguration, die Abbilddateiausführung, die globalen Flags und den Header der ausführbaren Datei heran. 18. Er aktiviert den Prozess Terminate für die Vorbeugemaßnahme gegen Heapbeschädigung, falls diese eingeschaltet ist. 19. Er initialisiert das Ausnahme-Dispatchprotokoll, falls das entsprechende globale Flag gesetzt ist. 20. Er initialisiert das Threadpoolpaket, das die Threadpool-API unterstützt. Es fragt NUMA- Informationen ab und berücksichtigt sie. 21. Er initialisiert und konvertiert den Umgebungs- und den Parameterblock, vor allem zur Unterstützung von WoW64-Prozessen. 22. Er öffnet das Objektverzeichnis \KnownDlls und erstellt den Pfad für bekannte DLLs. Für einen WoW64-Prozess wird stattdessen \KnownDlls32 verwendet. Der Abbildlader Kapitel 3 177
23. Für Store-Anwendungen liest er die Optionen der Anwendungsmodellrichtlinie, die in den Claims WIN://PKG und P://SKUID des Tokens codiert sind (siehe den Abschnitt »Anwendungscontainer« in Kapitel 7). 24. Er bestimmt das aktuelle Verzeichnis, den Systempfad und den Standardladepfad (zum Laden von Abbildungen und Öffnen von Dateien) des Prozesses sowie die Regeln für die Standardreihenfolge bei der DLL-Suche. Dazu gehört es, die aktuellen Richtlinieneinstellungen für Universal- (UWP), Desktop-Bridge- (Centennial) bzw. Silverlight-Anwendungen (Windows Phone 8) oder -Dienste zu lesen. 25. Er erstellt den ersten Eintrag in der Laderdatentabelle für Ntdll.dll und fügt ihn in die Moduldatenbank ein. 26. Er erstellt die Tabelle für den Abwicklungsverlauf. 27. Er initialisiert den Parallellader, der mithilfe des Threadpools und gleichzeitiger Threads alle Abhängigkeiten lädt, die keine Kreuzabhängigkeiten aufweisen. 28. Er erstellt den nächsten Eintrag in der Laderdatentabelle für die ausführbare Hauptdatei und fügt ihn in die Moduldatenbank ein. 29. Bei Bedarf verlagert er das Abbild der ausführbaren Hauptdatei. 30. Er initialisiert die Anwendungsverifizierung, falls sie aktiviert ist. 31. Bei einem Wow64-Prozess initialisiert er die Wow64-Engine. Der 64-Bit-Lader schließt dabei seine Initialisierung ab und der 32-Bit-Lader übernimmt die Kontrolle und startet die meisten bis jetzt beschriebenen Operationen neu. 32. Wenn es sich um ein .NET-Abbild handelt, validiert der Lader es. Anschließend lädt er Mscoree.dll (ein .NET-Laufzeit-Shim) und ruft den Eintrittspunkt der ausführbaren Hauptdatei ab (_CorExeMain). Damit überschreibt er den Ausnahmedatensatz, sodass dieser Eintrittspunkt statt der regulären main-Funktion verwendet wird. 33. Er initialisiert die TLS-Slots des Prozesses. 34. Für Windows-Teilsystem-Anwendungen lädt er manuell Kernel32.dll und Kernelbase.dll unabhängig von den tatsächlichen Importen des Prozesses. Er verwendet diese Bibliotheken nach Bedarf, um den SRP/Safer-Mechanismus zu initialisieren (Software Restriction Policies) und um die Thunkfunktion für die Initialisierung des Windows-Teilsystemthreads abzufangen. Schließlich löst er jegliche API-Set-Abhängigkeiten zwischen diesen beiden Bibliotheken auf. 35. Er initialisiert die Shim-Engine und parst die Shim-Datenbank. 36. Er aktiviert den parallelen Abbildlader, sofern mit den zuvor gescannten Kernladerfunk tionen keine Systemaufruf-Hooks oder »Umleitungen« verbunden sind. Dies geschieht auf der Grundlage der Anzahl von Laderthreads, die durch Richtlinien und Optionen zur Abbildausführung festgelegt wurden. 37. Er setzt die Variable LdrInitState auf 1, was »Importladevorgang läuft« bedeutet. Jetzt ist der Abbildlader bereit, die Importtabelle der ausführbaren Datei für die Anwendung zu analysieren und jegliche DLLs zu laden, die beim Kompilieren der Anwendung dynamisch verlinkt wurden. Das geschieht sowohl bei .NET-Images, deren Importe durch Aufrufe in der 178 Kapitel 3 Prozesse und Jobs
.NET-Laufzeitumgebung verarbeitet werden, als auch für reguläre Abbilder. Da jede importierte DLL wiederum ihre eigene Importtabelle haben kann, wurde diese Operation früher rekursiv durchgeführt, bis alle DLLs berücksichtigt und alle zu importierenden Funktionen gefunden waren. Beim Laden der einzelnen Dateien behielt der Lader Statusinformationen dafür bei und stellte die Moduldatenbank auf. In neueren Windows-Versionen erstellt der Lader stattdessen im Voraus eine Abhängigkeitsstruktur mit Knoten für die einzelnen DLLs und einem Abhängigkeitsgraphen. Dabei kann er Knoten herausarbeiten, die parallel geladen werden können. Wird eine Serialisierung benötigt, wird die Arbeitswarteschlange des Threadpools geleert, was als Synchronisierungspunkt fungiert. Ein solcher Punkt tritt auf dem Aufruf aller DLL-Initialisierungsroutinen für die statischen Importe auf, was einer der letzten Schritte des Laders ist. Nachdem dies erledigt ist, werden die statischen TLS-Initialisierer aufgerufen. Bei Windows-Anwendungen wird zwischen diesen beiden Schritten zu Anfang die Thunkfunktionen für die Initialisierung des Kernel32-Threads (BaseThreadInitThunk) und am Ende die Post-Prozessinitialisierungsroutine von Kernel32 aufgerufen. DLL-Namensauflösung und -Umleitung Bei der Namensauflösung ermittelt das System aus dem Namen einer PE-Binärdatei die physische Datei, wenn der Aufrufer keine eindeutige Dateikennung angegeben hat oder es nicht kann. Da die Speicherorte der verschiedenen Verzeichnisse (Anwendungsverzeichnis, Systemverzeichnis usw.) zur Verlinkungszeit nicht hartcodiert werden können, schließt dies die Auflösung aller Abhängigkeiten von Binärdateien sowie LoadLibrary-Operationen ein, in denen der Aufrufer keinen vollständigen Pfad angeben kann. Zum Auflösen der Abhängigkeiten von Binärdateien wird im grundlegenden Windows-Anwendungsmodell versucht, die Dateien in einem Suchpfad zu finden. Dies ist eine Liste von Speicherorten, die nacheinander nach einer Datei mit dem verlangten Grundnamen abgesucht werden. Dieser Suchpfadmechanismus kann jedoch von verschiedenen Systemkomponenten überschrieben werden, um das reguläre Anwendungsmodell zu erweitern. Das Prinzip des Suchpfads stammt noch aus dem Zeitalter der Befehlszeile, als der Begriff des »aktuellen Verzeichnisses« einer Anwendung tatsächlich eine sinnvolle Bedeutung hatte. Für moderne GUI-Anwendungen wirkt er dagegen etwas anachronistisch. Da sich das aktuelle Verzeichnis in diesem Suchpfad befand, konnten Angreifer jedoch die Ladeoperationen für Systembinärdateien überschreiben, indem sie schädliche Binärdateien desselben Namens in das aktuelle Verzeichnis der Anwendung stellten. Diese Technik wurde als Binary Planting bezeichnet. Um dieses Sicherheitsrisiko zu vermeiden, wurde die Such pfadberechnung um den sicheren DLL-Suchmodus ergänzt, der für alle Prozesse standardmäßig aktiviert ist. In diesem Modus wird das aktuelle Verzeichnis hinter die drei Systemverzeichnisse verlagert, was zu der folgenden Suchpfadreihenfolge führt: 1. Das Verzeichnis, in dem die Anwendung gestartet wurde 2. Das native Windows-Systemverzeichnis (z. B. C:\Windows\System32) 3. Da 16-Bit-Windows-Systemverzeichnis (z. B. C:\Windows\System) 4. Das Windows-Verzeichnis (z. B. C:\Windows) Der Abbildlader Kapitel 3 179
5. Das aktuelle Verzeichnis zum Startzeitpunkt der Anwendung 6. Jegliche in der Umgebungsvariablen %PATH% angegebenen Verzeichnisse Der DLL-Suchpfad wird für jede DLL-Ladeoperation neu berechnet. Der Algorithmus für diese Berechnung ist derselbe wie für den Standardsuchpfad, aber die Anwendung kann einzelne Pfadelemente überschreiben, indem sie die Variable %PATH% mit SetEnvironmentVariable bearbeitet, das aktuelle Verzeichnis mit SetCurrentDirectory ändert oder mit SetDllDirectoryAPI ein DLL-Verzeichnis für den Prozess angibt. In letzterem Fall ersetzt das DLL-Verzeichnis das aktuelle Verzeichnis im Suchpfad. Der Lader ignoriert dann die Einstellung des sicheren DLL-Suchmodus für den Prozess. Aufrufer können den DLL-Suchpfad auch für einzelne Ladeoperationen ändern, indem sie der API LoadLibraryEx das Flag LOAD_WITH_ALTERED_SEARCH_PATH übergeben. Ist dieses Flag gesetzt und enthält der an die API übergebene DLL-Name den String für einen vollständigen Pfad, dann wird bei der Berechnung des Suchpfads für die Operation der Pfad mit der DLL-Datei anstelle des Anwendungsverzeichnisses verwendet. Bei einem relativen Pfad ist das Verhalten nicht definiert, was gefährlich sein kann. Beim Laden von Desktop-Bridge-Anwendungen (Centennial) wird dieses Flag ignoriert. Neben LOAD_WITH_ALTERED_SEARCH_PATH können Anwendungen auch die Flags LOAD_ LIBRARY_SEARCH_DLL_LOAD_DIR, LOAD_LIBRARY_SEARCH_APPLICATION_DIR, LOAD_LIBRARY_SEARCH_ SYSTEM32 und LOAD_LIBRARY_SEARCH_USER_DIRS an LoadLibraryEx übergeben. Sie alle ändern die Suchreihenfolge, sodass nur in den Verzeichnissen gesucht wird, auf die sie verweisen. Beispielsweise wird bei LOAD_LIBRARY_SEARCH_DEFAULT_DIRS im Anwendungsverzeichnis, in System32 und im Benutzerverzeichnis gesucht. Die Flags können auch kombiniert werden, um mehrere Speicherorte zu durchsuchen. Mit SetDefaultDllDirectories können die Flags auch global gesetzt werden, sodass sie sich auf alle zukünftigen Bibliotheksladevorgänge auswirken. Bei Paketanwendungen und bei Anwendungen, die weder ein Paketdienst noch eine veraltete Anwendung für Silverlight 8.0 auf Windows Phone sind, gibt es noch eine weitere Möglichkeit, um die Suchreihenfolge zu beeinflussen. Unter diesen Umständen werden zur DLL-Suche nicht der herkömmliche Mechanismus und die APIs herangezogen, sondern eine paketgestützte Graphensuche. Das gilt auch, wenn statt der regulären Funktion Load LibraryEx die API LoadPackagedLibrary verwendet wird. Der paketgestützte Graph basiert auf den <PackageDependency>-Einträgen im Abschnitt <Dependencies> der Manifestdatei einer UWP-Anwendung und garantiert, dass in dem Paket keine willkürlichen DLLs geladen werden können. Beim Laden einer Paketanwendung (mit Ausnahme von Desktop-Bridge-Anwendungen) werden alle von der Anwendung konfigurierbaren APIs zur Änderung des DLL-Suchpfads (wie diejenigen, die Sie zuvor gesehen haben) deaktiviert. Nur das Standardverhalten des Systems wird verwendet (wobei für die meisten UWP-Anwendungen auch wie zuvor beschrieben die Paketabhängigkeiten untersucht werden). Doch selbst mit dem sicheren Suchmodus und den Standardpfad-Suchalgorithmen für ältere Anwendungen, bei denen das Anwendungsverzeichnis immer an erster Stelle steht, ist es leider immer noch möglich, dass eine Binärdatei von ihrem üblichen Speicherort an einen kopiert wird, der für die Benutzer zugänglich ist (beispielsweise von C:\Windows\System32\ Notepad.exe in C:\temp\Notepad.exe, wozu nicht einmal Administratorrechte erforderlich 180 Kapitel 3 Prozesse und Jobs
sind). In einer solchen Situation kann ein Angreifer eine besonders gestaltete DLL im Verzeichnis der Anwendung platzieren, wo sie aufgrund der zuvor aufgezeigten Reihenfolge Vorrang vor der System-DLL hat. Dies lässt sich dann zur Persistenz oder zu einer anderen Beeinflussung der Anwendung nutzen, die möglicherweise höhere Rechte hat (vor allem wenn der Benutzer, der nichts von dem Austausch ahnt, ihre Rechte über die Benutzerkontensteuerung erhöht hat). Als Schutz dagegen können Prozesse und Administratoren die Prozessvorbeugungsrichtlinie Prefer System32 Images nutzen (siehe Kapitel 7), die die Reihenfolge von Punkt 1 und 2 umkehrt. DLL-Namensumleitung Bevor sich der Lader daranmacht, einen DLL-Namensstring zu einer Datei aufzulösen, versucht er, Regeln zur DLL-Namensumleitung anzuwenden. Diese Regeln dienen dazu, Teile des DLL-Namensraums – der gewöhnlich dem Namensraum des Win32-Dateisystems entspricht – zu erweitern oder zu überschreiben, um damit das Windows-Anwendungsmodell zu erweitern. In der Reihenfolge der Anwendung sind dies folgende Regeln: QQ MinWin-API-Set-Umleitung Der Mechanismus der API-Sets beruht auf dem Prinzip von Kontrakten. Damit können verschiedene Versionen oder Editionen von Windows die Binärdatei, die eine bestimmte System-API exportiert, für die Anwendungen transparent ändern. Dieser Mechanismus wurde bereits kurz in Kapitel 2 erwähnt und wird in einem Abschnitt weiter hinten ausführlicher erklärt. QQ .LOCAL-Umleitung Mit dem .LOCAL-Umleitungsmechanismus können Anwendungen alle Ladevorgänge für einen gegebenen DLL-Basisnamen zu einer lokalen Kopie der DLL im Anwendungsverzeichnis umleiten, und zwar unabhängig davon, ob ein vollständiger Pfad angegeben wurde oder nicht. Dazu wird entweder eine Kopie der DLL mit demselben Basisnamen und der Endung .local erstellt (z. B. MyLibrary.dll.local) oder ein Dateiordner namens .local im Anwendungsverzeichnis erstellt und eine Kopie der lokalen DLL dort hineingelegt (z. B. C:\\MyApp\.LOCAL\MyLibrary.dll). Die von diesem Mechanismus umgeleiteten DLLs werden genauso behandelt wie die von SxS umgeleiteten (siehe den nächsten Punkt). Der Lader berücksichtigt die .LOCAL-Umleitung von DLLs nur, wenn die ausführbare Datei kein Manifest hat, weder ein eingebettetes noch ein externes. Diese Regel ist nicht standardmäßig aktiviert. Um sie global einzuschalten, fügen Sie den DWORD-Wert DevOverrideEnable zum IFEO-Schlüssel hinzu (HKLM\Software\Microsoft\ WindowsNT\CurrentVersion\Image File Execution Options) und setzen ihn auf 1. QQ Fusion-Umleitung (SxS) Fusion (auch Side-by-Side oder SxS genannt) ist eine Erweiterung des Windows-Anwendungsmodells, die es Komponenten ermöglicht, ausführlichere Informationen über Abhängigkeiten von Binärdateien auszudrücken (meistens Versionsinformationen). Dazu werden als Manifeste bezeichnete Binärressourcen eingebettet. Der Fusion-Mechanismus wurde ursprünglich verwendet, damit Anwendungen die richtige Version des Pakets Windows Common Controls (Comctl32.dll) laden konnten, nachdem die Binärdatei auf zwei verschiedene Versionen aufgeteilt worden war, die sich nebeneinander installieren ließen. Inzwischen wurde eine ähnliche Versionsverwaltung auch für andere Binärdateien eingeführt. In Visual Studio 2005 mit dem Microsoft-Linker erstellte Der Abbildlader Kapitel 3 181
Anwendungen verwenden Fusion, um die richtige Version der C-Laufzeitbibliotheken zu finden, während ab Visual Studio 2015 eine API-Set-Umleitung eingesetzt wird, um eine universelle CRT umzusetzen. Das Fusion-Laufzeitwerkzeug liest mithilfe des Windows Ressourcenladers die eingebetteten Abhängigkeitsinformationen im Ressourcenabschnitt der Binärdatei und verpackt sie in Nachschlagestrukturen, die als Aktivierungskontexte bezeichnet werden. Beim Systemund Prozessstart erstellt das System Aktivierungskontexte auf System- bzw. Prozessebene. Außerdem ist mit jedem Thread ein Aktivierungskontextstack verknüpft, wobei die Aktivierungskontextstruktur auf seiner Spitze als die aktive angesehen wird. Dieser threadweise Aktivierungskontextstack wird explizit über die APIs ActivateActCtx und DeactivacteActCtx und an einigen Stellen auch implizit vom System verwaltet, etwa wenn die DLL-Hauptroutine einer Binärdatei mit eingebetteten Abhängigkeitsinformationen aufgerufen wird. Beim Nachschlagen einer Fusion-DLL-Namensumleitung sucht das System nach Umleitungsinformationen in dem Aktivierungskontext oben auf dem Aktivierungskontextstack des Threads und danach nach den Prozess- und Systemaktivierungskontexten. Sind solche Umleitungsinformationen vorhanden, wird die in dem Aktivierungskontext angegebene Dateikennung für den Ladevorgang verwendet. QQ Umleitung zu bekannten DLLs (Known-DLL-Umleitung) Dieser Mechanismus ordnet bestimmte DLL-Basisnamen den Dateien im Systemverzeichnis zu, um zu verhindern, dass DLLs durch eine alternative Version an einem anderen Speicherort ersetzt werden. Die DLL-Versionsprüfung für 64-Bit- und Wow64-Anwendungen stellt einen Grenzfall des DLL-Suchpfadalgorithmus dar. Wenn eine DLL mit einem entsprechenden Basisnamen gefunden wurde, nachträglich aber erkannt wird, dass sie für die falsche Architektur kompiliert wurde – z. B. ein 64-Bit-Abbild in einer 32-Bit-Anwendung –, dann ignoriert der Lader den Fehler und nimmt die Suchpfadoperation wieder auf, wobei er mit dem Pfadelement hinter demjenigen beginnt, das zu der falschen Datei geführt hat. Dieses Verhalten erlaubt Anwendungen, in der globalen Umgebungsvariablen %PATH% sowohl 64- als auch 32-Bit-Einträge vorzunehmen. Experiment: Die DLL-Suchreihenfolge beobachten Mit dem Process Monitor von Sysinternals können Sie beobachten, wie der Lader nach DLLs sucht. Wenn er versucht, eine DLL-Abhängigkeit aufzulösen, ruft er CreateFile auf, um jeden Speicherort in der Suchreihenfolge zu sondieren, bis er entweder die angegebene DLL gefunden hat oder der Ladevorgang fehlschlägt. Die nachstehende Abbildung zeigt die Suche des Laders nach der ausführbaren Datei OneDrive.exe. Um dieses Experiment nachzuvollziehen, gehen Sie wie folgt vor: 1. Wenn OneDrive ausgeführt wird, schließen Sie es über sein Symbol in der Taskleiste. Schließen Sie alle Explorer-Fenster, die zurzeit auf OneDrive zugreifen. 2. Öffnen Sie Process Monitor und fügen Sie Filter hinzu, um nur den Prozess OneDrive. exe oder optional nur die CreateFile-Operationen anzuzeigen. → 182 Kapitel 3 Prozesse und Jobs
3. Wechseln Sie zu %LocalAppData%\Microsoft\OneDrive und starten Sie OneDrive.exe oder OneDrivePersonal.cmd (wodurch die Personal- statt der Business-Version von OneDrive gestartet wird). Sie sehen jetzt eine Ausgabe wie die folgende (beachten Sie dabei, dass OneDrive ein 32-Bit-Prozess ist, der hier auf einem 64-Bit-System läuft): Hier sind einige der Aufrufe aus dem zuvor beschriebenen Suchvorgang zu sehen: QQ Bekannte DLLs werden aus dem Systemspeicherort geladen (ole32.dll im Screenshot). QQ LoggingPlatform.dll wird aus einem Versionsunterverzeichnis geladen, wahrscheinlich, weil OneDrive SetDllDirectory aufruft, um Suchvorgänge zur letzten Version umzuleiten (17.3.6743.1212 im Screenshot). QQ Im Verzeichnis der ausführbaren Datei wird erfolglos nach MSVCR120.dll (MSVC-Laufzeitversion 12) gesucht. Anschließend wird die Suche im Versionsunterverzeichnis fortgesetzt, wo sich die DLL tatsächlich befindet. QQ Wsock32.dll (WinSock) wird erst im Pfad der ausführbaren Datei und dann im Versionsunterverzeichnis gesucht und schließlich im Systemverzeichnis gefunden (SysWow64). Beachten Sie, dass es sich hierbei nicht um eine bekannte DLL handelt. Die Datenbank der geladenen Module Der Lader führt eine Liste aller Module (DLLs und die primäre ausführbare Datei), die der Prozess geladen hat. Diese Informationen werden im PEB gespeichert, und zwar in einer Unterstruktur namens PEB_LDR_DATA. Dort unterhält der Lader drei doppelt verknüpfte Listen, die alle dieselben Informationen enthalten, jedoch in unterschiedlicher Anordnung (sortiert nach Laderreihenfolge, Speicherort und Initialisierungsreihenfolge). Die Strukturen in den Listen Der Abbildlader Kapitel 3 183
werden als Laderdaten-Tabelleneinträge (LDR_DATA_TABLE_ENTRY) bezeichnet und enthalten die Informationen über die einzelnen Module. Da Nachschlagevorgänge in verknüpften Listen algorithmisch kostspielig sind (aufgrund ihrer linearen Laufzeit), unterhält der Lader auch zwei Rot-Schwarz-Bäume, bei denen es sich um effiziente binäre Suchbäume handelt. Der erste ist nach Basisadressen sortiert, der zweite nach den Hashes der Modulnamen. Dank dieser Bäume kann der Suchalgorithmus mit logarithmischer Laufzeit ausgeführt werden, was erheblich effizienter ist und die Prozesserstellung in Windows 8 und höher beschleunigt. Als Sicherheitsmaßnahme sind die Wurzeln dieser beiden Bäume anders als verknüpfte Listen im PEB nicht zugänglich. Dadurch sind sie für Shellcode schwerer zu finden, der in einer Umgebung mit randomisiertem Adressraumlayout operiert (Address Space Layout Randomization, ASLR; siehe Kapitel 5). Tabelle 3–9 führt die Informationen auf, die der Lader in einem Eintrag unterhält. Feld Bedeutung BaseAddressIndexNode Verknüpft diesen Eintrag als Knoten mit dem nach Basisadressen sortierten Rot-Schwarz-Baum. BaseDllName/BaseNameHashValue Der Name des Moduls ohne vollständigen Pfad. Das zweite Feld enthält den mit RtlHashUnicodeString ermittelten Hash dieses Namens. DdagNode/NodeModuleLink Ein Zeiger auf die Datenstruktur, die den verteilten Abhängigkeitsgraphen (Distributed Dependency Graph, DDAG) im Auge behält. Dadurch wird das parallele Laden von Abhängigkeiten durch den Arbeitsthreadpool möglich. Das zweite Feld verknüpft die Struktur mit den damit verknüpften Laderdaten-Tabelleneinträgen (Teil desselben Graphen). DllBase Enthält die Basisadresse, an der das Modul geladen wurde. EntryPoint Enthält die ursprüngliche Routine des Moduls (z. B. DllMain). EntryPointActivationContext Enthält den SxS/Fusion-Aktivierungskontext beim Aufrufen von Initialisierern. Flags Laderstatusflags für das Modul (siehe Tabelle 3–10) ForwarderLinks Eine verknüpfte Liste der Module, die aufgrund von Exporttabellenweiterleitungen von dem Modul geladen wurden FullDllName Der vollständig qualifizierte Pfadname des Moduls HashLinks Eine verknüpfte Liste, die beim Starten und Herunterfahren des Prozesses verwendet wird, um das Nachschlagen zu beschleunigen ImplicitPathOptions Dient zur Speicherung der Pfadsuchflags, die mit der API LdrSetImplicitPathOptions festgelegt oder vom DLL-Pfad geerbt werden können. List Entry Links Verknüpft diesen Eintrag mit jeder der drei geordneten Listen in der Laderdatenbank. LoadContext Zeiger auf die aktuellen Laderinformationen über die DLL. Gewöhnlich NULL, sofern nicht aktiv geladen. ObsoleteLoadCount Referenzzähler für das Modul (also die Angabe, wie oft es geladen wurde). Dieser Zähler ist nicht mehr genau und wurde in die DDAG-Knotenstruktur verlagert. → 184 Kapitel 3 Prozesse und Jobs
Feld Bedeutung LoadReason Enthält einen Aufzählungswert, der erklärt, warum die DLL geladen wurde (dynamisch, statisch, als Forwarder, als verzögert geladene Abhängigkeit usw.). LoadTime Speichert den Wert der Systemzeit, zu der das Modul geladen wurde. MappingInfoIndexNode Verknüpft diesen Eintrag als Knoten mit dem nach Namenshashes sortierten Rot-Schwarz-Baum. OriginalBase Speichert die (vom Linker festgelegte) ursprüngliche Basis adresse des Moduls vor ASLR oder Relokalisierungen, was eine schnellere Verarbeitung relokalisierter Importeinträge ermöglicht. ParentDllBase Speichert bei statischen, Forwarder- oder verzögert geladenen Abhängigkeiten die Adresse der DLL, die von dieser DLL abhängt. SigningLevel Speichert die Signaturebene des Abbilds (siehe Kapitel 8 in Band 2). SizeOfImage Größe des Moduls im Arbeitsspeicher SwitchBackContext Wird von SwitchBack verwendet (siehe weiter hinten), um den aktuellen Windows-Kontext-GUID für das Modul und andere Daten zu speichern. TimeDateStamp Ein vom Linker geschriebener Zeitstempel, der den Verlinkungszeitpunkt des Moduls angibt. Der Lader ruft diesen Zeitstempel aus dem PE-Abbildheader des Moduls ab. TlsIndex Der TLS-Slot für das Modul Tabelle 3–9 Felder in einem Laderdaten-Tabelleneintrag Eine Möglichkeit, um die Laderdatenbank eines Prozesses zu untersuchen, besteht darin, sich die formatierte PEB-Ausgabe von WinDbg anzusehen. Im nächsten Experiment erfahren Sie, wie das gemacht wird und wie Sie die LDR_DATA_TABLE_ENTRY-Strukturen betrachten. Experiment: Die Datenbank der geladenen Module ausgeben Führen Sie vor Beginn dieses Experiments die gleichen Schritte wie bei den vorherigen beiden Versuchen aus, um Notepad.exe mit WinDbg als Debugger zu starten. Wenn Sie den ersten Haltepunkt erreichen (an dem Sie zuvor immer angewiesen wurden, (g) zu drücken), gehen Sie wie folgt vor: 1. Schauen Sie sich mit dem Befehl !peb den PEB des aktuellen Prozesses an. Zunächst sind wir nur an den angezeigten Ldr-Daten interessiert: 0:000> !peb PEB at 000000dd4c901000 InheritedAddressSpace: No ReadImageFileExecOptions: No BeingDebugged: Yes → Der Abbildlader Kapitel 3 185
ImageBaseAddress: 00007ff720b60000 Ldr 00007ffe855d23a0 Ldr.Initialized: Yes Ldr.InInitializationOrderModuleList: 0000022815d23d30 . 0000022815d24430 Ldr.InLoadOrderModuleList: 0000022815d23ee0 . 0000022815d31240 Ldr.InMemoryOrderModuleList: 0000022815d23ef0 . 0000022815d31250 Base TimeStamp Module 7ff720b60000 5789986a Jul 16 05:14:02 2016 C:\Windows\System32\ notepad.exe 7ffe85480000 5825887f Nov 11 10:59:43 2016 C:\WINDOWS\SYSTEM32\ ntdll.dll 7ffe84bd0000 57899a29 Jul 16 05:21:29 2016 C:\WINDOWS\System32\ KERNEL32.DLL 7ffe823c0000 582588e6 Nov 11 11:01:26 2016 C:\WINDOWS\System32\ KERNELBASE.dll ... 2. Die Adresse, die in der Ldr-Zeile gezeigt wird, ist ein Zeiger auf die zuvor beschriebene PEB_LDR_DATA-Struktur. WinDbg zeigt die Adressen der drei Listen an und gibt die Liste in der Initialisierungsreihenfolge aus, wobei für jedes Modul der vollständige Pfad, der Zeitstempel und die Basisadresse angegeben werden. 3. Sie können auch die einzelnen Moduleinträge jeweils für sich selbst analysieren, indem Sie die Modulliste durchgehen und dann die Daten an jeder Adresse in Form einer LDR_ DATA_TABLE_ENTRY-Struktur ausgeben. Anstatt das für jeden Eintrag selbst zu machen, können Sie sich den Großteil der Arbeit auch von WinDbg abnehmen lassen, indem Sie die Erweiterung !list und die folgende Syntax verwenden: !list –x "dt ntdll!_LDR_DATA_TABLE_ENTRY" @@C++(&@$peb->Ldr>InLoadOrderModuleList) 4. Nun sehen Sie die Einträge für die einzelnen Module: +0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x00000228'15d23d10 - +0x010 InMemoryOrderLinks : _LIST_ENTRY [ 0x00000228'15d23d20 - 0x00007ffe'855d23b0 ] 0x00007ffe'855d23c0 ] +0x020 InInitializationOrderLinks : _LIST_ENTRY [ 0x00000000'00000000 0x00000000'00000000 ] +0x030 DllBase : 0x00007ff7'20b60000 Void +0x038 EntryPoint : 0x00007ff7'20b787d0 Void +0x040 SizeOfImage : 0x41000 +0x048 FullDllName : _UNICODE_STRING "C:\Windows\System32\notepad.exe" +0x058 BaseDllName : _UNICODE_STRING "notepad.exe" 186 +0x068 FlagGroup : [4] "???" +0x068 Flags : 0xa2cc Kapitel 3 Prozesse und Jobs
In diesem Abschnitt geht es um den Benutzermodus-Lader aus Ntdll.dll. Der Kernel aber hat seinen eigenen Lader für Treiber und abhängige DLLs. Dieser Kernelmoduslader hat eine ähnliche Eintragsstruktur, die als KLDR_DATA_TABLE_ENTRY bezeichnet wird, und seine eigene Datenbank aus diesen Einträgen, die über die globale Datenvariable PsActiveModuleList direkt zugänglich ist. Um die Datenbank der geladenen Module des Kernels auszugeben, können Sie einen ähnlichen !list-Befehl wie im vorstehenden Experiment verwenden, wobei Sie allerdings den Zeiger am Ende des Befehls durch nt!PsActiveModuleList ersetzen und den neuen Struktur- bzw. Modulnamen angeben müssen, also !list nt!_KLDR_DATA_TABLE_ENTRY nt!PsActiveModuleList. Wenn Sie sich die Liste in diesem rohen Format ansehen, erhalten Sie einige besondere Einblicke in die Interna des Laders. So sehen Sie dabei beispielsweise das Feld Flags mit Statusinformationen, die Ihnen der reine !peb-Befehl nicht anzeigt. Die Bedeutung dieser Flags finden Sie in Tabelle 3–10. Da sowohl der Kernel- als auch der Benutzermoduslader diese Struktur verwendet, können die Flags unterschiedliche Bedeutung haben. In der Tabelle werden lediglich die Benutzermodusflags beschrieben (von denen einige auch in der Kernelstruktur vorhanden sein können). Flag Bedeutung Packaged Binary (0x1) Das Modul ist Teil einer Paketanwendung (kann nur für das Hauptmodul eines AppX-Pakets gesetzt werden). Marked for Removal (0x2) Das Modul wird entladen, sobald alle Referenzen (z. B. von einem ausführenden Arbeitsthread) entfernt wurden. Image DLL (0x4) Das Modul ist eine Abbild-DLL (also keine Daten-DLL und keine ausführbare Datei). Load Notifications Sent (0x8) Die registrierten DLL-Benachrichtigungscallouts wurden bereits über dieses Abbild benachrichtigt. Telemetry Entry Processed (0x10) Telemetriedaten wurden bereits für dieses Abbild verarbeitet. Process Static Import (0x20) Das Modul ist ein statischer Import der binären Hauptdatei der Anwendung. In Legacy Lists (0x40) Der Abbildeintrag befindet sich in den doppelt verknüpften Listen des Laders. In Indexes (0x80) Der Abbildeintrag befindet sich in den Rot-Schwarz-Bäumen des Laders. Shim DLL (0x100) Der Abbildeintrag steht für den DLL-Teil der Shim-Engine-/ Anwendungskompatibilitätsdatenbank. In Exception Table (0x200) Die .pdata-Ausnahmehandler des Moduls wurden von der invertierten Funktionstabelle des Laders erfasst. Load In Progress (0x800) Das Modul wird zurzeit geladen. Load Config Processed (0x1000) Das Ladekonfigurationsverzeichnis des Moduls wurde gefunden und verarbeitet. Entry Processed (0x2000) Der Lader hat die Verarbeitung des Moduls komplett abgeschlossen. Protect Delay Load (0x4000) ControlFlowGuard-Merkmale für diese Binärdatei haben den Schutz der Ladeverzögerungs-IAT angefordert (siehe Kapitel 7). Der Abbildlader Kapitel 3 → 187
Flag Bedeutung Process Attach Called (0x20000) Die Benachrichtigung DLL_PROCESS_ATTACH wurde bereits an die DLL gesendet. Process Attach Failed (0x40000) Bei der DllMain-Routine der DLL ist die Benachrichtigung DLL_PROCESS_ATTACH fehlgeschlagen. Don't Call for Threads (0x80000) Zu dieser DLL soll keine DLL_THREAD_ATTACH/DETACH-Benachrichtigung gesendet werden. Dieses Flag kann mit Disable ThreadLibraryCalls gesetzt werden. COR Deferred Validate (0x100000) Die Common Object Runtime (COR) wird dieses .NET-Abbild später validieren. COR Image (0x200000) Das Modul ist eine .NET-Anwendung. Don't Relocate (0x400000) Das Abbild darf weder relokalisiert noch randomisiert werden. COR IL Only (0x800000) Dies ist eine reine (Abbildlader-) .NET-Zwischensprachenbibliothek, die keinen nativen Assemblycode enthält. Compat Database Processed (0x40000000) Die Shim-Engine hat die DLL verarbeitet. Tabelle 3–10 Flags für Laderdaten-Tabelleneinträge Importanalyse Da Sie jetzt wissen, wie der Lader den Überblick über alle für einen Prozess geladenen Module behält, können Sie nun mit der Analyse der vom Lader durchgeführten Startinitialisierungsaufgaben fortfahren. In diesem Schritt erledigt der Lader Folgendes: 1. Er lädt die in der Importtabelle des ausführbaren Abbilds für den Prozess angegebenen DLLs. 2. Er prüft jeweils, ob die DLL bereits geladen wurde, indem er die Moduldatenbank abfragt. Steht die DLL nicht auf der Liste, öffnet der Lader sie und ordnet sie im Arbeitsspeicher zu. 3. Während der Zuordnung schaut sich der Lader zunächst die verschiedenen Pfade an, in denen die DLL zu finden sein könnte, und prüft, ob es sich um eine bekannte DLL handelt, also um eine, die das System bereits beim Start geladen und für die es eine im Arbeitsspeicher zugeordnete globale Datei für den Zugriff bereitgestellt hat. Es können dabei Abweichungen vom normalen Suchalgorithmus vorkommen, beispielsweise durch eine .local-Datei, die den Lader zwingt, die DLLs im lokalen Pfad zu verwenden, oder eine Manifestdatei, die eine umgeleitete DLL angibt, um die Verwendung einer bestimmten Version zu garantieren. 4. Nachdem der Lader die DLL auf der Festplatte gefunden und zugeordnet hat, prüft er, ob der Kernel sie an anderer Stelle geladen hat (Relokalisierung). Ist das der Fall, durchsucht der Lader die Relokalisierungsinformationen in der DLL und führt die erforderlichen Operationen aus. Stehen keine Relokalisierungsinformationen bereit, schlägt das Laden der DLL fehl. 5. Anschließend erstellt der Lader einen Laderdaten-Tabelleneintrag für die DLL und führt ihn in die Datenbank ein. 188 Kapitel 3 Prozesse und Jobs
6. Nach der Zuordnung einer DLL wird der Vorgang für die DLL wiederholt, um ihre Importtabelle und ihre gesamten Abhängigkeiten zu analysieren. 7. Nachdem alle DLLs geladen wurden, untersucht der Lader die IAT auf besondere Funktionen, die importiert wurden. Dies geschieht gewöhnlich nach Namen, kann aber auch nach Indexzahlen geschehen. Für jeden Namen durchläuft der Lader die Exporttabelle der importierten DLL und versucht, eine Übereinstimmung zu finden. Gibt es keine Übereinstimmung, wird die Operation abgebrochen. 8. Auch die Importtabelle eines Abbilds kann gebunden werden. Das bedeutet, dass die Entwickler zur Verlinkungszeit statische Adressen zuweisen, die auf importierte Funktionen in externen DLLs zeigen. Dadurch entfällt die Notwendigkeit, jeden Namen nachzuschlagen, allerdings wird vorausgesetzt, dass sich die DLLs, auf die die Anwendung zurückgreift, immer an derselben Adresse befinden. Da Windows eine Adressraumrandomisierung durchführt (siehe Kapitel 5), ist dies für Systemanwendungen und Bibliotheken gewöhnlich nicht der Fall. 9. Die Exporttabelle einer importierten DLL kann einen Forwarder-Eintrag haben, was bedeutet, dass die eigentliche Funktion in einer anderen DLL implementiert ist. Dieser Fall muss im Grunde genommen wie ein Import oder eine Abhängigkeit behandelt werden. Nach der Untersuchung der Exporttabelle muss daher jede DLL geladen werden, auf die ein Forwarder verweist. Der Lader kehrt danach zu Schritt 1 zurück. Nachdem alle importierten DLLs (und ihre eigenen Abhängigkeiten oder Importe) geladen, alle erforderlichen importierten Funktionen nachgeschlagen und gefunden und auch alle Forwarder geladen und verarbeitet wurden, ist der Schritt abgeschlossen: Alle von der Anwendung und ihren DLLs zur Kompilierungszeit definierten Abhängigkeiten sind erfüllt. Bei der Ausführung können verzögert geladene Abhängigkeiten und Laufzeitoperationen (wie der Aufruf von LoadLibrary) den Lader aufrufen und praktisch die gleichen Aufgaben erneut ausführen. Wenn diese Schritte beim Start des Prozesses ausgeführt werden, führt ein Fehlschlag dabei jedoch zu einem Fehler beim Starten der Anwendung. Wenn Sie beispielsweise versuchen, eine Anwendung auszuführen, die eine in der aktuellen Version des Betriebssystems nicht vorhandene Funktion benötigt, kann das zu einer Meldung wie der aus Abb. 3–12 führen. Abbildung 3–12 Das Dialogfeld, das erscheint, wenn eine erforderliche (importierte) Funktion in einer DLL nicht vorhanden ist Der Abbildlader Kapitel 3 189
Prozessinitialisierung nach dem Import Nachdem die erforderlichen Abhängigkeiten geladen worden sind, müssen noch verschiedene Initialisierungsaufgaben ausgeführt werden, bevor die Anwendung endlich gestartet werden kann. In dieser Phase erledigt der Lader Folgendes: 1. Diese Schritte beginnen an der Stelle, an der die Variable LdrInitState auf 2 gesetzt wird, was bedeutet, dass die Importe geladen sind. 2. Wenn Sie einen Debugger wie WinDbg benutzen, trifft er schließlich auf den ersten Haltepunkt. Hier müssen Sie wie in den früheren Experimenten (g) drücken, um mit der Ausführung fortzufahren. 3. Es wird geprüft, ob es sich um eine Anwendung des Windows-Teilsystems handelt. Wenn ja, sollte in den ersten Schritten zur Prozessinitialisierung die Funktion BaseThreadInitThunk erfasst worden sein. Jetzt wird sie aufgerufen und auf Erfolg geprüft. Auch die Funktion TermsrvGetWindowsDirectoryW, die (auf einem Gerät mit Unterstützung für Terminaldienste) bereits erfasst worden sein sollte, wird jetzt aufgerufen und setzt den System- und den Windows-Verzeichnispfad zurück. 4. Mithilfe des Verteilungsgraphen durchläuft der Lader rekursiv alle Abhängigkeiten und führt die Initialisierer für alle statischen Importe des Abbilds aus. Bei diesem Schritt wird die Routine DllMain für jede DLL aufgerufen, sodass jede DLL ihre eigenen Initialisierungsarbeiten durchführen kann, wozu auch das Laden weiterer DLLs zur Laufzeit gehören kann. Außerdem werden die TLS-Initialisierer der einzelnen DLLs verarbeitet. Dies ist einer der letzten Schritte, bei denen das Laden einer Anwendung fehlschlagen kann. Sämtliche geladenen DLLs müssen nach dem Abschluss ihrer DllMain-Routinen einen Erfolgscode zurückgeben. Anderenfalls bricht der Lader den Start der Anwendung ab. 5. Falls das Abbild TLS-Slots verwendet, wird sein TLS-Initialisierer aufgerufen. 6. Der Lader führt den Postinitialisierungs-Callback für die Shim-Engine aus, wenn das Modul aus Kompatibilitätsgründen Shims verwendet. 7. Der Lader führt die Postprozessinitialisierungsroutine für die zugehörige Teilsystem-DLL aus, die im PEB registriert ist. Bei Windows-Anwendungen werden dabei beispielsweise Überprüfungen für Terminaldienste ausgeführt. 8. Jetzt wird ein ETW-Ereignis geschrieben, um anzugeben, dass der Prozess erfolgreich geladen wurde. 9. Wenn es eine Mindestzusicherung für den Stack gibt, wird der Threadstack berührt, um eine Einlagerung der zugesicherten Seiten zu erzwingen. 10. LdrInitState wird auf 3 gesetzt, was bedeutet, dass die Initialisierung abgeschlossen ist. Anschließend wird das Feld ProcessInitializing des PEB auf 0 zurückgesetzt und die Variable LdrpProcessInitialized aktualisiert. 190 Kapitel 3 Prozesse und Jobs
SwitchBack In jeder neuen Version von Windows werden Bugs wie Race Conditions und falsche Parametervalidierung in vorhandenen API-Funktionen korrigiert. Wie klein diese Änderungen auch sein mögen, es besteht dabei immer die Gefahr von Inkompatibilitäten mit Anwendungen. Mithilfe der SwitchBack-Technologie, die im Lader implementiert ist, können Softwareentwickler jedoch im Manifest ihrer ausführbaren Dateien einen GUID für die Windows-Version einbetten, für die sie ihre Anwendung geschrieben haben. Nehmen wir an, ein Entwickler möchte die Verbesserungen nutzen, die in Windows 10 zu einer bestimmten API hinzugefügt wurden. Er könnte dann in dem Manifest den GUID von Windows 10 angeben. Bei einer älteren Anwendung, die von einem bestimmten Verhalten von Windows 7 abhängt, würde er dagegen den Windows 7-GUID in das Manifest aufnehmen. SwitchBack durchsucht diese Angaben und vergleicht sie mit den eingebetteten Informa tionen in SwitchBack-kompatiblen DLLs (im Abschnitt .sb_data des Abbilds), um zu bestimmen, welche Version der betreffenden API das Modul aufrufen soll. Da SwitchBack auf der Ebene der geladenen Module arbeitet, ist es dadurch möglich, dass in einem Prozess ältere und aktuelle DLLs dieselbe API aufrufen und dabei unterschiedliche Ergebnisse erzielen. SwitchBack-GUIDs Zurzeit sind GUIDs für die Kompatibilität mit allen Versionen ab Windows Vista definiert: QQ {e2011457-1546-43c5-a5fe-008deee3d3f0} für Windows Vista QQ {35138b9a-5d96-4fbd-8e2d-a2440225f93a} für Windows 7 QQ {4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38} für Windows 8 QQ {1f676c76-80e1-4239-95bb-83d0f6d0da78} für Windows 8.1 QQ {8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a} für Windows 10 Diese GUIDs müssen in der Manifestdatei der Anwendung unter dem Abschnitt <SupportedOS> im ID-Attribut des Eintrags für das Kompatibilitätsattribut vorhanden sein. Enthält das Manifest keinen GUID, wird als Standardkompatibilitätsmodus Windows Vista ausgewählt. Auf der Registerkarte Details des Task-Managers können Sie die Spalte Betriebssystemkontext einblenden, in der angezeigt wird, ob eine Anwendung in einem bestimmten Betriebssystemkontext läuft. Ein leerer Wert bedeutet dabei gewöhnlich, dass die Anwendung im Windows 10-Modus ausgeführt wird. Abb. 3–13 zeigt einige Beispielanwendungen, die auf einem Windows 10-System im Windows Vista- bzw. Windows 7-Modus ausgeführt werden. Im folgenden Beispielmanifest ist die Kompatibilität auf Windows 10 festgelegt: <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> <application> <!-- Windows 10 --> <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" /> </application> </compatibility> Der Abbildlader Kapitel 3 191
Abbildung 3–13 Prozesse mit abweichenden Kompatibilitätsmodi SwitchBack-Kompatibilitätsmodi Um Ihnen eine Vorstellung davon zu geben, was SwitchBack tun kann, sehen Sie hier, was die Ausführung im Windows 7-Kontext mit sich bringt: QQ RPC-Komponenten verwenden den Windows-Threadpool statt einer privaten Implementierung. QQ Für den primären Puffer kann keine DirectDraw-Sperre eingerichtet werden. QQ Blitting auf dem Desktop ist ohne Clippingfenster nicht erlaubt. QQ Eine Race Condition in GetOverlappedResult ist korrigiert. QQ Aufrufe von CreateFile dürfen ein Herabstufungsflag übergeben, um eine Datei auch dann exklusiv zu öffnen, wenn der Aufrufer keine Schreibrechte hat. Infolgedessen erhält NtCreateFile nicht das Flag FILE_DISALLOW_EXCLUSIVE. Die Ausführung im Windows 10-Modus dagegen betrifft das LFH-Verhalten (Low Fragmentation Heap). Ist kein Windows 10-GUID angegeben, werden alle LFH-Teilsegmente zwangsweise zugesichert und alle Zuweisungen mit einem Headerblock aufgefüllt. In Windows 10 wird 192 Kapitel 3 Prozesse und Jobs
zusätzlich die Vorbeugemaßnahme Raise Exception on Invalid Handle Close (siehe Kapitel 7) verwendet, sodass CloseHandle und RegCloseKey das Verhalten respektieren. Bei früheren Betriebssystemen dagegen wird dieses Verhalten vor dem Aufruf von NtClose deaktiviert und anschließend wieder aktiviert (sofern kein Debugger angeschlossen ist). Ein weiteres Beispiel ist die Einrichtung zur Rechtschreibprüfung, die für Sprachen ohne Rechtschreibprüfung NULL zurückgibt, in Windows 8.1 aber eine »leere« Rechtschreibprüfung. Die Implementierung der Funktion IShellLink::Resolve gibt bei der Übergabe eines relativen Pfads im Windows 8-Kompatibilitätsmodus E_INVALIDARG zurück, wohingegen die entsprechende Überprüfung im Windows 7-Modus nicht stattfindet. Aufrufe von GetVersionEx oder ähnlichen NtDll-Funktionen wie RtlVerifyVersionInfo geben die höchste zurück, die mit der angegebenen SwitchBack-Kontext-GUID übereinstimmt. Diese APIs sind veraltet und mittlerweile unerwünscht. Aufrufe von GetVersionEX geben jetzt auf allen Versionen von Windows 8 und höher 6.2 zurück, sofern kein höherer SwitchBack-GUID angegeben wurde. SwitchBack-Verhalten Bei Änderungen an einer Windows-API, die die Kompatibilität gefährden können, ruft der Eintrittscode die Funktion SbSwitchProcedure auf, um die SwitchBack-Logik zu starten. Dabei übergibt er einen Zeiger auf die SwitchBack-Modultabelle, die Informationen über den von dem Modul genutzten SwitchBack-Mechanismus sowie einen Zeiger auf ein Array mit Einträgen für die einzelnen SwitchBack-Punkte enthält Die Verzweigungspunkte werden dabei mit symbolischen Namen bezeichnet und mit einer ausführlichen Beschreibung und einem zugehörigen Vorbeugetag versehen. Gewöhnlich gibt es in einem Modul mehrere Verzweigungspunkte – einen für Windows Vista-Verhalten, einen für Windows 7-Verhalten usw. Für jeden Verzweigungspunkt wird der erforderliche SwitchBack-Kontext angegeben, der bestimmt, welche der Verzweigungen zur Laufzeit genommen wird. Die einzelnen Deskriptoren enthalten auch jeweils einen Funktionszeiger auf den Code, der in der Verzweigung ausgeführt werden soll. Läuft die Anwendung mit dem Windows 10-GUID, gehört dieser Zeiger zum SwitchBack-Kontext. Bei der Analyse der Modultabelle führt die API SbSelectProcedure einen Abgleich durch. Sie findet den Moduleintragsdeskriptor für den Kontext und ruft den darin enthaltenen Funktionszeiger auf. SwitchBack nutzt ETW, um die Auswahl des SwitchBack-Kontexts und der Verzweigungspunkte zu verfolgen und die Daten in den AIT-Logger von Windows einzuspeisen (Applica tion Impact Telemetry). Die Daten können regelmäßig von Microsoft abgefragt werden, um zu bestimmen, in welchem Umfang die einzelnen Kompatibilitätseinträge genutzt werden, die entsprechenden Anwendungen zu identifizieren (das Protokoll enthält eine vollständige Stack ablaufverfolgung) und Drittanbieterhersteller zu informieren. Wie bereits erwähnt, wird die Kompatibilitätsstufe der Anwendung in deren Manifest gespeichert. Wenn der Lader die Manifestdatei zum Ladezeitpunkt analysiert, erstellt er eine Kontextdatenstruktur und speichert sie im Element pShimData des PEB zwischen. Diese Kontextdaten enthalten die Kompatibilitäts-GUIDs, mit denen der Prozess ausgeführt wird, und Der Abbildlader Kapitel 3 193
bestimmen, welche Version der Verzweigungspunkte in den aufgerufenen SwitchBack-fähigen APIs ausgeführt wird. API-Sets SwitchBack verwendet eine API-Umleitung, um für Anwendungskompatibilität zu sorgen. Es gibt jedoch auch einen allgemeineren Umleitungsmechanismus, der in Windows für alle Anwendungen verwendet wird – die API-Sets. Sie ermöglichen eine feine Zuordnung von Windows-APIs zu Teil-DLLs, sodass es nicht nötig ist, riesige Mehrzweck-DLLs mit Tausenden von APIs zu handhaben, die bei den heutigen und zukünftigen Windows-Systemen nicht alle erforderlich sind. Diese Technologie wurde hauptsächlich entwickelt, um das Refactoring der untersten Schichten der Windows-Architektur zu ermöglichen und diese besser von den höheren Schichten zu trennen. Sie geht Hand in Hand mit der Aufteilung von u. a. Kernel32.dll und Advapi32.dll in mehrere virtuelle DLL-Dateien. So zeigt der Dependency-Walker-Screenshot aus Abb. 3–14, wie die Windows-Kernbibliothek Kernel32.dll Funktionen aus vielen anderen DLLs importiert, beginnend mit API-MS-WIN. Jede dieser DLLs enthält einen kleinen Teil der APIs, die Kernel32 normalerweise bereitstellt. Zusammen bilden sie die gesamte API-Oberfläche, die von Kernel32.dll verfügbar gemacht wird. Beispielsweise trägt die Bibliothek CORE-STRING nur die grundlegenden Stringfunktionen von Windows bei. Abbildung 3–14 API-Sets für Kernel32.dll 194 Kapitel 3 Prozesse und Jobs
Durch die Aufteilung der Funktionen auf mehrere Dateien werden zwei Ziele erreicht: Erstens sind künftige Anwendungen dadurch in der Lage, eine Verlinkung nur mit den API-Bibliotheken herzustellen, deren Funktionen sie tatsächlich benötigen. Zweitens: Nehmen wir an, Microsoft wollte eine Windows-Version herausgeben, die eine bestimmte Funktionalität nicht unterstützt, beispielsweise die Lokalisierung (etwa ein rein englisches eingebettetes System ohne Benutzerinteraktion). In diesem Fall könnte das Unternehmen einfach die entsprechenden Teil-DLLs herausnehmen und das Schema der API-Sets ändern. Dadurch würde die Binärdatei Kernel32 kleiner, und jegliche Anwendungen, die keine Lokalisierung benötigen, könnten immer noch ausgeführt werden. Mithilfe dieser Technologie wurde das »grundlegende« Windows-System MinWin definiert (und der Quellcode dafür erstellt). Es enthält nur ein Minimum an Diensten, zu denen der Kernel und die Kerntreiber gehören (einschließlich Dateisystemen, grundlegenden Systemprozessen wie Csrss und dem Dienststeuerungs-Manager sowie einer Handvoll Windows-Dienste). Windows Embedded und der Platform Builder bieten eine scheinbar ähnliche Technologie, da Systementwickler hier in der Lage sind, einzelne Windows-Komponenten wie die Shell oder den Netzwerkstack zu entfernen. Dadurch aber bleiben hängende Abhängigkeiten zurück, also Codepfade, die fehlschlagen, wenn sie ausgeführt werden, da sie die entfernten Komponenten benötigen. Die Abhängigkeiten von MinWin sind dagegen in sich abgeschlossen. Bei der Initialisierung ruft der Prozess-Manager die Funktion PspInitializeApiSetMap auf. Sie ist dafür zuständig, das Abschnittsobjekt der API-Set-Umleitungstabelle zu erstellen, die in %SystemRoot%\System32\ApiSetSchema.dll gespeichert ist. Diese DLL enthält keinen ausführbaren Code, verfügt aber über den Abschnitt .apiset mit den Daten zur Zuordnung der virtuellen API-Set-DLLs zu den logischen DLLs, die die APIs implementieren. Wird ein neuer Prozess gestartet, ordnet der Prozess-Manager das Abschnittsobjekt im Prozessadressraum zu und richtet das Feld ApiSetMap im PEB des Prozesses so ein, dass es auf die Basisadresse dieser Zuordnung zeigt. Die Laderfunktion LdrpApplyFileNameRedirection, die normalerweise für die weiter vorn beschriebene .local- und SxS/Fusion-Manifestumleitung zuständig ist, führt auch eine Überprüfung auf API-Set-Umleitungsdaten durch, wenn eine neue Importbibliothek geladen wird (dynamisch oder statisch), deren Name mit API beginnt. Die API-Set-Tabelle ist nach Bibliotheken geordnet, wobei jeder Eintrag beschreibt, in welcher logischen DLL die Funktion gefunden werden kann. Diese DLL ist es, die geladen wird. Die Schemadaten liegen zwar im Binärformat vor, doch mit dem Sysinternals-Programm Strings können Sie die zugehörigen Strings ausgeben, um sich anzusehen, welche DLLs zurzeit definiert sind: C:\Windows\System32>strings apisetschema.dll ... api-ms-onecoreuap-print-render-l1-1-0 printrenderapihost.dllapi-ms-onecoreuap-settingsync-status-l1-1-0 settingsynccore.dll api-ms-win-appmodel-identity-l1-2-0 kernel.appcore.dllapi-ms-win-appmodel-runtime-internal-l1-1-3 api-ms-win-appmodel-runtime-l1-1-2 api-ms-win-appmodel-state-l1-1-2 Der Abbildlader Kapitel 3 195
api-ms-win-appmodel-state-l1-2-0 api-ms-win-appmodel-unlock-l1-1-0 api-ms-win-base-bootconfig-l1-1-0 advapi32.dllapi-ms-win-base-util-l1-1-0 api-ms-win-composition-redirection-l1-1-0 ... api-ms-win-core-com-midlproxystub-l1-1-0 api-ms-win-core-com-private-l1-1-1 api-ms-win-core-comm-l1-1-0 api-ms-win-core-console-ansi-l2-1-0 api-ms-win-core-console-l1-1-0 api-ms-win-core-console-l2-1-0 api-ms-win-core-crt-l1-1-0 api-ms-win-core-crt-l2-1-0 api-ms-win-core-datetime-l1-1-2 api-ms-win-core-debug-l1-1-2 api-ms-win-core-debug-minidump-l1-1-0 ... api-ms-win-core-firmware-l1-1-0 api-ms-win-core-guard-l1-1-0 api-ms-win-core-handle-l1-1-0 api-ms-win-core-heap-l1-1-0 api-ms-win-core-heap-l1-2-0 api-ms-win-core-heap-l2-1-0 api-ms-win-core-heap-obsolete-l1-1-0 api-ms-win-core-interlocked-l1-1-1 api-ms-win-core-interlocked-l1-2-0 api-ms-win-core-io-l1-1-1 api-ms-win-core-job-l1-1-0 ... Jobs Ein Job (oder Auftrag) ist ein Kernelobjekt, das benannt, abgesichert und gemeinsam genutzt werden kann und es ermöglicht, eine Gruppe von Prozessen als eine Einheit zu verwalten und zu bearbeiten. Ein Prozess kann zwar zu beliebig vielen Jobs gehören, ist gewöhnlich aber nur Element eines einzigen. Die Verbindung eines Prozesses mit einem Job kann nicht aufgehoben werden, und alle von dem Prozess und seinen Abkömmlingen erstellten Prozesse werden mit demselben Jobobjekt verknüpft (es sei denn, die Kindprozesse wurden mit dem Flag CREATE_ BREAKAWAY_FROM_JOB erstellt und der Job hat dies nicht eingeschränkt). Das Jobobjekt zeichnet auch grundlegende Buchführungsinformationen für alle mit ihm verbundenen Prozesse auf, einschließlich derer, die bereits beendet wurden. 196 Kapitel 3 Prozesse und Jobs
Jobs können auch mit einem E/A-Abschlussportobjekt verbunden werden, auf das andere Threads mit der Windows-Funktion GetQueuedCompletionStatus oder durch Verwendung der Threadpool-API warten (native Funktion TpAllocJobNotification). Dadurch können daran interessierte Parteien (gewöhnlich der Ersteller des Jobs) nach Verletzungen der Grenzwerte und nach Ereignissen suchen, die die Sicherheit des Jobs gefährden, z. B. nach der Schaffung eines neuen Prozesses oder der unnatürlichen Beendigung eines Prozesses. Jobs spielen bei verschiedenen Systemmechanismen eine wichtige Rolle: QQ Sie verwalten moderne Apps (UWP-Prozesse), wie in Kapitel 9 von Band 2 noch genauer erklärt wird. Jede moderne App wird in einem Job ausgeführt. Das können Sie sich mithilfe von Process Explorer ansehen, was im Experiment »Das Jobobjekt anzeigen« weiter hinten in diesem Kapitel erklärt wird. QQ Sie dienen zur Implementierung der Windows-Unterstützung für Container. Das geschieht mithilfe des Mechanismus der Serversilos, der weiter hinten in diesem Abschnitt beschrieben wird. QQ Sie bilden die wichtigste Einrichtung, durch die der Desktop Activity Moderator (DAM) Drosselung, Timervirtualisierung, das Einfrieren von Timern und andere Leerlaufverhalten für Win32-Anwendungen und -Dienste verwaltet. Der DAM wird in Kapitel 8 von Band 2 beschrieben. QQ Sie ermöglichen die Definition und Verwaltung von Planungsgruppen für die in Kapitel 4 beschriebene dynamische gleichmäßige Planung (Dynamic Fair-Share Scheduling, DFSS). QQ Sie ermöglichen die Angabe einer benutzerdefinierten Speicherpartition, die wiederum die Verwendung der in Kapitel 5 beschriebenen API zur Speicherpartitionierung erlaubt. QQ Sie dienen als entscheidende Voraussetzung für Merkmale wie Ausführen als (sekundäre Anmeldung), Anwendungs-Boxing und den Programmkompatibilitäts-Assistenten. QQ Sie stellen einen Teil der Sicherheitssandbox für Anwendungen wie Google Chrome und den Microsoft Office-Dokumentkonverter sowie einen Schutz gegen DoS-Angriffe durch WMI-Anforderungen (Windows Management Instrumentation) bereit. Grenzwerte für Jobs Für einen Job können Sie unter anderem die folgenden CPU-, Speicher- und E/A-Grenzwerte festlegen: QQ Maximale Anzahl aktiver Prozesse Schränkt die Anzahl der gleichzeitig vorhandenen Prozesse im Job ein. Sobald dieser Grenzwert erreicht ist, wird die Erstellung neuer Prozesse, die diesem Job zugewiesen werden sollen, blockiert. QQ Grenzwert für die CPU-Zeit im Benutzermodus für den gesamten Job Schränkt die CPU-Zeit im Benutzermodus ein, die die Prozesse im Job verbrauchen dürfen (einschließlich der Prozesse, die bereits ausgeführt wurden und beendet sind). Sobald der Grenzwert erreicht ist, werden standardmäßig alle Prozesse im Job mit einem Fehlercode beendet. Es ist dann auch nicht mehr möglich, neue Prozesse in dem Job zu erstellen (sofern der Grenzwert nicht zurückgesetzt wird). Es wird ein Signal über dieses Jobobjekt Jobs Kapitel 3 197
gesendet, damit jegliche Threads, die auf den Job warten, freigegeben werden können. Dieses Standardverhalten können Sie mit einem Aufruf von EndOfJobTimeAction in der Struktur JOBOBJECT_END_OF_JOB_TIME_INFORMATION ändern, die mit der Informationsklasse JobObjectEndOfJobTimeInformation übergeben wird, sodass stattdessen eine Benachrichtigung durch den Abschlussport des Jobs gesendet wird. QQ Grenzwert für die CPU-Zeit im Benutzermodus für einzelne Prozesse Ist dieser Wert angegeben, können die einzelnen Prozesse im Job nur jeweils begrenzte CPU-Zeit im Benutzermodus verbrauchen. Ist der Grenzwert erreicht, wird der Prozess beendet, wobei es keine Möglichkeiten für Aufräumarbeiten mehr gibt. QQ Prozessoraffinität des Jobs Richtet die Prozessoraffinitätsmaske für jeden Prozess im Job ein. Einzelne Threads können ihre Affinität auf eine Teilmenge der Jobaffinität einschränken, doch Prozesse können ihre Prozessaffinitätseinstellungen nicht ändern. QQ Gruppenaffinität des Jobs Stellt eine Liste von Gruppen auf, denen die Prozesse im Job zugeordnet werden können. Affinitätsänderungen erfolgen dann durch Auswahl der entsprechenden Gruppen. Diese Angabe dient als gruppengesteuerte Version der (veralteten) Festlegung der Jobprozessoraffinität und verhindert, dass Letztere verwendet wird. QQ Prioritätsklasse der Jobprozesse Legt die Prioritätsklasse für jeden Prozess im Job fest. Anders als sonst können Threads ihre Priorität dabei nicht erhöhen. Jegliche Versuche dazu werden ignoriert. Bei einem Aufruf von SetThreadPriority wird zwar kein Fehler zurückgegeben, doch die Priorität wird nicht erhöht. QQ Standardminimum und -maximum für den Arbeitssatz Legt das Minimum und Maximum des Arbeitssatzes für jeden Prozess im Job fest. Dies ist jedoch keine jobweite Einstellung: Jeder Prozess hat seinen eigenen Arbeitssatz, wobei für alle Arbeitssätze dieselben Minimum- und Maximumwerte gelten. QQ Grenzwert für den zugesicherten virtuellen Speicher der Prozesse und des Jobs Legt die Höchstmenge des virtuellen Adressraums fest, der von einem einzelnen Prozess oder dem gesamten Job zugesichert werden kann. QQ Steuerung der CPU-Rate Legt das Maximum an CPU-Zeit fest, das der Job benutzen darf, bevor er gedrosselt wird. Diese Einstellung wird für die in Kapitel 4 beschriebene Unterstützung von Planungsgruppen verwendet. QQ Steuerung der Netzwerkbandbreitenrate Legt das Maximum an Bandbreite fest, das dem gesamten Job für ausgehende Verbindungen zur Verfügung steht, bevor eine Drosselung auftritt. Damit kann für die von dem Job gesendeten Netzwerkpakete auch ein DSCP-Wert (Differentiated Services Code Point) für QoS-Zwecke festgelegt werden. Dies lässt sich nur für einen Job in einer Hierarchie einstellen und wirkt sich auf den Job und jegliche Kindjobs aus. QQ Steuerung der Bandbreitenrate für Festplatten-E/A Entspricht der Steuerung der Netzwerkbandbreitenrate, allerdings für Festplatten-E/A. Damit lässt sich die Bandbreite selbst oder die Anzahl der E/A-Operationen pro Sekunde (IOPS) steuern. Die Einstellung kann für ein bestimmtes Volume oder für alle Volumes auf dem System vorgenommen werden. 198 Kapitel 3 Prozesse und Jobs
Für viele dieser Einstellungen kann der Besitzer des Jobs Schwellenwerte angeben, bei deren Erreichen eine Benachrichtigung gesendet wird. (Ist keine Benachrichtigung registriert, wird der Job einfach beendet.) Dazu werden die Benachrichtigungen in eine Warteschlange am E/A-Abschlussport des Jobs gestellt (siehe Dokumentation des Windows SDK). Bei den Ratensteuerungswerten können auch Toleranzbereiche mit Intervallen angegeben werden, sodass ein Prozess beispielsweise alle fünf Minuten bis zu zehn Sekunden lang den Grenzwert für die Netzwerkbandbreite um 20 % überschreiten darf. Für die Prozesse in einem Job können Sie auch Einschränkungen in Bezug auf die Benutzeroberfläche einrichten. Beispielsweise können Sie Prozesse daran hindern, Handles für Fenster zu öffnen, die Threads außerhalb des Jobs gehören, in der Zwischenablage zu lesen und zu schreiben und Systemparameter der Benutzerschnittstelle über die Windows-Funk tion SystemParametersInfo zu ändern. Diese Einschränkungen werden vom GDI/USER-Treiber Win32k.sys des Windows-Teilsystems verwaltet und durch den Job-Callout durchgesetzt, den dieser Treiber im Prozess-Manager registriert. Um allen Prozessen in einem Job Zugriff auf bestimmte Benutzerhandles (z. B. Fensterhandles) zu gewähren, können Sie die Funktion UserHandleGrantAccess aufrufen. Sie kann jedoch (logischerweise) nur von einem Prozess aufgerufen werden, der nicht zu dem betreffenden Job gehört. Umgang mit Jobs Ein Jobobjekt erstellen Sie mit der API CreateJobObject. Zu Anfang befinden sich keinerlei Prozesse in dem Job. Um ihn mit einem Prozess zu versehen, rufen Sie AssignProcessToJobObject auf. Sie kann mehrmals verwendet werden, um einem Job mehrere Prozesse oder einen Prozess zu mehreren Jobs hinzuzufügen. Bei der zweiten Möglichkeit entsteht ein verschachtelter Job, was wir uns im nächsten Abschnitt ansehen werden. Sie können einen Prozess auch dadurch zu einem Job hinzufügen, dass Sie mit dem weiter vorn behandelten Prozesserstellungsattribut PS_CP_JOB_LIST manuell ein Handle zu dem Jobobjekt angeben. Dabei können Sie auch Handles für mehrere Jobobjekte angeben, die alle verknüpft werden. Die interessanteste API für Jobs ist SetInformationJobObject, mit der Sie die im vorherigen Abschnitt erwähnten Grenzwerte und Einstellungen einrichten können. Des Weiteren umfasst sie die internen Informationsklassen, die von Mechanismen wie Containern (Silos), dem DAM oder Windows-UWP-Anwendungen genutzt werden. Diese Werte können mit QueryInformationJobObject wieder gelesen werden, um daran interessierten Parteien die für den Job festgelegten Grenzwerte mitzuteilen. Diese Funktion muss auch aufgerufen werden, wenn Benachrichtigungen über Grenzwerte eingerichtet sind, damit der Aufrufer genau erfährt, welche Grenzwerte verletzt wurden. Ebenfalls oft nützlich ist die Funktion TerminateJobObject, die alle Prozesse in dem Job beendet (so, als wäre TerminateProcess für jeden einzelnen dieser Prozesse aufgerufen worden). Verschachtelte Jobs Bis Windows 7 und Windows Server 2008 R2 konnte ein Prozess immer nur mit einem einzigen Job verbunden sein. Dadurch waren Jobs weniger nützlich, als sie hätten sein können, da eine Anwendung manchmal im Voraus gar nicht wissen konnte, ob ein Prozess, den sie verwalten Jobs Kapitel 3 199
musste, zu einem Job gehörte oder nicht. Ab Windows 8 und Windows Server 2012 kann ein Prozess jedoch mit mehreren Jobs verbunden sein, wodurch im Grunde genommen eine Jobhierarchie entsteht. Ein Kindjob unterhält einen Teil der Prozesse seines Elternjobs. Sobald ein Prozess zu mehr als einem Job hinzugefügt wurde, versucht das System, eine Hierarchie zu bilden. Zurzeit ist der Aufbau einer solchen Hierarchie nicht möglich, falls einer der fraglichen Jobs Einschränkungen für die Benutzeroberfläche festlegt (SetInformationJobObject mit dem Argument JobObjectBasicUIRestrictions). Die Einschränkungen eines Kindjobs können restriktiver sein als die seines Elternjobs, aber nicht weniger streng. Wenn beispielsweise ein Elternjob einen Speichergrenzwert von 100 MB hat, dann kann ein Kindjob für seine Prozesse (und seine eigenen Kindjobs) keinen höheren Grenzwert festlegen (entsprechende Anforderungen schlagen einfach fehl), aber durchaus einen niedrigeren, z. B. 80 MB. Benachrichtigungen an den E/A-Abschlussport des Jobs werden an den Job und alle seine Vorfahren gesendet. Damit eine Benachrichtigung an die Vorfahrenjobs gesendet wird, muss der Job selbst nicht über einen E/A-Abschlussport verfügen. Die Ressourcen für einen Elternjob schließen die gesammelten Ressourcen ein, die von seinen direkt verwalteten Prozessen sowie sämtlichen Prozessen in Kindjobs genutzt werden. Wird ein Job beendet (TerminateJobObject), dann werden alle Prozesse in ihm und in seinen Kindjobs beendet, wobei der Vorgang bei den Kindjobs am unteren Ende der Hierarchie beginnt. Abb. 3–15 zeigt vier Prozesse, die in einer Jobhierarchie verwaltet werden. Abbildung 3–15 Eine Jobhierarchie Um diese Hierarchie zu erstellen, müssen die Prozesse vom Stammjob aus zu den einzelnen Jobs hinzugefügt werden: 1. Prozess 1 wird zu Job 1 hinzugefügt. 2. Prozess 2 wird zu Job 2 hinzugefügt. Dadurch entsteht die erste Verschachtelung. 3. Prozess 2 wird zu Job 1 hinzugefügt. 4. Prozess 2 wird zu Job 3 hinzugefügt. Dadurch entsteht die zweite Verschachtelung. 5. Prozess 3 wird zu Job 2 hinzugefügt. 6. Prozess 4 wird zu Job 1 hinzugefügt. 200 Kapitel 3 Prozesse und Jobs
Experiment: Das Jobobjekt anzeigen Sie können sich benannte Jobobjekte in der Leistungsüberwachung ansehen. Halten Sie dabei nach dem Kategorien Auftragsobjekt und Auftragsobjektdetails Ausschau. Unbenannte Jobs können Sie mit den Befehlen !job und dt nt!_ejob des Kerneldebuggers einsehen. Ob ein Prozess mit einem Job verbunden ist, können Sie sich mithilfe des Kerneldebuggerbefehls !process und in Process Explorer ansehen. Um ein unbenanntes Jobobjekt zu erstellen und anzuzeigen, gehen Sie wie folgt vor: 1. Führen Sie an einer Eingabeaufforderung den Befehl runas aus, um einen Prozess zu erstellen, der innerhalb der Eingabeaufforderung (Cmd.exe) läuft, z. B. runas /user: <Domäne>\<Benutzername> cmd. 2. Sie werden nach Ihrem Kennwort gefragt. Nachdem Sie es eingegeben haben, erscheint ein Fenster mit einer Eingabeaufforderung. Der Windows-Dienst zur Ausführung von runas-Befehlen erstellt einen unbenannten Job, der alle Prozesse enthält (damit er diese Prozesse bei der Abmeldung beenden kann). 3. Starten Sie Process Explorer, öffnen Sie das Menü Options, wählen Sie Configure Colors und aktivieren Sie den Eintrag Jobs. Der Prozess Cmd.exe und sein Kindprozess ConHost.exe werden als Teil eines Jobs hervorgehoben: 4. Doppelklicken Sie auf den Prozess Cmd.exe oder ConHost.exe, um sein Eigenschaftendialogfeld zu öffnen. Klicken Sie dort auf die Registerkarte Job, um sich Informationen über den Job anzusehen, zu dem der Prozess gehört: → Jobs Kapitel 3 201
5. Führen Sie an der Befehlszeile Notepad.exe aus. 6. Öffnen Sie den Prozess für den Editor und rufen Sie dessen Registerkarte Job auf. Wie Sie sehen, läuft der Editor in demselben Job wie die Eingabeaufforderung, da Cmd.exe nicht das Flag CREATE_BREAKAWAY_FROM_JOB verwendet. Bei verschachtelten Jobs zeigt die Registerkarte Job die Prozesse in dem Job an, zu dem der Prozess unmittelbar gehört, sowie in allen Kindjobs. 7. Führen Sie den Kerneldebugger auf einem aktiven System aus und geben Sie den Befehl !process ein, um Notepad.exe zu finden und die folgenden grundlegenden Informationen darüber anzuzeigen: lkd> !process 0 1 notepad.exe PROCESS ffffe001eacf2080 SessionId: 1 Cid: 3078 Peb: 7f4113b000 ParentCid: 05dc DirBase: 4878b3000 ObjectTable: ffffc0015b89fd80 HandleCount: 188. Image: notepad.exe ... 8. BasePriority 8 CommitCharge 671 Job ffffe00189aec460 Beachten Sie den von null verschiedenen Jobzeiger. Um eine Übersicht über den Job zu bekommen, geben Sie den Debuggerbefehl !job ein: lkd> !job ffffe00189aec460 Job at ffffe00189aec460 Basic Accounting Information TotalUserTime: 0x0 TotalKernelTime: 0x0 TotalCycleTime: 0x0 ThisPeriodTotalUserTime: 0x0 ThisPeriodTotalKernelTime: 0x0 TotalPageFaultCount: 0x0 TotalProcesses: 0x3 ActiveProcesses: 0x3 FreezeCount: 0 BackgroundCount: 0 TotalTerminatedProcesses: 0x0 PeakJobMemoryUsed: 0x10db PeakProcessMemoryUsed: 0xa56 Job Flags Limit Information (LimitFlags: 0x0) Limit Information (EffectiveLimitFlags: 0x0) → 202 Kapitel 3 Prozesse und Jobs
9. Wie Sie sehen, ist ActiveProcesses auf 3 gesetzt (Cmd.exe, ConHost.exe und Notepad. exe). Um sich eine Liste der Prozesse anzusehen, die Teil des Jobs sind, geben Sie hinter dem Befehl !job das Flag 2 an: lkd> !job ffffe00189aec460 2 ... Processes assigned to this job: PROCESS ffff8188d84dd780 SessionId: 1 Cid: 5720 Peb: 43bedb6000 ParentCid: 13cc DirBase: 707466000 ObjectTable: ffffbe0dc4e3a040 HandleCount: <Data Not Accessible> Image: cmd.exe PROCESS ffff8188ea077540 SessionId: 1 Cid: 30ec Peb: dd7f17c000 ParentCid: 5720 DirBase: 75a183000 ObjectTable: ffffbe0dafb79040 HandleCount: <Data Not Accessible> Image: conhost.exe PROCESS ffffe001eacf2080 SessionId: 1 Cid: 3078 Peb: 7f4113b000 ParentCid: 05dc DirBase: 4878b3000 ObjectTable: ffffc0015b89fd80 HandleCount: 188. Image: notepad.exe 10. Sie können auch den Befehl dt verwenden, um sich das Jobobjekt und zusätzliche Felder mit Informationen über den Job anzusehen, z. B. seine Elementebene und seine Beziehungen zu anderen Jobs (Elternjob, Geschwisterjobs und Stammjob). lkd> dt nt!_ejob ffffe00189aec460 +0x000 Event : _KEVENT +0x018 JobLinks : _LIST_ENTRY [ 0xffffe001'8d93e548 - 0xffffe001'df30f8d8 ] +0x028 ProcessListHead : _LIST_ENTRY [ 0xffffe001'8c4924f0 0xffffe001'eacf24f0 ] +0x038 JobLock : _ERESOURCE +0x0a0 TotalUserTime : _LARGE_INTEGER 0x0 +0x0a8 TotalKernelTime : _LARGE_INTEGER 0x2625a +0x0b0 TotalCycleTime : _LARGE_INTEGER 0xc9e03d ... +0x0d4 TotalProcesses : 4 +0x0d8 ActiveProcesses : 3 +0x0dc TotalTerminatedProcesses : 0 ... → Jobs Kapitel 3 203
+0x428 ParentJob : (null) +0x430 RootJob : 0xffffe001'89aec460 _EJOB ... +0x518 EnergyValues : 0xffffe001'89aec988 _PROCESS_ENERGY_VALUES +0x520 SharedCommitCharge : 0x5e8 Windows-Container (Serversilos) Das Aufkommen des billigen, allgegenwärtigen Cloudcomputings hat zu einer weiteren erheblichen Umwälzung des Internets geführt. Für den Aufbau von Onlinediensten und Back-EndServern für mobile Anwendungen ist jetzt kaum mehr erforderlich, als bei einem der vielen Cloudanbieter auf eine Schaltfläche zu klicken. Allerdings hat sich die Konkurrenz zwischen diesen Anbietern verschärft, und der Bedarf für einen Wechsel von einem Cloudanbieter zu einem anderen, von einem Cloudanbieter zu einem Rechenzentrum oder von einem Rechenzentrum zu einem persönlichen High-End-Server ist gestiegen. Aufgrund dieser Entwicklung werden portierbare Back-Ends immer wichtiger, die nach Bedarf bereitgestellt und verlagert werden können, ohne dafür den Mehraufwand in Kauf nehmen zu müssen, den die Ausführung in einer virtuellen Maschine mit sich bringt. Um diesen Bedarf zu befriedigen, wurden Technologien wie Docker entwickelt. Sie ermöglichen die Verlagerung einer »Paketanwendung« von einer Linux-Distribution auf eine andere, ohne sich dabei um die Feinheiten der Bereitstellung einer lokalen Installation oder um den Ressourcenverbrauch einer virtuellen Maschine sorgen zu müssen. Dies war ursprünglich eine reine Linux-Technologie, doch im Rahmen des Anniversary-Updates hat Microsoft Docker auch für Windows 10 verfügbar gemacht. Dabei gibt es zwei verschiedene Betriebsmodi: QQ Durch Bereitstellung einer Anwendung in einem schweren, aber vollständig isolierten Hyper-V-Container, was sowohl für Server als auch für Clients möglich ist QQ Durch Bereitstellung einer Anwendung in einem schlanken, betriebssystemisolierten Silocontainer, was aus Lizenzgründen nur für Server möglich ist In diesem Abschnitt sehen wir uns den zweiten Fall an. Um dies möglich zu machen, mussten tief greifende Änderungen am Betriebssystem vorgenommen werden. Clientsysteme haben zwar prinzipiell auch die Möglichkeit, Serversilocontainer zu erstellen, doch diese Funktion ist zurzeit deaktiviert. Im Gegensatz zu Hyper-V-Containern, die auf eine echte virtualisierte Umgebung zurückgreifen, bietet ein Serversilocontainer eine zweite »Instanz« aller Benutzermoduskomponenten, läuft aber oberhalb desselben Kernels und derselben Treiber. Dadurch kommt eine weit schlankere Containerumgebung zustande, was aber auf Kosten der Sicherheit geht. Jobobjekte und Silos Die Möglichkeiten zum Erstellen eines Silos hängen von mehreren nicht dokumentierten Unterklassen ab, die zur API SetJobObjectInformation gehören. Ein Silo ist also im Grunde genom- 204 Kapitel 3 Prozesse und Jobs
men ein Superjob mit zusätzlichen Regeln und Fähigkeiten. Ein Jobobjekt kann nicht nur für die bisher betrachteten Zwecke der Isolierung und des Ressourcenmanagements verwendet werden, sondern auch, um einen Silo zu erstellen. Solche Jobs werden als Hybridjobs bezeichnet. Jobobjekte können zwei Arten von Silos beherbergen, nämlich Anwendungssilos (die zurzeit zur Implementierung von Desktop Bridge verwendet werden und in Kapitel 9 von Band 2 erläutert werden) und Serversilos. Letztere ermöglichen die Verwendung von Docker-Containern. Siloisolierung Das erste Element zur Definition eines Serversilos ist ein benutzerdefiniertes Stammverzeichnisobjekt (\) des Objekt-Managers. (Der Objekt-Manager wird in Kapitel 8 von Band 2 behandelt.) Diesen Mechanismus haben Sie zwar noch nicht kennengelernt, aber wir wollen hier nur erwähnen, dass sich alle für die Anwendung sichtbaren benannten Objekte (wie Dateien, Regis trierungsschlüssel, Ereignisse, Mutexe, RPC-Ports usw.) in einem Stammnamensraum befinden. Dadurch können Anwendungen solche Objekte erstellen, finden und gemeinsam nutzen. Da ein Serversilo seinen eigenen Stamm haben kann, ist es möglich, den Zugriff auf jegliche benannten Objekte zu steuern. Dies kann auf dreierlei Weise geschehen: QQ Durch Erstellen einer neuen Kopie eines vorhandenen Objekts, um innerhalb des Silos eine alternative Zugriffsmöglichkeit darauf zu bieten QQ Durch Erstellen einer symbolischen Verknüpfung zu einem vorhandenen Objekt, um den direkten Zugriff darauf zu ermöglichen QQ Durch Erstellen eines ganz neuen Objekts, das nur innerhalb des Silos existiert, ähnlich wie die Objekte, die eine Containeranwendung nutzt Diese grundlegenden Möglichkeiten werden dann mit dem von Docker verwendeten Dienst Vmcompute (Virtual Machine Computer) kombiniert, der mit weiteren Komponenten zusammen- arbeitet, um eine vollständige Isolierschicht zu bilden: QQ Eine grundlegende WIM-Datei (Windows Image) als Basisbetriebssystem Dadurch wird eine separate Kopie des Betriebssystems bereitgestellt. Zurzeit bietet Microsoft sowohl ein Server Core- als auch ein Nano Server-Abbild an. QQ Die Bibliothek Ntdll.dll des Hostbetriebssystems Sie überschreibt die gleichnamige Bibliothek im Abbild des Basisbetriebssystems. Das liegt daran, dass Serversilos denselben Kernel und dieselben Treiber nutzen. Da Ntdll.dll Systemaufrufe verarbeitet, ist sie die einzige Benutzermoduskomponente, die das Hostbetriebssystem wiederverwenden muss. QQ Ein virtuelles Sandbox-Dateisystem, bereitgestellt vom Filtertreiber Wcifs.sys Dadurch wird es möglich, dass der Container vorübergehende Änderungen am Dateisystem vornimmt, ohne dass sich dies auf das zugrunde liegende NTFS-Laufwerk auswirkt. Nach dem Herunterfahren des Containers können diese Änderungen gelöscht werden. QQ Eine virtuelle Sandbox-Registrierung, bereitgestellt von der Kernelkomponente VReg Dadurch kann ein vorübergehender Satz von Registrierungshives bereitgestellt werden. Außerdem bildet dies eine weitere Schicht zur Namensraumisolierung, da der Stammnamensraum des Objekt-Managers nur den Registrierungsstamm isolieren kann, nicht aber die Hives. Jobs Kapitel 3 205
QQ Der Sitzungs-Manager (Smss.exe) Er wird jetzt verwendet, um zusätzliche Dienstoder Konsolensitzungen zu erstellen. Diese neue Fähigkeit wird zur Unterstützung von Containern benötigt. Dazu wird Smss so erweitert, dass er nicht nur zusätzliche Benutzer sitzungen handhaben kann, sondern auch die Sitzungen, die von den gestarteten Con tainern benötigt werden. Abb. 3–16 zeigt die Architektur solcher Container und die zuvor genannten Komponenten. Hostbetriebssystem Container 1 Container 2 Grundlegendes Windows-Abbild (WIM) Grundlegendes Windows-Abbild (WIM) Silo Silo Virtuelle Registrierung Container-Manager Virtuelle Registrierung Virtuelles DS Prozess Host compute dienst Host netzwerk dienst Andere Dienste Host-SMSS Prozess Dienste Host-SMSS Host-NTDLL Virtuelles DS Prozess Dienste Host-NTDLL Host-DS Wcifs.sys VReg Ntfs.sys Konfigurations-Manager Hostregis trierung Exekutive und Kernel Abbildung 3–16 Containerarchitektur Silogrenzen Die genannten Komponenten bilden eine Umgebung für die Isolierung im Benutzermodus. Da jedoch die Hostkomponente Ntdll.dll verwendet wird, die mit dem Hostkernel und den Hosttreibern kommuniziert, ist es wichtig, zusätzliche Isolierungsgrenzen zu erstellen, die der Kernel anbietet, um zwischen den Silos zu unterscheiden. Dazu enthält jeder Serversilo sein eigenes, isoliertes Exemplar der folgenden Elemente: QQ Gemeinsame Silodaten (SILO_USER_SHARED_DATA) Sie enthalten den benutzerdefinierten Systempfad, die Sitzungs-ID, die Vordergrund-PID sowie Produkttyp bzw. -suite. Dies sind Elemente der ursprünglichen KUSER_SHARED_DATA-Struktur. Sie dürfen jedoch nicht vom Host bezogen werden, da sie dort auf Informationen für das Host- statt das Basisbetriebssystemabbild verweisen, das stattdessen verwendet werden soll. Mehrere Komponenten und APIs wurden modifiziert, um beim Nachschlagen solcher Daten die gemeinsamen Silodaten statt der gemeinsamen Benutzerdaten zu lesen. Die ursprüngliche KUSER_SHARED_DATA-Struktur bleibt mit ihrer ursprünglichen Sicht der Hostinformationen an der üblichen Adresse. Daher ist dies eine Möglichkeit, durch die der Hoststatus in den Containerstatus durchsickern kann. QQ Namensraum des Objektverzeichnisstamms Er enthält seinen eigenen symbolischen \SystemRoot-Link, sein eigenes \Device-Verzeichnis (durch das alle Benutzermoduskompo- 206 Kapitel 3 Prozesse und Jobs
nenten indirekt auf die Gerätetreiber zugreifen), eigene Geräte- und DOS-Geräte-Zuordnungen (über die Benutzermodusanwendungen beispielsweise auf Treiber zugreifen, die im Netzwerk zugeordnet sind), ein eigenes \Sessions-Verzeichnis usw. QQ API-Set-Zuordnung Sie basiert auf dem API-Set-Schema des Basisbetriebssystemabbilds, also nicht auf dem Schema, das im Dateisystem des Hostbetriebssystems gespeichert ist. Wie Sie wissen, nutzt der Lader API-Set-Zuordnungen, um zu bestimmen, welche DLL eine bestimmte Funktion implementiert. Dies kann sich von einer SKU zur anderen unterscheiden. Anwendungen müssen daher die SKU des Basis- und nicht des Hostbetriebssystems erkennen können. QQ Anmeldesitzung Sie ist mit den lokal eindeutigen IDs (Local Unique ID, LUID) SYSTEM und Anonymous sowie der LUID des virtuellen Dienstkontos verbunden, das den Benutzer im Silo beschreibt. Im Grunde genommen steht die für das Token der Dienste und der Anwendung, die innerhalb der von Smss erstellten Containerdienstsitzung ausgeführt werden. Weitere Informationen über LUIDs und Anmeldesitzungen erhalten Sie in Kapitel 7. QQ ETW-Verfolgung und Loggerkontexte Sie dienen zur Isolierung von ETW-Operationen im Silo, damit die Status nicht zum Container oder Hostbetriebssystem durchsickern (siehe Kapitel 9 in Band 2). Silokontexte Neben diesen Isoliergrenzen, die der Kernelkern des Hostbetriebssystems bereitstellt, können andere Komponenten innerhalb des Kernels sowie Treiber (auch von Drittanbietern) zusätzliche Kontextdaten für Silos bereitstellen. Dazu richten Sie mit der API PsCreateSiloContext benutzerdefinierte Daten für einen Silo ein oder verknüpfen ein bestehendes Objekt mit einem Silo. Jeder Silokontext dieser Art nutzt einen Siloslotindex, der in alle laufenden und künftigen Serversilos eingefügt wird und einen Zeiger auf den Kontext enthält. Im Lieferumfang des Systems sind 32 systemweite Speicherslotindizes sowie 256 Erweiterungsslots enthalten, was sehr viele Möglichkeiten zur Erweiterung bietet. Wenn ein Serversilo erstellt wird, erhält er ein Array für seinen lokalen Silospeicher (Silo-Local Storage, SLS), der vergleichbar mit dem lokalen Threadspeicher eines Threads ist (TLS). Die Einträge in diesem Array geben die Slotindizes an, die zur Speicherung von Silokontexten zugewiesen sind. Die einzelnen Silos haben am selben Slotindex jeweils einen anderen Zeiger, speichern dort aber stets einen gleichartigen Kontext. (Beispielsweise kann dem Treiber Foo in allen Silos der Index 5 gehören, an dem er dann in jedem Silo einen anderen Zeiger bzw. Kontext speichert.) Einige dieser Slots werden auch von integrierten Kernelkomponenten wie dem Objekt-Manager, dem Security Reference Monitor (SRM) und dem Konfigurations-Manager genutzt, andere von mitgelieferten Treibern (wie Afd.sys, dem Treiber für zusätzliche WinSock-Funktionen). Wie für den Umgang mit gemeinsamen Silodaten wurden mehrere Komponenten und APIs modernisiert, um die Daten von dem entsprechenden Silokontext abzurufen statt von dem, was einmal eine globale Kernelvariable war. Beispielsweise hat jeder Container jetzt seinen eigenen Lsass.exe-Prozess, und da der SRM des Kernels ein eigenes Handle für diesen Prozess benötigt (siehe Kapitel 7), kann dies nicht mehr ein Singleton in einer globalen Varia- Jobs Kapitel 3 207
blen sein. Daher greift der SRM jetzt auf das Handle zu, indem er den Silokontext des aktiven Serversilos abfragt und die Variable dann aus der zurückgegebenen Datenstruktur entnimmt. Doch was geschieht mit dem Lsass.exe-Prozess, der auf dem Hostbetriebssystem ausgeführt wird? Wie kann der SRM auf sein Handle zugreifen, da es doch keinen Serversilo für diese Prozesse und diese Sitzung (Sitzung 0) gibt? Für diesen Zweck unterhält der Kernel jetzt einen Stammhostsilo. Anders ausgedrückt, der Host selbst wird als Teil eines Silos betrachtet. Dabei handelt es sich jedoch nicht um einen Silo in der eigentlichen Bedeutung, sondern um einen Klick, damit Kontextabfragen für den aktuellen Silo auch dann funktionieren, wenn es gar keinen aktuellen Silo gibt. Dazu wird die globale Kernelvariable PspHostSiloGlobals gespeichert, die über ein eigenes Array für den lokalen Silospeicher sowie über andere Silokontexte verfügt, die von den integrierten Kernelkomponenten verwendet werden. Bei verschiedenen Silo-APIs wird ein Aufruf mit dem NULL-Zeiger als »kein Silo – verwende den Hostsilo« gedeutet. Experiment: Den SRM-Silokontext für den Hostsilo ausgeben Auch ein Windows 10-System muss nicht unbedingt Serversilos unterhalten, insbesondere wenn es sich um ein Clientsystem handelt. Es ist jedoch stets ein Hostsilo vorhanden, der die isolierten Silokontexte für den Kernel enthält. Die Windows-Debuggererweiterung !silo können Sie zusammen mit dem Parameter -g Host verwenden, um eine Ausgabe wie die folgende zu bekommen: lkd> !silo -g Host Server silo globals fffff801b73bc580: Default Error Port: ffffb30f25b48080 ServiceSessionId : 0 Root Directory : 00007fff00000000 '' State : Running In der Ausgabe bildet der Zeiger auf die globalen Silovariablen einen Hyperlink. Wenn Sie darauf klicken, sehen Sie die folgende Befehlsausführung als Ausgabe: lkd> dx -r1 (*((nt!_ESERVERSILO_GLOBALS *)0xfffff801b73bc580)) (*((nt!_ESERVERSILO_GLOBALS *)0xfffff801b73bc580)) [Type: _ESERVERSILO_GLOBALS] [+0x000] ObSiloState [Type: _OBP_SILODRIVERSTATE] [+0x2e0] SeSiloState [Type: _SEP_SILOSTATE] [+0x310] SeRmSiloState [Type: _SEP_RM_LSA_CONNECTION_STATE] [+0x360] CmSiloState : 0xffffc308870931b0 [Type: _CMP_SILO_CONTEXT *] [+0x368] EtwSiloState : 0xffffb30f236c4000 [Type: _ETW_SILODRIVERSTATE *] ... Wenn Sie jetzt auf das Feld SeRmSiloState klicken, wird es erweitert und zeigt unter anderem einen Zeiger auf den Prozess Lsass.exe: lkd> dx -r1 ((ntkrnlmp!_SEP_RM_LSA_CONNECTION_STATE *)0xfffff801b73bc890) ((ntkrnlmp!_SEP_RM_LSA_CONNECTION_STATE *)0xfffff801b73bc890) : → 208 Kapitel 3 Prozesse und Jobs
0xfffff801b73bc890 [Type: _SEP_RM_LSA_CONNECTION_STATE *] [+0x000] LsaProcessHandle : 0xffffffff80000870 [Type: void *] [+0x008] LsaCommandPortHandle : 0xffffffff8000087c [Type: void *] [+0x010] SepRmThreadHandle : 0x0 [Type: void *] [+0x018] RmCommandPortHandle : 0xffffffff80000874 [Type: void *] Siloüberwachung Kerneltreiber können sich mit ihrem eigenen Silokontext versehen. Woher aber wissen sie, welche Silos ausgeführt werden und welche neuen Silos als Container erstellt und gestartet werden? Dazu ist die Siloüberwachung da. Sie stellt APIs für Benachrichtigungen darüber bereit, dass ein Serversilo erstellt oder beendet wurde (PsRegisterSiloMonitor, PsStartSiloMonitor, PsUnregisterSiloMonitor), sowie über jegliche bereits existierenden Silos. Jede Siloüberwachung kann durch den Aufruf von PsGetSiloMonitorContextSlot ihren eigenen Slotindex abrufen und dann nach Bedarf in den Funktionen PsInsertSiloContext, PsReplaceSiloContext und PsRemoveSiloContext nutzen. Mit PsAllocSiloContextSlot lassen sich auch weitere Slots zuweisen, was allerdings nur dann erforderlich ist, wenn eine Komponente aus irgendeinem Grund zwei Kontexte speichern möchte. Treiber können auch die APIs PsInsertPermanentSiloContext oder PsMakeSiloContextPermanent nutzen, um dauerhafte Silokontexte anzulegen, bei denen keine Referenzzählung stattfindet und die nicht an die Lebensdauer des Serversilos oder die Anzahl der Get-Funktionen für Silokontexte gebunden sind. Ein solcher dauerhafter Kontext kann mit PsGetSiloContext und PsGetPermanentSiloContext abgerufen werden. Experiment: Siloüberwachung und -kontext Damit Sie ein Bild von der Verwendung der Siloüberwachung und der Speicherung von Silokontexten bekommen, schauen wir uns den Treiber für zusätzliche WinSock-Funktionen (Afd.sys) und die zugehörige Überwachung an. Als Erstes geben wir die Datenstruktur für die Überwachung aus. Leider befindet sie sich nicht in den Symboldateien, weshalb wir dies in Form von Rohdaten tun müssen. lkd> dps poi(afd!AfdPodMonitor) ffffe387'a79fc120 ffffe387'a7d760c0 ffffe387'a79fc128 ffffe387'a7b54b60 ffffe387'a79fc130 00000009'00000101 ffffe387'a79fc138 fffff807'be4b5b10 afd!AfdPodSiloCreateCallback ffffe387'a79fc140 fffff807'be4bee40 afd!AfdPodSiloTerminateCallback Als Nächstes rufen wir den Slot (in diesem Beispiel 9) vom Hostsilo ab. Silos speichern ihren SLS im Feld Storage. Es umfasst ein Array aus Datenstrukturen (Sloteinträgen), die wiederum jeweils einen Zeiger enthalten, sowie einige Flags. Um den Offset für den richtigen Sloteintrag zu bekommen, multiplizieren wir den Index mit 2. Anschließend greifen wir auf das zweite Feld (+1) zu, damit wir den Zeiger auf den Kontextzeiger erhalten: → Jobs Kapitel 3 209
lkd> r? @$t0 = (nt!_ESERVERSILO_GLOBALS*)@@masm(nt!PspHostSiloGlobals) lkd> ?? ((void***)@$t0->Storage)[9 * 2 + 1] void ** 0xffff988f'ab815941 Das Permanenzflag (0x2) wird mit einer OR-Verknüpfung in den Zeiger eingeschlossen. Maskieren Sie es und vergewissern Sie sich dann mit dem Ausdruck !object, dass das Ergebnis wirklich ein Silokontext ist: lkd> !object (0xffff988f'ab815941 & -2) Object: ffff988fab815940 Type: (ffff988faaac9f20) PsSiloContextNonPaged Serversilos erstellen Da Silos, wie bereits erwähnt, ein Merkmal von Jobobjekten sind, müssen Sie zum Erstellen eines Serversilos zunächst ein Jobobjekt anlegen. Dazu verwenden Sie die API CreateJobObject, die im Rahmen des Anniversary-Updates so modifiziert wurde, dass Jobs jetzt eine Job-ID (JID) haben. Sie stammt aus demselben Wertebereich wie die Prozess- und die Thread-ID, nämlich aus der Client-ID-Tabelle. Dadurch ist die JID nicht nur unter den anderen Jobs, sondern auch unter den Prozessen und Threads einzigartig. Außerdem wird automatisch ein Container-GUID erstellt. Anschließend wird die API SetInformationJobObject mit der Informationsklasse Siloerstellung verwendet. Dadurch wird im EJOB-Exekutivobjekt für den Job das Flag Silo gesetzt und das SLS-Slotarray im Element Storage zugewiesen. Damit liegt jetzt ein Anwendungssilo vor. Danach wird mit einem weiteren Aufruf von SetInformationJobObject, aber einer anderen Informationsklasse, der Namensraum für das Stammobjektverzeichnis erstellt. Für die neue Klasse ist das Recht Trusted Computing Base (TCB) erforderlich. Da Silos normalerweise nur vom Dienst Vmcompute erstellt werden, wird dadurch sichergestellt, dass virtuelle Objektnamensräume nicht böswillig verwendet werden, um Anwendungen zu verwirren oder sogar funktionsunfähig zu machen. Beim Anlegen des Namensraums erstellt oder öffnet der Objekt- Manager ein neues Siloverzeichnis unter dem echten Hoststamm (\) und hängt die JID an, um einen neuen virtuellen Stamm zu bilden (z. B. \Silos\148\). Anschließend erstellt er die Objekte KernelObjects, ObjectTypes, GLOBALROOT und DosDevices. Der Stamm wird dann als Silokontext mit dem Slotindex gespeichert, der sich in PsObjectDirectorySiloContextSlot befindet und vom Objekt-Manager beim Start zugewiesen wurde. Der nächste Schritt besteht darin, den Silo zu einem Serversilo zu machen. Dazu wird wiederum SetInformationJobObject mit einer weiteren Informationsklasse aufgerufen. Im Kernel wird die Funktion PspConvertSiloToServerSilo ausgeführt, die die ESERVERSILO_GLOBALS-Struktur initialisiert (die Sie schon in dem Experiment gesehen haben, in dem Sie PspHostSiloGlobals mit dem Befehl !silo ausgegeben haben). Dadurch werden die gemeinsamen Silodaten, die API-Set-Zuordnung, der Systemstamm und diverse Silokontexte initialisiert, z. B. derjenige, mit dem die SRM den Lsass.exe-Prozess erkennt. Während des Konvertierungsvorgangs erhalten die Siloüberwachungen, die Callbacks registriert und gestartet haben, entsprechende Nachrichten, z. B. dass sie ihre eigenen Silokontextdaten hinzufügen können. 210 Kapitel 3 Prozesse und Jobs
Der letzte Schritt besteht darin, den Serversilo durch Initialisieren einer neuen Dienstsitzung zu starten, also praktisch Sitzung 0 für den Serversilo. Dies erfolgt durch eine ALPC-Nachricht, die an den Smss-Port SmApiPort gesendet wird und ein Handle für das von Vmcompute erstellte und nun zu einem Serversilo umgewandelte Jobobjekt enthält. Wie beim Erstellen einer normaler Benutzersitzung klont sich Smss selbst, wobei der Klon diesmal jedoch mit dem Jobobjekt verbunden wird. Dadurch wird die neue Smss-Kopie an Containerelemente des Serversilos angehängt. Smss glaubt, dass dies Sitzung 0 ist, und führt seine üblichen Pflichten aus, z. B. den Start von Scrss.exe, Wininit.exe, Lsass.exe usw. Dieser »Startprozess« fährt wie gewöhnlich fort, wobei Wininit.exe als Nächstes den Dienststeuerungsmanager (Services.exe) startet, der wiederum alle Dienste mit automatischem Start in Gang setzt usw. Im Serversilo können jetzt auch neue Anwendungen ausgeführt werden. Wie zuvor beschrieben, werden sie mit einer Anmeldesitzung ausgeführt, die mit einem LUID für ein virtuelles Dienstkonto verbunden ist. Hilfsfunktionen Vielleicht ist Ihnen aufgefallen, dass ein Startprozess wie in der vorstehenden kurzen Beschreibung nicht erfolgreich sein kann. Beispielsweise würde er im Rahmen der Initialisierung versuchen, die benannte Pipe ntsvcs zu erstellen, für die eine Kommunikation mit \Device\NamedPipe bzw. aus der Sicht von Services.exe mit \Silos\JID\Device\NamedPipe erforderlich wäre. Ein solches Geräteobjekt gibt es aber nicht! Damit Gerätetreiber auf Funktionen zugreifen können, müssen sie daher Informationen bekommen und ihre eigenen Siloüberwachungen registrieren, die dann Benachrichtigungen nutzen, um ihre eigenen Geräteobjekte für die einzelnen Silos zu erstellen. Der Kernel bietet die API PsAttachSiloToCurrentThread (und das Pendant PsDetachSiloFromCurrentThread), die vorübergehend das Feld Silo in der ETHREAD-Struktur des übergebenen Jobobjekts ausfüllt. Dadurch wird jeglicher Zugriff, z. B. auf den Objekt-Manager, so behandelt, als käme er von dem Silo. Beispielsweise kann der Treiber für benannte Pipes diesen Mechanismus nutzen, um ein NamedPipe-Objekt im Namensraum \Device zu erstellen, der jetzt zu \Silos\JID\ gehört. Des Weiteren stellt sich die Frage, wie Anwendungen, die im Grunde genommen in einer »Dienstsitzung« gestartet werden, interaktiv sein und Ein- und Ausgaben verarbeiten können. Beim Start in einem Windows-Container sind weder grafische Benutzeroberflächen noch die Verwendung eines Remotedesktops für den Zugriff auf einen Container möglich. Daher können nur Befehlszeilenanwendungen ausgeführt werden. Doch auch sie brauchen gewöhnlich eine interaktive Sitzung. Wie also können sie funktionieren? Das Geheimnis liegt in dem besonderen Hostprozess CExecSvc.exe, der den Containerausführungsdienst implementiert. Dieser Dienst verwendet eine benannte Pipe zur Kommunikation mit dem Docker- und dem Vmcompute-Dienst auf dem Host und startet die eigentlichen Containeranwendungen in der Sitzung. Er emuliert auch die Konsolenfunktion, die normalerweise von Conhost.exe bereitgestellt wird, indem er die Ein- und Ausgabe durch eine benannte Pipe zum Fenster der eigentlichen Eingabeaufforderung (oder PowerShell) leitet, das ursprünglich zur Ausführung des Befehls docker auf dem Host verwendet wurde. Dieser Dienst wird auch für Befehle wie docker cp zur Übertragung von Dateien vom oder zum Container verwendet. Jobs Kapitel 3 211
Die Containervorlage Neben den Geräteobjekten, die beim Erstellen eines Silos von den Treibern angelegt werden, gibt es noch zahllose weitere Objekte, die vom Kernel und anderen Komponenten gebildet werden. Die Dienste, die in Sitzung 0 laufen, erwarten, mit all diesen Objekten zu kommunizieren, und umgekehrt. Im Benutzermodus gibt es kein Siloüberwachungssystem, das diesen Bedarf befriedigen könnte. Es wäre auch nicht sinnvoll, jeden Treiber zu zwingen, immer ein besonderes Geräteobjekt zur Darstellung jedes Silos zu erstellen. Wenn ein Silo Musik auf der Soundkarte abspielen möchte, sollte er kein separates Geräteobjekt zur Darstellung derselben Karte verwenden müssen, die auch von allen anderen Silos und dem Host selbst verwendet wird. Das wäre nur dann erforderlich, wenn eine Klangisolierung zwischen den einzelnen Silos nötig wäre. Ein weiteres Beispiel ist der AFD. Er verwendet zwar eine Siloüberwachung, mit der er aber nur ermittelt, welcher Benutzermodusdienst den DNS-Client beherbergt, mit dem der AFD sprechen muss, um Kernelmodus-DNS-Anforderungen zu erfüllen. Diese Information gilt jeweils für einen Silo. Die Überwachung dient nicht dazu, separate \Silos\JID\Device\Afd-Objekte zu erstellen, da es in dem System nur einen einzigen Netzwerk-/WinSock-Stack gibt. Neben Treibern und Objekten enthält die Registrierung auch verschiedene globale Informationen, die in allen Silos vorhanden und sichtbar sein müssen. Um diese Informationen herum kann dann die Komponente VReg eine Sandbox einrichten. Um all diese Bedürfnisse zu erfüllen, werden der Silonamensraum, die Registrierung und das Dateisystem in einer besonderen Containervorlagendatei definiert, die sich standardmäßig in %SystemRoot%\System32\Containers\wsc.def befindet, wenn das Merkmal Windows-Con tainer im Dialogfeld Features hinzufügen/entfernen aktiviert wurde. Diese Datei beschreibt den Namensraum für den Objekt-Manager und die Registrierung und die zugehörigen Regeln, was die bedarfsweise Definition von symbolischen Verknüpfungen zu den echten Objekten auf dem Host ermöglicht. Außerdem gibt sie an, welches Jobobjekt, welche Volumebereitstellungspunkte und welche Netzwerkisolierungsrichtlinien verwendet werden sollen. Theoretisch kann in zukünftigen Windows-Versionen auch die Verwendung anderer Vorlagendateien zugelassen sein, um andere Arten von Containerumgebungen möglich zu machen. Das folgende Listing zeigt einen Auszug aus Wsc.def auf einem System mit aktivierten Containern: <!-- Silodefinitionsdatei für Cmdserver.exe --> <container> <namespace> <ob shadow="false"> <symlink name="FileSystem" path="\FileSystem" scope="Global" /> <symlink name="PdcPort" path="\PdcPort" scope="Global" /> <symlink name="SeRmCommandPort" path="\SeRmCommandPort" scope="Global" /> <symlink name="Registry" path="\Registry" scope="Global" /> <symlink name="Driver" path="\Driver" scope="Global" /> <objdir name="BaseNamedObjects" clonesd="\BaseNamedObjects" shadow="false"/> <objdir name="GLOBAL??" clonesd="\GLOBAL??" shadow="false"> 212 Kapitel 3 Prozesse und Jobs
<!-- Verzeichnisse müssen vom Host zugeordnet werden --> <symlink name="ContainerMappedDirectories" path="\ ContainerMappedDirectories" scope="Local" /> <!-- Gültige Links zu \Device --> <symlink name="WMIDataDevice" path="\Device\WMIDataDevice" scope="Local" /> <symlink name="UNC" path="\Device\Mup" scope="Local" /> ... </objdir> <objdir name="Device" clonesd="\Device" shadow="false"> <symlink name="Afd" path="\Device\Afd" scope="Global" /> <symlink name="ahcache" path="\Device\ahcache" scope="Global" /> <symlink name="CNG" path="\Device\CNG" scope="Global" /> <symlink name="ConDrv" path="\Device\ConDrv" scope="Global" /> ... <registry> <load key="$SiloHivesRoot$\Silo$TopLayerName$Software_Base" path="$TopLayerPath$\Hives\Software_Base" ReadOnly="true" /> ... <mkkey name="ControlSet001" clonesd="\REGISTRY\Machine\SYSTEM\ControlSet001" /> <mkkey name="ControlSet001\Control" clonesd="\REGISTRY\Machine\SYSTEM\ControlSet001\Control" /> Zusammenfassung Dieses Kapitel hat die Struktur von Prozessen, ihre Erstellung und Zerstörung gezeigt. Wir haben uns angesehen, wie Jobs verwendet werden können, um eine Gruppe von Prozessen als eine Einheit zu verwalten, und wie Serversilos eine neue Ära der Containerunterstützung in Windows Server-Versionen einläuten. Im nächsten Kapitel geht es um Threads – um ihre Struktur und ihre Operation, ihre Einplanung zur Ausführung und die verschiedenen Möglichkeiten, um sie zu bearbeiten und zu nutzen. Zusammenfassung Kapitel 3 213
K apite l 4 Threads Dieses Kapitel erklärt die Datenstrukturen und Algorithmen im Zusammenhang mit Threads und der Threadplanung in Windows. Im ersten Abschnitt geht es darum, wie Threads erstellt werden. Danach werden die internen Mechanismen von Threads und der Threadplanung beschrieben. Abgeschlossen wird das Kapitel mit einer Erörterung von Threadpools. Threads erstellen Bevor wir uns mit den internen Strukturen zur Verwaltung von Threads beschäftigen, wollen wir uns ansehen, wie Threads erstellt werden und welche Schritte und Argumente dazu erforderlich sind. Die einfachste Möglichkeit, um im Benutzermodus einen Thread zu erstellen, bietet die Funktion CreateThread. Sie legt einen Thread im aktuellen Prozess an und nimmt die folgenden Argumente entgegen: QQ Eine optionale Struktur mit Sicherheitsattributen Sie nennt den Sicherheitsdeskriptor, der mit dem neuen Thread verbunden werden soll, und gibt an, ob das Threadhandle vererbbar sein soll. (Die Handlevererbung wird in Kapitel 8 von Band 2 behandelt.) QQ Eine optionale Stackgröße Wird hier 0 angegeben, so wird der Standardwert aus dem Header der ausführbaren Datei verwendet. Dies gilt stets für den ersten Thread in einem Benutzermodusprozess. (Der Threadstack wird in Kapitel 5 ausführlicher beschrieben.) QQ Ein Funktionszeiger Er dient als Eintrittspunkt für die Ausführung des neuen Threads. QQ Ein optionales Argument übergeben. Dies dient dazu, Argumente an die Funktion des Threads zu QQ Zwei optionale Flags Eines dieser Flags gibt an, ob der Thread im angehaltenen Zustand gestartet wird (CREATE_SUSPENDED). Das andere steuert die Interpretation des Stackgrößenarguments (ursprüngliche zugesicherte Größe oder maximale reservierte Größe). Nach erfolgreichem Abschluss werden ein von null verschiedenes Handle für den neuen Thread und, falls vom Aufrufer angefordert, eine eindeutige Thread-ID zurückgegeben. Mit CreateRemoteThread steht eine erweiterte Funktion zur Threaderstellung zur Verfügung. Sie nimmt als zusätzliches erstes Argument ein Handle für den Zielprozess entgegen, in dem der Thread erstellt wird. Damit können Sie einen Thread in einen anderen Prozess injizieren. Diese Technik wird häufig in Debuggern genutzt, um ein Anhalten in einem zu debuggenden Prozess zu erzwingen. Dabei injiziert der Debugger den Thread, der sofort die Funktion DebugBreak aufruft und damit einen Haltepunkt einrichtet. Eine weitere gängige Anwendung dieser Technik besteht darin, einen Prozess interne Informationen eines anderen Prozesses gewinnen 215
zu lassen. Das geht leichter, wenn der Thread im Kontext des Zielprozesses ausgeführt wird (unter anderem ist dabei der gesamte Adressraum sichtbar). Dies kann sowohl für legitime als auch für schädliche Zwecke eingesetzt werden. Damit CreateRemoteThread funktioniert, muss das Prozesshandle mit ausreichend Zugriffsrechten für eine solche Operation versehen sein. Eine Injektion in geschützte Prozesse ist damit nicht möglich, da Handles für solche Prozesse nur stark eingeschränkte Rechte haben. Als letzte Funktion wollen wir hier noch CreateRemoteThreadEx erwähnen, die den beiden zuvor erwähnten übergeordnet ist. Die Implementierungen von CreateThread und Create RemoteThread rufen einfach CreateRemoteThreadEx mit den entsprechenden Standardwerten auf. CreateRemoteThreadEx bietet darüber hinaus die Möglichkeit, eine Attributliste anzugeben (ähnlich wie bei der STARTUPINFOEX-Struktur beim Erstellen von Prozessen, die gegenüber der STARTUPINFO-Struktur ein zusätzliches Element enthält). Zu diesen Attributen gehören beispielsweise Einstellungen für den idealen Prozessor und die Gruppenaffinität (die beide weiter hinten in diesem Kapitel behandelt werden). Wenn alles gut geht, ruft CreateRemoteThreadEx schließlich NtCreateThreadEx in Ntdll.dll auf. Dabei erfolgt der übliche Übergang in den Kernelmodus, wobei die Ausführung in der Exekutivfunktion NtCreateThreadEx fortgesetzt wird. Die Kernelmodusaspekte der Threaderstellung laufen dort ab (siehe weiter hinten in diesem Kapitel im Abschnitt »Geburt eines Threads«). Die Threaderstellung im Kernelmodus wird durch die Funktion PsCreateSystemThread erreicht (die im WDK dokumentiert ist). Das ist praktisch für Treiber, die als unabhängige Prozesse innerhalb des Systemprozesses laufen müssen (das heißt, sie sind nicht mit einem bestimmten Prozess verbunden). Die Funktion kann einen Thread auch unter einem beliebigen Prozess erstellen, was für Treiber jedoch nicht von Bedeutung ist. Bei der Beendigung der Funktion eines Kernelthreads wird das Threadobjekt nicht automatisch zerstört. Stattdessen müssen die Treiber PsTerminateSystemThread aus der Threadfunktion heraus aufrufen, um den Thread ordnungsgemäß zu beenden. Diese Funktion springt logischerweise nie zurück. Interne Strukturen von Threads In diesem Abschnitt geht es um die internen Strukturen, die im Kernel (und manchmal auch im Benutzermodus) verwendet werden, um Threads zu verwalten. Sofern nicht ausdrücklich anders angegeben, gilt alles, was in diesem Abschnitt beschrieben wird, sowohl für Benutzer- als auch für Kernelmodusthreads. Datenstrukturen Auf Betriebssystemebene wird ein Windows-Thread als ein Exekutivthreadobjekt dargestellt. Es schließt eine ETHREAD-Struktur (siehe Abb. 4–1) ein, die wiederum eine KTHREAD-Struktur (siehe Abb. 4–2) als erstes Element enthält. Die ETHREAD-Struktur und die Strukturen, auf die sie zeigt, befinden sich im Systemadressraum. Die einzige Ausnahme bildet der Threadumgebungsblock (TEB), der im Prozessadressraum steht (ähnlich wie der PEB, da die Benutzermoduskomponenten darauf zugreifen müssen). 216 Kapitel 4 Threads
Threadsteuerungsblock (TCB) Erstellungs- und Beendigungszeit des Threads ID des Threads und des Elternprozesses Prozess, zu dem der Thread gehört Threadflags Zugriffstoken Threadstartadresse Informationen zum Identitätswechsel Ausstehende E/A-Anforderungen Timerinformationen ALPC-Nachrichteninformationen Threadlisteneintrag Threadsperre Beendigungscode Energieverbrauchswerte (Win 10) Informationen zum CPU-Satz (Win 10) Abbildung 4–1 Wichtige Felder der Exekutivthreadstruktur (ETHREAD) Dispatcher-Header Threadumgebungsblock (TEB) Kernel- und Benutzerzeit Zeiger auf die Systemdiensttabelle (x86) Systemdiensttabelle Einfrier- und Unterbrechungszähler Win32-Threadobjekt Elternprozess Stackinformationen Planungsinformationen Trap-frame Synchronisierungsinformationen Warteblock Liste der Objekte, auf die der Thread wartet Liste der ausstehenden APCs Threadlisteneintrag Abbildung 4–2 Wichtige Felder der Kernelthreadstruktur (KTHREAD) Der Windows-Teilsystemprozess Csrss unterhält für jeden Thread, der in einer Windows-Teilsystemanwendung erstellt wurde, eine parallele Struktur namens CSR_THREAD. Für jeden Thread, der eine USER- oder GDI-Funktion des Windows-Teilsystems aufgerufen hat, führt der Kernelmodusteil des Windows-Teilsystems (Win32k.sys) auch eine eigene Datenstruktur (W32THREAD), auf die die KTHREAD-Struktur zeigt. Interne Strukturen von Threads Kapitel 4 217
Die Win32k-Threadstruktur gehört zur Exekutive, ist eine Struktur höherer Ebene und steht im Zusammenhang mit der Grafikdarstellung. Dass sich der Zeiger da rauf in der KTHREAD- und nicht in der ETHREAD-Struktur befindet, scheint Schichtverletzung oder ein Versehen in der Abstraktionsarchitektur des Standardkernels zu sein. Der Scheduler und andere maschinennahe Komponenten verwenden dieses Feld nicht. Die meisten Felder in Abb. 4–1 sind selbsterklärend. Das erste Element der ETHREAD-Struktur ist der Threadsteuerungsblock (TCB), bei dem es sich um eine Struktur vom Typ KTHREAD handelt. Auf ihn folgen Angaben zur Bezeichnung des Threads und des Prozesses (unter anderem ein Zeiger auf den Besitzerprozess, sodass Zugriff auf dessen Umgebungsinformationen besteht), Sicherheitsinformationen in Form eines Zeigers auf das Zugriffstoken und Angaben für Identitätswechsel, Felder für ALPC-Nachrichten (Advanced Local Procedure Calls) und ausstehende E/A-Anforderungen (IRPs) sowie Windows 10-spezifische Felder zur Energieverwaltung (siehe Kapitel 6) und zu CPU-Sätzen (weiter hinten in diesem Kapitel erklärt). Einige dieser Felder werden in diesem Buch noch ausführlicher erläutert. Um sich den Aufbau einer ETHREAD-Struktur genau anzusehen, können Sie mit dem Kerneldebuggerbefehl dt ihr Format anzeigen lassen. Sehen wir uns nun zwei der bereits erwähnten wichtigen Threaddatenstrukturen genauer an, nämlich die ETHREAD- und die KTHREAD-Struktur. Die KTHREAD-Struktur ist das Element Tcb der ETHREAD-Struktur und enthält Informationen, die der Windows-Kernel benötigt, um Threadplanungs-, Synchronisierungs- und Zeiteinhaltungsfunktionen auszuführen. Experiment: ETHREAD- und KTHREAD-Strukturen anzeigen Die ETHREAD- und KTHREAD-Strukturen können Sie mit dem Befehl dt des Kerneldebuggers einsehen. Die folgende Ausgabe zeigt das Format einer ETHREAD-Struktur auf einem 64-Bit-System mit Windows 10: lkd> dt nt!_ethread +0x000 Tcb : _KTHREAD +0x5d8 CreateTime : _LARGE_INTEGER +0x5e0 ExitTime : _LARGE_INTEGER ... +0x7a0 EnergyValues : Ptr64 _THREAD_ENERGY_VALUES +0x7a8 CmCellReferences : Uint4B +0x7b0 SelectedCpuSets : Uint8B +0x7b0 SelectedCpuSetsIndirect : Ptr64 Uint8B +0x7b8 Silo : Ptr64 _EJOB Die KTHREAD-Struktur können Sie mit einem ähnlichen Befehl ausgeben lassen. Sie können jedoch auch wie im Experiment »Das Format einer EPROCESS-Struktur anzeigen« aus Kapitel 3 den Befehl dt nt!_ETHREAD Tcb verwenden: → 218 Kapitel 4 Threads
lkd> dt nt!_kthread +0x000 Header : _DISPATCHER_HEADER +0x018 SListFaultAddress : Ptr64 Void +0x020 QuantumTarget : Uint8B +0x028 InitialStack : Ptr64 Void +0x030 StackLimit : Ptr64 Void +0x038 StackBase : Ptr64 Void +0x040 ThreadLock : Uint8B +0x048 CycleTime : Uint8B +0x050 CurrentRunTime : Uint4B ... +0x5a0 ReadOperationCount : Int8B +0x5a8 WriteOperationCount : Int8B +0x5b0 OtherOperationCount : Int8B +0x5b8 ReadTransferCount : Int8B +0x5c0 WriteTransferCount : Int8B +0x5c8 OtherTransferCount : Int8B +0x5d0 QueuedScb : Ptr64 _KSCB Experiment: Der Befehl !thread des Kerneldebuggers Der Befehl !thread des Kerneldebuggers gibt einen Teil der Informationen in den Threaddatenstrukturen aus. Einige davon können von keinem anderen Programm angezeigt werden, darunter die folgenden: QQ Adressen von internen Strukturen QQ Angaben zur Priorität QQ Informationen über den Stack QQ Liste der ausstehenden E/A-Anforderungen QQ Liste der Objekte, auf die ein Thread wartet Um Threadinformationen einzusehen, verwenden Sie den Befehl !process, der zuerst Informationen über den Prozess und dann über alle darin enthaltenen Threads ausgibt, oder den Befehl !thread mit der Adresse eines Threadobjekts, wenn Sie nur einen bestimmten Thread anzeigen lassen wollen. Im folgenden Beispiel sehen wir uns zunächst alle Instanzen von Explorer.exe an: lkd> !process 0 0 explorer.exe PROCESS ffffe00017f3e7c0 SessionId: 1 Cid: 0b7c Peb: 00291000 ParentCid: 0c34 DirBase: 19b264000 ObjectTable: ffffc00007268cc0 HandleCount: 2248. Image: explorer.exe → Interne Strukturen von Threads Kapitel 4 219
PROCESS ffffe00018c817c0 SessionId: 1 Cid: 23b0 Peb: 00256000 ParentCid: 03f0 DirBase: 2d4010000 ObjectTable: ffffc0001aef0480 HandleCount: 2208. Image: explorer.exe Anschließend wählen wir eine dieser Instanzen aus und lassen deren Threads anzeigen: lkd> !process ffffe00018c817c0 2 PROCESS ffffe00018c817c0 SessionId: 1 Cid: 23b0 Peb: 00256000 ParentCid: 03f0 DirBase: 2d4010000 ObjectTable: ffffc0001aef0480 HandleCount: 2232. Image: explorer.exe THREAD ffffe0001ac3c080 Cid 23b0.2b88 Teb: 0000000000257000 Win32Thread: ffffe0001570ca20 WAIT: (UserRequest) UserMode Non-Alertable ffffe0001b6eb470 SynchronizationEvent THREAD ffffe0001af10800 Cid 23b0.2f40 Teb: 0000000000265000 Win32Thread: ffffe000156688a0 WAIT: (UserRequest) UserMode Non-Alertable ffffe000172ad4f0 SynchronizationEvent ffffe0001ac26420 SynchronizationEvent THREAD ffffe0001b69a080 Cid 23b0.2f4c Teb: 0000000000267000 Win32Thread: ffffe000192c5350 WAIT: (UserRequest) UserMode Non-Alertable ffffe00018d83c00 SynchronizationEvent ffffe0001552ff40 SynchronizationEvent ... THREAD ffffe00023422080 Cid 23b0.3d8c Teb: 00000000003cf000 Win32Thread: ffffe0001eccd790 WAIT: (WrQueue) UserMode Alertable ffffe0001aec9080 QueueObject THREAD ffffe00023f23080 Cid 23b0.3af8 Teb: 00000000003d1000 Win32Thread: 0000000000000000 WAIT: (WrQueue) UserMode Alertable ffffe0001aec9080 QueueObject THREAD ffffe000230bf800 Cid 23b0.2d6c Teb: 00000000003d3000 Win32Thread: 0000000000000000 WAIT: (WrQueue) UserMode Alertable ffffe0001aec9080 QueueObject THREAD ffffe0001f0b5800 Cid 23b0.3398 Teb: 00000000003e3000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Alertable ffffe0001d19d790 SynchronizationEvent ffffe00022b42660 SynchronizationTimer Die Threadliste ist hier aus Platzgründen gekürzt. Für jeden Thread wird die Adresse (ETHREAD) angezeigt, die an den Befehl !thread übergeben werden kann; die Client-ID (Cid), also die Prozess- und Thread-ID (wobei die Prozess-ID bei allen hier gezeigten Threads identisch ist, da sie alle zum selben Explorer.exe-Prozess gehören); der Threadumgebungsblock (TEB; Erklärung folgt in Kürze); und der Threadstatus (meistens Wait, wobei der Grund für den Wartevorgang in Klammern angegeben ist). In der anschließenden Zeile können die Synchronisierungsobjekte angezeigt werden, auf die der Thread wartet. → 220 Kapitel 4 Threads
Um mehr Informationen über einen bestimmten Thread zu gewinnen, übergeben Sie mit dem Befehl !thread seine Adresse: lkd> !thread ffffe0001d45d800 THREAD ffffe0001d45d800 Cid 23b0.452c Teb: 000000000026d000 Win32Thread: ffffe0001aace630 WAIT: (UserRequest) UserMode Non-Alertable ffffe00023678350 NotificationEvent ffffe00022aeb370 Semaphore Limit 0xffff ffffe000225645b0 SynchronizationEvent Not impersonating DeviceMap ffffc00004f7ddb0 Owning Process ffffe00018c817c0 Image: explorer.exe Attached Process N/A Image: N/A Wait Start TickCount 7233205 Ticks: 270 (0:00:00:04.218) Context Switch Count 6570 IdealProcessor: 7 UserTime 00:00:00.078 KernelTime 00:00:00.046 Win32 Start Address 0c Stack Init ffffd000271d4c90 Current ffffd000271d3f80 Base ffffd000271d5000 Limit ffffd000271cf000 Call 0000000000000000 Priority 9 BasePriority 8 PriorityDecrement 0 IoPriority 2 PagePriority 5 GetContextState failed, 0x80004001 Unable to get current machine context, HRESULT 0x80004001 Child-SP RetAddr : Args to Child : Call Site ffffd000'271d3fc0 fffff803'bef086ca : 00000000'00000000 00000000'00000001 00000000'00000000 00000000'00000000 : nt!KiSwapContext+0x76 ffffd000'271d4100 fffff803'bef08159 : ffffe000'1d45d800 fffff803'00000000 ffffe000'1aec9080 00000000'0000000f : nt!KiSwapThread+0x15a ffffd000'271d41b0 fffff803'bef09cfe : 00000000'00000000 00000000'00000000 ffffe000'0000000f 00000000'00000003 : nt!KiCommitThreadWait+0x149 ffffd000'271d4240 fffff803'bf2a445d : ffffd000'00000003 ffffd000'271d43c0 00000000'00000000 fffff960'00000006 : nt!KeWaitForMultipleObjects+0x24e ffffd000'271d4300 fffff803'bf2fa246 : fffff803'bf1a6b40 ffffd000'271d4810 ffffd000'271d4858 ffffe000'20aeca60 : nt!ObWaitForMultipleObjects+0x2bd ffffd000'271d4810 fffff803'befdefa3 : 00000000'00000fa0 fffff803'bef02aad ffffe000'1d45d800 00000000'1e22f198 : nt!NtWaitForMultipleObjects+0xf6 ffffd000'271d4a90 00007ffe'f42b5c24 : 00000000'00000000 00000000'00000000 00000000'00000000 00000000'00000000 : nt!KiSystemServiceCopyEnd+0x13 (TrapFrame @ ffffd000'271d4b00) 00000000'1e22f178 00000000'00000000 : 00000000'00000000 00000000'00000000 00000000'00000000 00000000'00000000 : 0x00007ffe'f42b5c24 Es wird eine Menge an Informationen über den Thread angezeigt, darunter seine Priorität, Angaben zum Stack, Benutzer- und Kernelzeiten usw. Viele dieser Informationen werden Sie in diesem Kapitel sowie in Kapitel 5 und 6 noch näher betrachten. Interne Strukturen von Threads Kapitel 4 221
Experiment: Threadinformationen mit tlist einsehen Das folgende Listing ist die ausführliche Anzeige eines Prozesses, wie sie tlist in den Debugging Tools for Windows ausgibt. (Achten Sie darauf, die passende tlist-Version für die Bitversion des Zielprozesses zu verwenden.) Hier wird in der Threadliste Win32StartAddr angezeigt, was die Adresse ist, die die Anwendung an die Funktion CreateThread übergeben hat. Alle anderen Programme (außer Process Explorer) zeigen als Threadstartadresse jedoch wirklich die tatsächliche Adresse an (eine Funktion in Ntdll.dll) und nicht die von der Anwendung angegebene. Im Folgenden sehen Sie die gekürzte Ausgabe von tlist für Word 2016: C:\Dbg\x86>tlist winword 120 WINWORD.EXE Chapter04.docm - Word CWD: C:\Users\pavely\Documents\ CmdLine: "C:\Program Files (x86)\Microsoft Office\Root\Office16\WINWORD. EXE" /n "D:\OneDrive\WindowsInternalsBook\7thEdition\Chapter04\Chapter04.docm VirtualSize: 778012 KB PeakVirtualSize: 832680 KB WorkingSetSize:185336 KB PeakWorkingSetSize:227144 KB NumberOfThreads: 45 12132 Win32StartAddr:0x00921000 LastErr:0x00000000 State:Waiting 15540 Win32StartAddr:0x6cc2fdd8 LastErr:0x00000000 State:Waiting 7096 Win32StartAddr:0x6cc3c6b2 LastErr:0x00000006 State:Waiting 17696 Win32StartAddr:0x77c1c6d0 LastErr:0x00000000 State:Waiting 17492 Win32StartAddr:0x77c1c6d0 LastErr:0x00000000 State:Waiting 4052 Win32StartAddr:0x70aa5cf7 LastErr:0x00000000 State:Waiting 14096 Win32StartAddr:0x70aa41d4 LastErr:0x00000000 State:Waiting 6220 Win32StartAddr:0x70aa41d4 LastErr:0x00000000 State:Waiting 7204 Win32StartAddr:0x77c1c6d0 LastErr:0x00000000 State:Waiting 1196 Win32StartAddr:0x6ea016c0 LastErr:0x00000057 State:Waiting 8848 Win32StartAddr:0x70aa41d4 LastErr:0x00000000 State:Waiting 3352 Win32StartAddr:0x77c1c6d0 LastErr:0x00000000 State:Waiting 11612 Win32StartAddr:0x77c1c6d0 LastErr:0x00000000 State:Waiting 17420 Win32StartAddr:0x77c1c6d0 LastErr:0x00000000 State:Waiting 13612 Win32StartAddr:0x77c1c6d0 LastErr:0x00000000 State:Waiting 15052 Win32StartAddr:0x77c1c6d0 LastErr:0x00000000 State:Waiting ... 12080 Win32StartAddr:0x77c1c6d0 LastErr:0x00000000 State:Waiting 9456 Win32StartAddr:0x77c1c6d0 LastErr:0x00002f94 State:Waiting 9808 Win32StartAddr:0x77c1c6d0 LastErr:0x00000000 State:Waiting 16208 Win32StartAddr:0x77c1c6d0 LastErr:0x00000000 State:Waiting 9396 Win32StartAddr:0x77c1c6d0 LastErr:0x00000000 State:Waiting 2688 Win32StartAddr:0x70aa41d4 LastErr:0x00000000 State:Waiting → 222 Kapitel 4 Threads
9100 Win32StartAddr:0x70aa41d4 LastErr:0x00000000 State:Waiting 18364 Win32StartAddr:0x70aa41d4 LastErr:0x00000000 State:Waiting 11180 Win32StartAddr:0x70aa41d4 LastErr:0x00000000 State:Waiting 16.0.6741.2037 shp 0x00920000 C:\Program Files (x86)\Microsoft Office\Root\ Office16\WINWORD.EXE 10.0.10586.122 shp 0x77BF0000 C:\windows\SYSTEM32\ntdll.dll 10.0.10586.0 shp 0x75540000 C:\windows\SYSTEM32\KERNEL32.DLL 10.0.10586.162 shp 0x77850000 C:\windows\SYSTEM32\KERNELBASE.dll 10.0.10586.63 shp 0x75AF0000 C:\windows\SYSTEM32\ADVAPI32.dll ... 10.0.10586.0 shp 0x68540000 C:\Windows\SYSTEM32\VssTrace.DLL 10.0.10586.0 shp 0x5C390000 C:\Windows\SYSTEM32\adsldpc.dll 10.0.10586.122 shp 0x5DE60000 C:\Windows\SYSTEM32\taskschd.dll 10.0.10586.0 shp 0x5E3F0000 C:\Windows\SYSTEM32\srmstormod.dll 10.0.10586.0 shp 0x5DCA0000 C:\Windows\SYSTEM32\srmscan.dll 10.0.10586.0 shp 0x5D2E0000 C:\Windows\SYSTEM32\msdrm.dll 10.0.10586.0 shp 0x711E0000 C:\Windows\SYSTEM32\srm_ps.dll 10.0.10586.0 shp 0x56680000 C:\windows\System32\OpcServices.dll 0x5D240000 C:\Program Files (x86)\Common Files\Microsoft Shared\Office16\WXPNSE.DLL 16.0.6701.1023 shp 0x77E80000 C:\Program Files (x86)\Microsoft Office\Root\ Office16\GROOVEEX.DLL 10.0.10586.0 shp 0x693F0000 C:\windows\system32\dataexchange.dll Der TEB aus Abb. 4–3 ist eine der Datenstrukturen, die sich nicht im System-, sondern im Prozessadressraum befinden. Sein Header ist der Threadinformationsblock (TIB), der hauptsächlich zur Kompatibilität mit OS/2- und Win9x-Anwendungen da ist. Durch die Verwendung eines Anfangs-TIBs ist es auch möglich, Ausnahme- und Stackinformationen beim Erstellen von neuen Threads in kleineren Strukturen unterzubringen. Interne Strukturen von Threads Kapitel 4 223
Stackbasis Stackgrenzwert Threadinformationsblock (TIB) Teilsystem-TIB Client-ID Fiberarten Aktives RPC-Handle Ausnahmeliste TLS-Slots (lokaler Threadspeicher) Zeiger auf sich selbst Letzter Fehlerwert PEB des Elternprozesses PEB GDI-Informationen User32-Clientinformationen OpenGL-Informationen Winsock-Daten Idealer Prozessor ETW-Ablaufverfolgungsdaten COM/OLE-Daten Instrumentierungsdaten WDF-Daten TxF-Daten Zähler für Sperren im eigenen Besitz Abbildung 4–3 Wichtige Felder des Threadumgebungsblocks Im TEB sind Kontextinformationen für den Abbildlader und verschiedene Windows-DLLs gespeichert. Da diese Komponenten im Benutzermodus laufen, brauchen Sie eine Datenstruktur, die vom Benutzermodus aus schreibbar ist. Aus diesem Grund ist diese Struktur im Prozess- und nicht im Systemadressraum untergebracht, wo sie nur im Kernelmodus schreibbar wäre. Die Adresse des TEB können Sie mit dem Kerneldebuggerbefehl !thread herausfinden. Experiment: Den TEB untersuchen Mit dem Befehl !teb des Kernel- oder Benutzermodusdebuggers können Sie die TEB-Struktur ausgeben. Ohne weitere Angaben wird dabei der TEB für den aktuellen Thread des Debuggers angezeigt. Um den TEB eines anderen Threads einzusehen, geben Sie die TEB-Adresse an. Wenn Sie den Kerneldebugger einsetzen, muss der aktuelle Prozess festgelegt sein, bevor Sie den Befehl an die TEB-Adresse ausgeben, damit der richtige Prozesskontext verwendet wird. Wie Sie den TEB mit dem Kerneldebugger ausgeben, erfahren Sie im nächsten Experiment. Zur Anzeige des TEB mit dem Benutzermodusdebugger gehen Sie wie folgt vor: → 224 Kapitel 4 Threads
1. Öffnen Sie WinDbg. 2. Öffnen Sie das Menü File und wählen Sie Run Executable. 3. Wechseln Sie zu C:\Windows\System32\Notepad.exe. Der Debugger sollte am ersten Haltepunkt anhalten. 4. Geben Sie den Befehl !teb ein, um sich den TEB des einzigen zurzeit existierenden Threads anzusehen. Das folgende Beispiel zeigt die Ausgabe auf einem 64-BitWindows-System: 0:000> !teb TEB at 000000ef125c1000 ExceptionList: 0000000000000000 StackBase: 000000ef12290000 StackLimit: 000000ef1227f000 SubSystemTib: 0000000000000000 FiberData: 0000000000001e00 ArbitraryUserPointer: 0000000000000000 Self: 000000ef125c1000 EnvironmentPointer: 0000000000000000 ClientId: 00000000000021bc . 0000000000001b74 RpcHandle: 0000000000000000 Tls Storage: 00000266e572b600 PEB Address: 000000ef125c0000 LastErrorValue: 0 LastStatusValue: 0 Count Owned Locks: 0 HardErrorMode: 0 5. Geben Sie den Befehl g ein oder drücken Sie (F5), um die Ausführung des Editors fortzusetzen. 6. Klicken Sie im Editor auf Datei und Öffnen und dann auf Abbrechen, um das Öffnen-Dialogfeld zu schließen. 7. Drücken Sie (Strg) + (Untbr) oder öffnen Sie das Menü Debug und wählen Sie Break, um den Prozess zwangsweise anzuhalten. 8. Geben Sie den Befehl ~ (Tilde) ein, um alle Threads in dem Prozess anzuzeigen. Dadurch ergibt sich folgende Ausgabe: 0:005> ~ 0 Id: 21bc.1b74 Suspend: 1 Teb: 000000ef'125c1000 Unfrozen 1 Id: 21bc.640 Suspend: 1 Teb: 000000ef'125e3000 Unfrozen 2 Id: 21bc.1a98 Suspend: 1 Teb: 000000ef'125e5000 Unfrozen 3 Id: 21bc.860 Suspend: 1 Teb: 000000ef'125e7000 Unfrozen 4 Id: 21bc.28e0 Suspend: 1 Teb: 000000ef'125c9000 Unfrozen → Interne Strukturen von Threads Kapitel 4 225
. 5 Id: 21bc.23e0 Suspend: 1 Teb: 000000ef'12400000 Unfrozen 6 Id: 21bc.244c Suspend: 1 Teb: 000000ef'125eb000 Unfrozen 7 Id: 21bc.168c Suspend: 1 Teb: 000000ef'125ed000 Unfrozen 8 Id: 21bc.1c90 Suspend: 1 Teb: 000000ef'125ef000 Unfrozen 9 Id: 21bc.1558 Suspend: 1 Teb: 000000ef'125f1000 Unfrozen 10 Id: 21bc.a64 Suspend: 1 Teb: 000000ef'125f3000 Unfrozen 11 Id: 21bc.20c4 Suspend: 1 Teb: 000000ef'125f5000 Unfrozen 12 Id: 21bc.1524 Suspend: 1 Teb: 000000ef'125f7000 Unfrozen 13 Id: 21bc.1738 Suspend: 1 Teb: 000000ef'125f9000 Unfrozen 14 Id: 21bc.f48 Suspend: 1 Teb: 000000ef'125fb000 Unfrozen 15 Id: 21bc.17bc Suspend: 1 Teb: 000000ef'125fd000 Unfrozen 9. Für jeden Thread wird die TEB-Adresse angezeigt. Um sich einen bestimmten Thread anzusehen, übergeben Sie mit dem Befehl !teb seine TEB-Adresse. Für Thread 9 aus der vorherigen Ausgabe sieht das wie folgt aus: 0:005> !teb 000000ef'125f1000 TEB at 000000ef125f1000 ExceptionList: 0000000000000000 StackBase: 000000ef13400000 StackLimit: 000000ef133ef000 SubSystemTib: 0000000000000000 FiberData: 0000000000001e00 ArbitraryUserPointer: 0000000000000000 Self: 000000ef125f1000 EnvironmentPointer: 0000000000000000 ClientId: 00000000000021bc . 0000000000001558 RpcHandle: 0000000000000000 Tls Storage: 00000266ea1af280 PEB Address: 000000ef125c0000 LastErrorValue: 0 LastStatusValue: c0000034 Count Owned Locks: 0 HardErrorMode: 0 10. Natürlich ist es auch möglich, sich die tatsächliche Struktur an der TEB-Adresse anzusehen (hier aus Platzgründen gekürzt): 0:005> dt ntdll!_teb 000000ef'125f1000 +0x000 NtTib : _NT_TIB +0x038 EnvironmentPointer : (null) +0x040 ClientId : _CLIENT_ID +0x050 ActiveRpcHandle : (null) +0x058 ThreadLocalStoragePointer : 0x00000266'ea1af280 Void → 226 Kapitel 4 Threads
+0x060 ProcessEnvironmentBlock +0x068 LastErrorValue : 0x000000ef'125c0000 _PEB : 0 +0x06c CountOfOwnedCriticalSections : 0 ... +0x1808 LockCount : 0 +0x180c WowTebOffset : 0n0 +0x1810 ResourceRetValue : 0x00000266'ea2a5e50 Void +0x1818 ReservedForWdf : (null) +0x1820 ReservedForCrt : 0 +0x1828 EffectiveContainerId : _GUID {00000000-0000-0000-0000000000000000} Experiment: Den TEB mit einem Kerneldebugger untersuchen Um sich den TEB mit dem Kerneldebugger anzusehen, gehen Sie wie folgt vor: 1. Suchen Sie den Prozess des Threads, für dessen TEB Sie sich interessieren. Beispielsweise gibt der folgende Befehl die Prozesse von Explorer.exe jeweils mit einer Liste ihrer Threads und einigen grundlegenden Informationen dazu aus (hier gekürzt): lkd> !process 0 2 explorer.exe PROCESS ffffe0012bea7840 SessionId: 2 Cid: 10d8 Peb: 00251000 ParentCid: 10bc DirBase: 76e12000 ObjectTable: ffffc000e1ca0c80 HandleCount: <Data Not Accessible> Image: explorer.exe THREAD ffffe0012bf53080 Cid 10d8.10dc Teb: 0000000000252000 Win32Thread: ffffe0012c1532f0 WAIT: (WrUserRequest) UserMode Non-Alertable ffffe0012c257fe0 SynchronizationEvent THREAD ffffe0012a30f080 Cid 10d8.114c Teb: 0000000000266000 Win32Thread: ffffe0012c2e9a20 WAIT: (UserRequest) UserMode Alertable ffffe0012bab85d0 SynchronizationEvent THREAD ffffe0012c8bd080 Cid 10d8.1178 Teb: 000000000026c000 Win32Thread: ffffe0012a801310 WAIT: (UserRequest) UserMode Alertable ffffe0012bfd9250 NotificationEvent ffffe0012c9512f0 NotificationEvent ffffe0012c876b80 NotificationEvent ffffe0012c010fe0 NotificationEvent ffffe0012d0ba7e0 NotificationEvent ffffe0012cf9d1e0 NotificationEvent ... → Interne Strukturen von Threads Kapitel 4 227
THREAD ffffe0012c8be080 Cid 10d8.1180 Teb: 0000000000270000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Alertable fffff80156946440 NotificationEvent THREAD ffffe0012afd4040 Cid 10d8.1184 Teb: 0000000000272000 Win32Thread: ffffe0012c7c53a0 WAIT: (UserRequest) UserMode Non-Alertable ffffe0012a3dafe0 NotificationEvent ffffe0012c21ee70 Semaphore Limit 0xffff ffffe0012c8db6f0 SynchronizationEvent THREAD ffffe0012c88a080 Cid 10d8.1188 Teb: 0000000000274000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Alertable ffffe0012afd4920 NotificationEvent ffffe0012c87b480 SynchronizationEvent ffffe0012c87b400 SynchronizationEvent ... 2. Gibt es mehr als einen Explorer.exe-Prozess, wählen Sie für die folgenden Schritte einen davon nach Belieben aus. 3. Für jeden Thread wird die TEB-Adresse angezeigt. Da sich der TEB im Benutzerraum befindet, hat diese Adresse nur im Kontext des zugehörigen Prozesses eine Bedeutung. Sie müssen zu dem Prozess/Thread umschalten, den der Debugger sieht. Wählen Sie den ersten Explorerthread aus, da sich dessen Kernelstack wahrscheinlich im physischen Arbeitsspeicher befindet. Bei einem Thread, bei dem das nicht der Fall ist, erhalten Sie eine Fehlermeldung. lkd> .thread /p ffffe0012bf53080 Implicit thread is now ffffe001'2bf53080 Implicit process is now ffffe001'2bea7840 4. Dadurch wird der Kontext auf den angegebenen Thread umgeschaltet (und damit auch auf den entsprechenden Prozess). Jetzt können Sie den Befehl !teb mit der für den Thread angegebenen TEB-Adresse verwenden: lkd> !teb 0000000000252000 TEB at 0000000000252000 ExceptionList: 0000000000000000 StackBase: 00000000000d0000 StackLimit: 00000000000c2000 SubSystemTib: 0000000000000000 FiberData: 0000000000001e00 ArbitraryUserPointer: 0000000000000000 Self: 0000000000252000 EnvironmentPointer: 0000000000000000 ClientId: 00000000000010d8 . 00000000000010dc RpcHandle: 0000000000000000 Tls Storage: 0000000009f73f30 → 228 Kapitel 4 Threads
PEB Address: 0000000000251000 LastErrorValue: 0 LastStatusValue: c0150008 Count Owned Locks: 0 HardErrorMode: 0 Die CSR_THREAD-Struktur aus Abb. 4–4 entspricht der CSR_PROCESS-Struktur, allerdings für Threads. Wie Sie wissen, wird Letztere von jedem Csrss-Prozess in einer Sitzung unterhalten und gibt die Windows-Teilsystemthreads an, die darin laufen. In der CSR_THREAD-Struktur befinden sich ein Handle, den Csrss für den Thread verwendet, mehrere Flags, die Client-ID (Thread- und Prozess-ID) sowie die Angabe der Threaderstellungszeit. Threads werden bei Csrss registriert, wenn sie ihre erste Nachricht an Csrss senden, gewöhnlich, weil eine API die Benachrichtigung von Scrss über eine Operation oder Bedingung erfordert. Erstellungszeit Threadlink Hashlinks Client-ID Threadhandle Flags Referenzzähler Zähler für Identitätswechsel Abbildung 4–4 Felder der CSR_THREAD-Struktur Experiment: Die CSR_THREAD-Struktur untersuchen Sie können die CSR_THREAD-Struktur mit dem Befehl dt des Kernelmodusdebuggers ausgeben, wenn sich der Debugger im Kontext des Csrss-Prozesses befindet. Befolgen Sie dazu die Anweisungen aus dem Experiment »Die CSR_PROZESS-Struktur untersuchen« aus Kapitel 3. Die folgende Beispielausgabe stammt von einem x64-System mit Windows 10: lkd> dt csrss!_csr_thread +0x000 CreateTime : _LARGE_INTEGER +0x008 Link : _LIST_ENTRY +0x018 HashLinks : _LIST_ENTRY +0x028 ClientId : _CLIENT_ID +0x038 Process : Ptr64 _CSR_PROCESS +0x040 ThreadHandle : Ptr64 Void +0x048 Flags : Uint4B +0x04c ReferenceCount : Int4B +0x050 ImpersonateCount : Uint4B Interne Strukturen von Threads Kapitel 4 229
Die W32THREAD-Struktur aus Abb. 4–5 entspricht der W32PROCESS-Struktur, allerdings für Threads. Sie enthält hauptsächlich Informationen für das GDI-Teilsystem (Pinsel- und Gerätekontextattribute) und DirectX, aber auch für das UMPD-Framework (User-Mode Print Driver), mit dem Hersteller Benutzermodus-Druckertreiber schreiben. Des Weiteren enthält sie den RenderingStatus, der für Desktop-Compositing und Antialiasing genutzt wird. Thread Referenzzähler Pinsel/DC-Attribute Benutzermodus-Druckdaten Spritestatus Flags Rendering-/AA-Daten Threadsperre DirectX-Thread Abbildung 4–5 Felder der W32THREAD-Struktur Geburt eines Threads Der Lebenszyklus eines Threads beginnt, wenn ein Prozess (im Kontext eines Threads, z. B. des Threads, der die main-Funktion ausführt) einen neuen Thread erstellt. Die Anforderung geht schließlich an die Windows-Exekutive, wo der Prozess-Manager Speicherplatz für ein Threadobjekt zuweist und den Kernel aufruft, um den Threadsteuerungsblock (KTHREAD) zu initialisieren. Wie bereits erwähnt, führen die verschiedenen Threaderstellungsfunktionen letzten Endes dazu, dass CreateRemoteThreadEx ausgeführt wird. Innerhalb dieser Funktion in Kernel32.dll werden dann die folgenden Schritte ausgeführt, um einen Windows-Thread zu erstellen: 1. Die Funktion wandelt die Windows-API-Parameter in native Flags um und erstellt eine native Struktur, die die Objektparameter beschreibt (OBJECT_ATTRIBUTES, siehe Kapitel 8 in Band 2). 2. Sie erstellt eine Attributliste mit zwei Einträgen, nämlich der Client-ID und der TEB-Adresse (siehe »Der Ablauf von CreateProcess« in Kapitel 3). 3. Sie bestimmt, ob der Thread im aufrufenden Prozess oder in einem anderen Prozess erstellt wird, der durch ein übergebenes Handle bestimmt wird. Ist das Handle gleich dem von GetCurrentProcess zurückgegebenen Pseudohandle (Wert -1), dann ist es derselbe Prozess. Ein anderes Handle kann immer noch ein gültiges Handle für denselben Prozess sein, weshalb NtQueryInformationProcess (in Ntdll) aufgerufen wird, um herauszufinden, ob das der Fall ist. 4. Sie ruft NtCreateThreadEx (in Ntdll) auf, um den Wechsel zur Exekutive im Kernelmodus vorzunehmen. Der Vorgang wird dann in einer Funktion mit demselben Namen und denselben Argumenten fortgesetzt. 230 Kapitel 4 Threads
5. NtCreateThreadEx (in der Exekutive) erstellt und initialisiert den Benutzermodus-Threadkontext (dessen Struktur von der Architektur abhängt) und ruft dann PspCreateThread auf, um ein angehaltenes Exekutivthreadobjekt zu erstellen. (Eine Beschreibung der Schritte, die diese Funktion ausführt, finden Sie in den Phasen 3 und 5 unter »Der Ablauf von Cre ateProcess« in Kapitel 3.) Danach gibt die Funktion die Steuerung zurück und landet wieder im Benutzermodus bei CreateRemoteThreadEx. 6. CreateRemoteThreadEx weist einen Aktivierungskontext für den Thread zu, der von der Side- by-Side-Assemblyunterstützung verwendet wird. Anschließend fragt sie den Aktivierungsstack ab, um zu sehen, ob eine Aktivierung erforderlich ist, und führt diese bei Bedarf durch. Der Aktivierungsstackzeiger wird im TEB des neuen Threads gespeichert. 7. Sofern der Aufrufer den Thread nicht mit dem Flag CREATE_SUSPENDED erstellt hat, wird der Thread jetzt aufgenommen, sodass er zur Ausführung eingeplant werden kann. Wenn ein Thread zu laufen beginnt, werden die im Abschnitt »Phase 7: Prozessinitialisierung im Kontext des neuen Prozesses durchführen« von Kapitel 3 beschriebenen Schritte ausgeführt, bevor die angegebene Startadresse des eigentlichen Benutzers aufgerufen wird. 8. Das Threadhandle und die Thread-ID werden an den Aufrufer zurückgegeben. Die Threadaktivität untersuchen Eine Untersuchung der Threadaktivität ist vor allem dann wichtig, wenn Sie herauszufinden versuchen, warum ein Hostprozess für mehrere Dienste (wie Svchost.exe, Dllhosts.exe oder Lsass.exe) läuft oder warum ein Prozess nicht mehr reagiert. Es gibt eine Reihe von Werkzeugen, die einen Blick auf verschiedene Elemente des Status von Windows-Threads zulassen, z. B. WinDbg (beim Anschluss an einen Benutzerprozess und im Kerneldebuggingmodus), die Leistungsüberwachung und Process Explorer. (Werkzeuge zur Anzeige von Threadplanungsinformationen sind im Abschnitt »Threadplanung« angegeben.) Um mit Process Explorer die Threads in einem Prozess einzusehen, doppelklicken Sie auf den Prozess, um sein Eigenschaftendialogfeld zu öffnen. Alternativ können Sie auch auf den Prozess rechtsklicken und Properties wählen. Wechseln Sie anschließend zur Registerkarte Threads. Sie zeigt eine Liste der Threads in dem Prozess sowie vier Spalten mit Informationen darüber an: die ID des Threads, seinen prozentualen Anteil der CPU-Nutzung (auf der Grundlage des eingestellten Aktualisierungsintervalls), die Anzahl der ihm zugeschlagenen Zyklen und seine Startadresse. Sie können die Anzeige nach allen vier Spalten sortieren. Neu erstellte Threads werden grün hervorgehoben, Threads, die beendet werden, in Rot. (Die Dauer dieser Hervorhebung können Sie im Menü Options mit Difference Highlight Dura tion ändern.) Das kann praktisch sein, um eine unnötige Threaderstellung in einem Prozess aufzuspüren. (Im Allgemeinen sollten Threads beim Start des Prozesses erstellt werden und nicht jedes Mal, wenn in einem Prozess eine Anforderung verarbeitet wird.) Wenn Sie einen Thread in der Liste auswählen, zeigt Process Explorer jeweils die Thread-ID, den Startzeitpunkt, den Status, die CPU-Zeit, die Anzahl der Zyklen, die Anzahl der Kontextwechsel, den idealen Prozessor und seine Gruppe sowie die E/A-, die Speicher-, die Basis- und die aktuelle (dynamische) Priorität an. Mit der Schaltfläche Kill können Sie einen einzelnen Die Threadaktivität untersuchen Kapitel 4 231
Thread beenden, allerdings sollten Sie dabei äußerste Vorsicht walten lassen. Die Schaltfläche Suspend unterbindet die fortgesetzte Ausführung eines Threads und verhindert so, dass ein außer Kontrolle geratener Thread CPU-Zeit in Anspruch nimmt. Dies kann jedoch zu Deadlocks führen, weshalb auch diese Schaltfläche ebenso mit Vorsicht zu genießen ist wie Kill. Mit der Schaltfläche Permissions schließlich können Sie den Sicherheitsdeskriptor des Threads einsehen (siehe Kapitel 7). Anders als der Task-Manager und andere Prozess- und Prozessorüberwachungswerkzeuge verwendet der Process Explorer einen Taktzykluszähler für die Threadlaufzeitbestimmung (weiter hinten in diesem Kapitel erklärt) statt eines Taktintervalltimers. Daher wird im Process Explorer eine erheblich abweichende Sicht der CPU-Nutzung angezeigt. Viele Threads laufen so kurz, dass nur selten (wenn überhaupt) der gerade ausgeführte Thread gesehen wird, wenn ein Interrupt für einen Taktintervalltimer auftritt. Daher werden sie für einen Großteil ihrer CPUZeit gar nicht berücksichtigt, weshalb Werkzeuge mit solchen Timern eine CPU-Zeit von 0 % messen. Die Gesamtzahl der Taktzyklen dagegen entspricht der Anzahl der Prozessorzyklen, die für die Threads in dem Prozess aufgelaufen sind. Sie hängt von der Auflösung des Taktintervalltimers an, da der Zähler bei jedem Zyklus intern vom Prozessor geführt und bei jedem Interrupt von Windows aktualisiert wird. (Vor einem Kontextwechsel erfolgt eine abschließende Akkumulation.) Die Threadstartadresse wird im Format Modul! Funktion angezeigt, wobei es sich bei Modul um den Namen der .exe- oder .dll-Datei handelt. Für den Funktionsnamen ist ein Zugriff auf die Symboldateien für das Modul erforderlich (siehe das Experiment »Einzelheiten über Prozesse mit Process Explorer einsehen« in Kapitel 1). Wenn Ihnen nicht klar ist, worum es sich bei dem Modul handelt, klicken Sie auf die Schaltfläche Module, um das Eigenschaftendialogfeld für die Explorerdatei des Moduls mit der Startadresse des Threads zu öffnen (z. B. die .exe- oder .dll-Datei). Bei Threads, die mit der Windows-Funktion CreateThread erstellt wurden, zeigt Process Explorer die an CreateThread übergebene Funktion und nicht die tatsächliche Startfunktion des Threads an. Das liegt daran, dass Windows-Threads mit einer gemeinsamen Threadstart-Wrapperfunktion beginnen (TrlUserThreadStart in Ntdll.dll). Würde Process Explorer die tatsächliche Startadresse anzeigen, dann sähe es so aus, als begännen die meisten Threads in den Prozessen an derselben Adresse, und das wäre nicht sehr hilfreich, um herauszufinden, welchen Code der Thread ausführt. Falls Process Explorer die vom Benutzer angegebene Startadresse nicht abrufen kann (etwa bei einem geschützten Prozess), zeigt er jedoch die Wrapperfunktion RtlUserThreadStart an. Die angezeigte Threadstartadresse reicht unter Umständen nicht aus, um genau festzustellen, was der Thread tut und welche Komponenten in dem Prozess für seinen CPU-Verbrauch verantwortlich sind. Das ist vor allem dann der Fall, wenn die Threadstartadresse eine generische Startfunktion ist, etwa wenn der Funktionsname nicht angibt, was der Thread tatsächlich macht. In diesem Fall kann eine Untersuchung des Threadstacks die Antwort liefern. Dazu doppelklicken Sie auf den betreffenden Thread (oder wählen ihn aus und klicken auf die Schaltfläche 232 Kapitel 4 Threads
Stack). Process Explorer zeigt dann den Stack des Threads an (bei einem Thread im Kernelmodus sowohl den Benutzer- als auch den Kernelstack). Einen Benutzermodusdebugger (WinDbg, Ntsd und Cdb) können Sie an einen Prozess anhängen, um sich den Benutzerstack eines Threads anzeigen zu lassen. In Process Explorer dagegen müssen Sie lediglich auf eine Schaltfläche klicken, um sowohl den Benutzer- als auch den Kernelstack zu sehen. Wie die beiden nächsten Experimente zeigen, können Sie den Benutzer- und den Kernelthreadstack auch mit WinDbg im lokalen Kerneldebuggingmodus untersuchen. Bei der Untersuchung von 32-Bit-Prozessen, die als Wow64-Prozesse auf 64-Bit-Systemen laufen (siehe Kapitel 8 in Band 2), zeigt Process Explorer sowohl den 32- als auch den 64-Bit-Stack für die Threads an. Da ein Thread zum Zeitpunkt des echten (64-Bit-)Systemaufrufs bereits zum 64-Bit-Stack und -Kontext gewechselt ist, würde die Betrachtung des 64-Bit-Stacks nur die halbe Geschichte zeigen, nämlich den 64-Bit-Part des Threads mit dem Thunkcode von Wow64. Bei der Untersuchung von Wow64-Prozessen müssen Sie daher auf jeden Fall sowohl den 32als auch den 64-Bit-Stack berücksichtigen. Experiment: Einen Threadstack mit einem Benutzermodusdebugger anzeigen Gehen Sie wie folgt vor, um WinDbg an einen Prozess anzuhängen und Informationen über den Thread und dessen Stack einzusehen: 1. Führen Sie Notepad.exe und WinDbg.exe aus. 2. Öffnen Sie in WinDbg das Menü File und wählen Sie Attach to Process. 3. Suchen Sie die Notepad.exe-Instanz und klicken Sie auf OK, um den Debugger anzuhängen. Der Debugger sollte jetzt die Ausführung des Editors anhalten. 4. Führen Sie die Threads in dem Prozess mit dem Befehl ~ auf. Für jeden Thread werden die Debugger-ID, die Client-ID (Prozess-ID.Thread-ID), der Unterbrechungszähler (dieser Wert sollte meistens 1 lauten, da der Thread aufgrund des Haltepunktes unterbrochen wurde), die TEB-Adresse und die Angabe angezeigt, ob der Thread mit einem Debuggerbefehl eingefroren wurde. 0:005> ~ 0 Id: 612c.5f68 Suspend: 1 Teb: 00000022'41da2000 Unfrozen 1 Id: 612c.5564 Suspend: 1 Teb: 00000022'41da4000 Unfrozen 2 Id: 612c.4f88 Suspend: 1 Teb: 00000022'41da6000 Unfrozen 3 Id: 612c.5608 Suspend: 1 Teb: 00000022'41da8000 Unfrozen 4 Id: 612c.cf4 Suspend: 1 Teb: 00000022'41daa000 Unfrozen . 5 Id: 612c.9f8 Suspend: 1 Teb: 00000022'41db0000 Unfrozen → Die Threadaktivität untersuchen Kapitel 4 233
5. Beachten Sie den Punkt, der in der Ausgabe vor Thread 5 steht. Er kennzeichnet den aktuellen Debuggerthread. Um den Aufrufstack einzusehen, geben Sie den Befehl k ein: 0:005> k # Child-SP RetAddr Call Site 00 00000022'421ff7e8 00007ff8'504d9031 ntdll!DbgBreakPoint 01 00000022'421ff7f0 00007ff8'501b8102 ntdll!DbgUiRemoteBreakin+0x51 02 00000022'421ff820 00007ff8'5046c5b4 KERNEL32!BaseThreadInitThunk+0x22 03 00000022'421ff850 00000000'00000000 ntdll!RtlUserThreadStart+0x34 6. Der Debugger hat in den Notepad-Prozess einen Thread injiziert, der eine Haltepunktanweisung ausgibt (DbgBreakPoint). Um sich den Aufrufstack eines anderen Threads anzusehen, verwenden Sie den Befehl ~nk, wobei n die in WinDbg angezeigte Threadnummer ist. (Der aktuelle Debuggerthread wird dadurch nicht geändert.) Das folgende Beispiel zeigt die Ausgabe für Thread 2: 0:005> ~2k # Child-SP RetAddr Call Site 00 00000022'41f7f9e8 00007ff8'5043b5e8 ntdll!ZwWaitForWorkViaWorkerFactory+0x14 01 00000022'41f7f9f0 00007ff8'501b8102 ntdll!TppWorkerThread+0x298 02 00000022'41f7fe00 00007ff8'5046c5b4 KERNEL32!BaseThreadInitThunk+0x22 03 00000022'41f7fe30 00000000'00000000 ntdll!RtlUserThreadStart+0x34 7. Um den Debugger auf einen anderen Thread umzuschalten, verwenden Sie den Befehl ~ns (wobei n wiederum die Threadnummer ist). Im folgenden Beispiel schalten wir zu Thread 0 um und zeigen dessen Stack an: 0:005> ~0s USER32!ZwUserGetMessage+0x14: 00007ff8'502e21d4 c3 ret 0:000> k # Child-SP RetAddr Call Site 00 00000022'41e7f048 00007ff8'502d3075 USER32!ZwUserGetMessage+0x14 01 00000022'41e7f050 00007ff6'88273bb3 USER32!GetMessageW+0x25 02 00000022'41e7f080 00007ff6'882890b5 notepad!WinMain+0x27b 03 00000022'41e7f180 00007ff8'341229b8 notepad!__mainCRTStartup+0x1ad 04 00000022'41e7f9f0 00007ff8'5046c5b4 KERNEL32!BaseThreadInitThunk+0x22 05 00000022'41e7fa20 00000000'00000000 ntdll!RtlUserThreadStart+0x34 8. 234 Auch wenn sich der Thread zurzeit im Kernelmodus befindet, zeigt ein Benutzermodusdebugger die letzte Funktion im Benutzermodus an (ZwUserGetMessage in der vorstehenden Ausgabe). Kapitel 4 Threads
Experiment: Einen Threadstack mit einem lokalen Kernelmodusdebugger anzeigen In diesem Experiment verwenden wir einen lokalen Kerneldebugger, um den Benutzer- und den Kernelmodusstack eines Threads einzusehen. Als Beispiel nutzen wir dafür einen der Threads des Explorers, aber Sie können dieses Experiment auch mit anderen Prozessen und Threads ausprobieren. 1. Zeigen Sie alle Prozesse an, die im Abbild Explorer.exe ausgeführt werden. (Wenn Sie die Explorer-Option Ordnerfenster in eigenem Prozess starten gewählt haben, wird mehr als eine Instanz des Explorers angezeigt. Ein Prozess verwaltet den Desktop und die Taskleiste, der andere die Explorer-Fenster.) lkd> !process 0 0 explorer.exe PROCESS ffffe00197398080 SessionId: 1 Cid: 18a0 Peb: 00320000 ParentCid: 1840 DirBase: 17c028000 ObjectTable: ffffc000bd4aa880 HandleCount: <Data Not Accessible> Image: explorer.exe PROCESS ffffe00196039080 SessionId: 1 Cid: 1f30 Peb: 00290000 ParentCid: 0238 DirBase: 24cc7b000 ObjectTable: ffffc000bbbef740 HandleCount: <Data Not Accessible> Image: explorer.exe 2. Wählen Sie eine Instanz aus und lassen Sie sich die Threadübersicht dafür anzeigen: lkd> !process ffffe00196039080 2 PROCESS ffffe00196039080 SessionId: 1 Cid: 1f30 Peb: 00290000 ParentCid: 0238 DirBase: 24cc7b000 ObjectTable: ffffc000bbbef740 HandleCount: <Data Not Accessible> Image: explorer.exe THREAD ffffe0019758f080 Cid 1f30.0718 Teb: 0000000000291000 Win32Thread: ffffe001972e3220 WAIT: (UserRequest) UserMode Non-Alertable ffffe00192c08150 SynchronizationEvent THREAD ffffe00198911080 Cid 1f30.1aac Teb: 00000000002a1000 Win32Thread: ffffe001926147e0 WAIT: (UserRequest) UserMode Non-Alertable ffffe00197d6e150 SynchronizationEvent ffffe001987bf9e0 SynchronizationEvent → Die Threadaktivität untersuchen Kapitel 4 235
THREAD ffffe00199553080 Cid 1f30.1ad4 Teb: 00000000002b1000 Win32Thread: ffffe0019263c740 WAIT: (UserRequest) UserMode Non-Alertable ffffe0019ac6b150 NotificationEvent ffffe0019a7da5e0 SynchronizationEvent THREAD ffffe0019b6b2800 Cid 1f30.1758 Teb: 00000000002bd000 Win32Thread: 0000000000000000 WAIT: (Suspended) KernelMode Non-Alertable SuspendCount 1 ffffe0019b6b2ae0 NotificationEvent ... 3. Wechseln Sie in den Kontext des ersten Threads in dem Prozess (Sie können auch andere Threads auswählen): lkd> .thread /p /r ffffe0019758f080 Implicit thread is now ffffe001'9758f080 Implicit process is now ffffe001'96039080 Loading User Symbols .............................................. 4. Schauen Sie sich jetzt den Thread an, um seine Einzelheiten und seinen Aufrufstack zu untersuchen (Adressen hier gekürzt): lkd> !thread ffffe0019758f080 THREAD ffffe0019758f080 Cid 1f30.0718 Teb: 0000000000291000 Win32Thread : ffffe001972e3220 WAIT : (UserRequest)UserMode Non - Alertable ffffe00192c08150 SynchronizationEvent Not impersonating DeviceMap ffffc000b77f1f30 Owning Process ffffe00196039080 Image : explorer.exe Attached Process N / A Image : N / A Wait Start TickCount 17415276 Ticks : 146 (0:00 : 00 : 02.281) Context Switch Count 2788 UserTime 00 : 00 : 00.031 KernelTime 00 : 00 : 00.000 IdealProcessor : 4 *** WARNING : Unable to verify checksum for C : \windows\explorer.exe Win32 Start Address explorer!wWinMainCRTStartup(0x00007ff7b80de4a0) Stack Init ffffd0002727cc90 Current ffffd0002727bf80 Base ffffd0002727d000 Limit ffffd00027277000 Call 0000000000000000 Priority 8 BasePriority 8 PriorityDecrement 0 IoPriority 2 PagePriority 5 → 236 Kapitel 4 Threads
... Call Site ... nt!KiSwapContext + 0x76 ... nt!KiSwapThread + 0x15a ... nt!KiCommitThreadWait + 0x149 ... nt!KeWaitForSingleObject + 0x375 ... nt!ObWaitForMultipleObjects + 0x2bd ... nt!NtWaitForMultipleObjects + 0xf6 ... nt!KiSystemServiceCopyEnd + 0x13 (TrapFrame @ ffffd000'2727cb00) ... ntdll!ZwWaitForMultipleObjects + 0x14 ... KERNELBASE!WaitForMultipleObjectsEx + 0xef ... USER32!RealMsgWaitForMultipleObjectsEx + 0xdb ... USER32!MsgWaitForMultipleObjectsEx + 0x152 ... explorerframe!SHProcessMessagesUntilEventsEx + 0x8a ... explorerframe!SHProcessMessagesUntilEventEx + 0x22 ... explorerframe!CExplorerHostCreator::RunHost + 0x6d ... explorer!wWinMain + 0xa04fd ... explorer!__wmainCRTStartup + 0x1d6 Einschränkungen für Threads in geschützten Prozessen Wie bereits in Kapitel 3 erwähnt, bestehen bei geschützten Prozessen (was sowohl klassische geschützte Prozesse als auch PPLs einschließt) starke Einschränkungen, was die Zugriffsrechte angeht, selbst für Benutzer mit den höchsten Rechten auf dem System. Diese Einschränkungen gelten auch für die Threads in diesen Prozessen. Dadurch wird sichergestellt, dass der Code, der in einem geschützten Prozess läuft, durch Windows-Standardfunktionen nicht gekapert oder auf andere Weise beeinträchtigt werden kann, denn dazu müssten diese Funktionen Zugriffsrechte bekommen, die für Threads in geschützten Prozessen nicht vergeben werden. Die einzigen gewährten Berechtigungen sind THREAD_SUSPEND_RESUME und THREAD_SET/QUERY_LIMITED_INFORMATION. Experiment: Informationen über Threads in geschützten Prozessen mit Process Explorer anzeigen In diesem Experiment schauen Sie sich Informationen über die Threads in einem geschützten Prozess an. Gehen Sie dazu wie folgt vor: 1. Suchen Sie in der Prozessliste einen geschützten oder einen PPL-Prozess, z. B. Audiodg. exe oder Csrss.exe. → Die Threadaktivität untersuchen Kapitel 4 237
2. Öffnen Sie das Eigenschaftendialogfeld des Prozesses und klicken Sie auf die Registerkarte Threads. 3. Process Explorer zeigt nicht die Win32-Threadstartadresse an, sondern stattdessen den standardmäßigen Threadstartwrapper in Ntdll.dll. Wenn Sie auf die Schaltfläche Stack klicken, wird eine Fehlermeldung angezeigt, da Process Explorer dazu den virtuellen Speicher innerhalb des geschützten Prozesses lesen müsste, was er aber nicht tun kann. 4. Es werden zwar die Basis- und die dynamische Priorität angezeigt, die E/A- und die Speicherpriorität aber nicht (genauso wenig wie die Zyklen). Auch das ist ein Beispiel für das eingeschränkte Zugriffsrecht THREAD_QUERY_LIMITED_INFORMATION im Gegensatz zu dem vollständigen Informationszugriffsrecht THREAD_QUERY_INFORMATION. 5. Versuchen Sie, einen Thread in dem geschützten Prozess zu beenden. Auch dabei erhalten Sie eine Fehlermeldung, da Sie nicht über das Zugriffsrecht THREAD_TERMINATE verfügen. Threadplanung Dieser Abschnitt erläutert die Planungsrichtlinien und -algorithmen von Windows. Die ersten Teilabschnitte geben eine knappe Beschreibung der Threadplanung in Windows und eine Definition der wichtigsten Begriffe. Anschließend werden die Windows-Prioritätsstufen sowohl für Windows-APIs als auch für den Windows-Kernel erklärt. Nach einer Übersicht über die 238 Kapitel 4 Threads
Windows-Hilfsprogramme und -Werkzeuge rund um die Threadplanung werden die Datenstrukturen und Algorithmen des Windows-Planungssystems ausführlich vorgestellt. Dabei werden auch gängige Planungssituationen beschrieben und erklärt, wie die Thread- und die Prozessorauswahl erfolgen. Überblick über die Threadplanung in Windows Windows verwendet ein prioritätsgesteuertes, präemptives Planungssystem. Es wird immer mindestens einer der ausführbaren (betriebsbereiten) Threads höchster Priorität ausgeführt, wobei einige davon jedoch nur auf bestimmten Prozessoren laufen dürfen oder bevorzugt auf bestimmten Prozessoren ausgeführt werden sollten, was als Prozessoraffinität bezeichnet wird. Eine solche Affinität besteht immer zu einer ganzen Gruppe von Prozessoren, die bis zu 64 Prozessoren umfassen kann. Normalerweise können Threads nur auf verfügbaren Prozessoren in der mit dem Prozess verbundenen Gruppe ausgeführt werden. (Dies dient zur Kompatibilität mit älteren Windows-Versionen, die nur 64 Prozessoren unterstützen.) Entwickler können die Prozessoraffinität durch entsprechende APIs sowie über die Affinitätsmaske im Abbildheader ändern. Benutzer können Werkzeuge einsetzen, um die Affinität zur Laufzeit oder beim Erstellen des Prozesses zu ändern. Threads in einem Prozess können zwar unterschiedlichen Gruppen zugeordnet werden, doch ein Thread kann nur auf den Prozessoren seiner Gruppe ausgeführt werden. Entwickler haben auch die Möglichkeit, Anwendungen zu schreiben, die mithilfe einer erweiterten Planungs-API die Affinitäten ihrer Threads mit logischen Prozessoren in verschiedenen Gruppen verknüpfen. Dadurch wird aus dem Prozess ein Mehrgruppenprozess, dessen Threads theoretisch auf jedem verfügbaren Prozessor auf dem Computer ausgeführt werden können. Nachdem ein Thread ausgewählt wurde, wird er eine bestimmte Zeit lang ausgeführt, bis ein anderer Thread derselben Priorität an der Reihe ist. Dieser Zeitraum wird als Quantum bezeichnet, wobei sich die Quantumswerte zwischen den einzelnen Systemen und Prozessen aus drei Gründen unterscheiden: QQ Systemkonfigurationseinstellungen (langes oder kurzes Quantum, festes oder variables, Trennung von Prioritäten) QQ Vordergrund- oder Hintergrundstatus des Prozesses QQ Änderung des Quantums durch das Jobobjekt Diese Einzelheiten werden im Abschnitt über Quanten weiter hinten in diesem Kapitel ausführlich erklärt. Es kann jedoch sein, dass ein Thread gar nicht dazu kommt, sein Quantum komplett auszufüllen, da Windows einen präemptiven Scheduler verwendet. Wenn ein Thread mit höherer Priorität zur Ausführung bereitsteht, kann der aktuelle Thread vorzeitig unterbrochen werden, bevor er eine Zeitscheibe komplett genutzt hat. Es ist sogar möglich, dass ein Thread zur Ausführung ausgewählt, aber schon vor Beginn seines Quantums abgebrochen wird. Der Windows-Planungscode ist im Kernel implementiert. Es gibt jedoch kein eigenes Modul und keine eigene Routine dafür. Der Code ist über die Stellen im Kernel verstreut, in denen Ereignisse rund um die Threadplanung auftreten können. Die Routinen für diese Aufgaben Threadplanung Kapitel 4 239
werden gesammelt als Dispatcher des Kernels bezeichnet. Die folgenden Ereignisse können eine solche Threaddisponierung erfordern: QQ Ein Thread ist zur Ausführung bereit. Das kann etwa der Fall sein, wenn ein Thread neu erstellt wurde oder einen Wartezustand verlassen hat. QQ Ein Thread verlässt den Ausführungsstatus, da sein Zeitquantum abgelaufen ist, er beendet wird, er die Ausführung aufgebt oder er in einen Wartezustand eintritt. QQ Die Priorität eines Threads ändert sich, entweder aufgrund eines Systemdienstaufrufs oder weil Windows selbst den Prioritätswert geändert hat. QQ Die Prozessoraffinität eines Threads ändert sich, sodass er nicht mehr auf dem Prozessor ausgeführt werden kann, auf dem er läuft. Bei jedem dieser Ereignisse muss Windows entscheiden, welcher Thread als Nächstes auf dem logischen Prozessor ausgeführt werden soll, auf dem der betreffende Thread lief, bzw. auf welchem logischen Prozessor der Thread jetzt laufen soll. Nachdem ein logischer Prozessor für den neuen Thread ausgewählt wurde, erfolgt dort schließlich ein Kontextwechsel zu diesem Thread. Dabei wird der flüchtige Prozessorstatus des laufenden Threads gespeichert, der flüchtige Status eines anderen Threads geladen und die Ausführung des neuen Threads gestartet. Windows nimmt die Planung für einzelne Threads vor. Das ist sinnvoll, da Prozesse nicht ausgeführt werden, sondern nur die Ressourcen und den Kontext für die Ausführung ihrer Threads liefern. Da Planungsentscheidungen rein auf Threadebene erfolgen, wird dabei nicht berücksichtigt, zu welchen Prozessen die Threads gehören. Wenn beispielsweise Prozess A über zehn ausführbare Threads verfügt und Prozess B über zwei, wobei alle zwölf Threads dieselbe Priorität haben, dann kann jeder Thread theoretisch ein Zwölftel der CPU-Zeit erhalten. Windows vergibt also nicht 50 % der CPU-Zeit an Prozess A und die anderen 50 % an Prozess B. Prioritätsstufen Für ein Verständnis der Threadplanungsalgorithmen sind zunächst einmal Kenntnisse der Prioritätsstufen in Windows erforderlich. Wie Abb. 4–6 zeigt, verwendet Windows intern 32 Prioritätsstufen von 0 bis 31 (wobei 31 die höchste Priorität dargestellt). Die Werte sind in folgende Bereiche aufgeteilt: QQ 16 Echtzeitstufen (16 bis 31) QQ 16 variable Stufen (0 bis 15), wobei Stufe 0 für den Nullseitenthread reserviert ist (siehe Kapitel 5) 240 Kapitel 4 Threads
Echtzeit Dynamisch (normal) Nullseitenthread Abbildung 4–6 Threadprioritätsstufen Threadprioritätsstufen werden von der Windows-API und vom Windows-Kernel zugewiesen. Die Windows-API ordnet die Prozesse zunächst nach der Prioritätsklasse, der sie bei ihrer Erstellung zugeordnet wurden (wobei die Zahl in Klammern der interne PROCESS_PRIORITY_CLASS-Index ist, den der Kernel erkennt): QQ Echtzeit (4) QQ Hoch (3) QQ Höher als normal (6) QQ Normal (2) QQ Niedriger als normal (5) QQ Leerlauf (1) Mit der Windows-API SetPriorityClass kann die Prioritätsklasse eines Prozesses auf einen dieser Werte eingestellt werden. Anschließend wird den einzelnen Threads in den Prozessen eine relative Priorität zugewiesen. Hier stehen die Zahlen für den Abstand zur Basispriorität des Prozesses: QQ Zeitkritisch (15) QQ Am höchsten (2) QQ Höher als normal (1) QQ Normal (0) QQ Niedriger als normal (-1) QQ Am niedrigsten (-2) QQ Leerlauf (-15) Die Prioritätsstufen Zeitkritisch und Leerlauf (+15 und -15) werden als Sättigungswerte bezeichnet und stellen besondere Stufen dar und keine echten Offsets. Diese Werte können an die Windows-API SetThreadPriority übergeben werden, um die relative Priorität eines Threads zu ändern. In der Windows-API hat daher jeder Thread eine Basispriorität, die von der Prozessprioritätsklasse und seiner relativen Threadpriorität abhängt. Im Kernel werden die bereits erwähnten PROCESS_PRIORITY_CLASS-Indizes mithilfe des globalen Arrays PspPriorityTable in die Basisprio- Threadplanung Kapitel 4 241
ritäten 4, 8, 13, 14, 6 oder 10 umgesetzt. (Hierbei handelt es sich um eine feste Zuordnung, die nicht geändert werden kann.) Anschließend wird die relative Threadpriorität als Differenz dieser Basispriorität zugeschlagen. Beispielsweise erhält ein Thread mit der höchsten Priorität dadurch eine Threadbasispriorität, die zwei Stufen höher liegt als die Basispriorität seines Prozesses. Die Zuordnung der Windows-Prioritäten zu den internen numerischen Windows-Prioritäten wird in Abb. 4–7 und Tabelle 4–1 dargestellt. Normale (dynamische) Prioritäten Echtzeitprioritäten Prioritätsklasse »Echtzeit« Prioritätsklasse »Hoch« Prioritätsklasse »Höher als normal« Prioritätsklasse »Normal« Prioritätsklasse »Niedriger als normal« Prioritätsklasse »Leerlauf« Priorität Abbildung 4–7 In der Windows-API verwendete Threadprioritäten Prioritätsklasse (relative Priorität) Echtzeit Hoch Höher als normal Normal Niedriger als normal Leerlauf Zeitkritisch (+Sättigung) 31 15 15 15 15 15 Am höchsten (+2) 26 15 12 10 8 6 Höher als normal (+1) 25 14 11 9 7 5 Normal (0) 24 13 10 8 6 4 Niedriger als normal (-1) 23 12 9 7 5 3 Am niedrigsten (-2) 22 11 8 6 4 2 Leerlauf (-Sättigung) 16 1 1 1 1 1 Tabelle 4–1 Zuordnung der Windows-Kernelprioritäten zu den Windows-API-Prioritäten Wie Sie sehen, behalten die relativen Threadprioritäten Echtzeit und Leerlauf ihre Werte unabhängig von der Prozessprioritätsklasse bei (sofern diese nicht Echtzeit lautet). Das liegt daran, dass die Windows-API die Sättigung der Priorität vom Kernel anfordert, indem sie +16 oder -16 als gewünschte relative Priorität übergibt. Die Formel zur Berechnung dieser Werte lautet wie folgt (wobei HIGH_PRIORITY gleich 31 ist): If Time-Critical: ((HIGH_PRIORITY+1) / 2 If Idle: -((HIGH_PRIORITY+1) / 2 242 Kapitel 4 Threads
Diese Werte werden vom Kernel als Anforderung zur Sättigung erkannt, sodass das Feld Saturation in der KTHREAD-Struktur ausgefüllt wird. Bei positiver Sättigung erhält der Thread dadurch die höchstmögliche Priorität in seiner Prioritätsklasse (dynamisch oder Echtzeit), bei negativer Sättigung die niedrigste. Spätere Anforderungen zur Änderung der Basispriorität des Prozesses haben keine Auswirkungen mehr auf die Basispriorität dieser Threads, da gesättigte Threads im Verarbeitungscode übersprungen werden. Wie Tabelle 4–1 zeigt, gibt es für Threads sieben mögliche Windows-API-Prioritätsstufen (sechs bei der Prioritätsklasse Hoch). In der Prioritätsklasse Echtzeit können Sie sogar alle Prioritätsstufen zwischen 16 und 31 einstellen (siehe Abb. 4–7). Die Werte, die nicht von den in der Tabelle gezeigten Konstanten abgedeckt werden, können mit den Argumenten -7, -6, -5, -4, -3, 3, 4, 5 und 6 für SetThreadPriority festgelegt werden. (Weitere Informationen erhalten Sie im Abschnitt »Echtzeitprioritäten« weiter hinten.) Für den Scheduler spielt es jedoch keine Rolle, wie die Priorität (als Kombination der Prozessprioritätsklasse und der relativen Threadpriorität) durch die Windows-API gebildet wurde. Für ihn zählt nur das Endergebnis. Beispielsweise gibt es zwei Möglichkeiten, die Prioritätsstufe 10 zu erreichen, nämlich erstens durch einen Prozess der Prioritätsklasse Normal (8) und einen Thread mit der relativen Priorität Am höchsten (+2) und zweitens durch einen Prozess der Prioritätsklasse Höher als normal (10) und einen Thread mit der relativen Priorität Normal (0). Der Scheduler sieht nur den Wert 10, weshalb die Threads für ihn die gleiche Priorität aufweisen. Ein Prozess hat nur einen Basisprioritätswert, ein Thread dagegen eine aktuelle (dynamische) und eine Basispriorität. Planungsentscheidungen werden aufgrund der aktuellen Priorität getroffen. Wie in dem Abschnitt »Prioritätserhöhung« weiter hinten erklärt, kann das System unter bestimmten Umständen die Priorität eines Threads kurzzeitig im dynamischen Bereich (1 bis 15) heraufsetzen. Dagegen ändert Windows niemals die Threadpriorität im Echtzeitbereich (16 bis 31), weshalb bei solchen Threads die Basis- und die aktuelle Priorität stets identisch sind. Ein Thread erbt seine ursprüngliche Basispriorität von seinem Prozess, der seine Basispriorität wiederum standardmäßig von seinem Erstellerprozess erbt. Dieses Verhalten können Sie in der Funktion CreateProcess und mit dem Befehl start ändern. Es ist auch möglich, die Priorität eines Prozesses nachträglich zu ändern. Dazu können Sie die Funktion SetPriorityClass sowie verschiedene Werkzeuge verwenden, in denen sie zur Verfügung steht, darunter der Task-Manager und Process Explorer. (Dazu rechtsklicken Sie auf den Prozess und wählen die neue Prioritätsklasse aus.) Beispielsweise können Sie die Priorität eines CPU-intensiven Prozesses verringern, damit er die normalen Systemaktivitäten nicht beeinträchtigt. Bei der Änderung der Prozesspriorität wird die Priorität seiner Threads entsprechend herauf- oder heruntergesetzt, wobei deren relative Prioritäten jedoch bestehen bleiben. Anwendungen und Dienste starten gewöhnlich mit der Basispriorität Normal, weshalb ihr ursprünglicher Thread normalerweise auf der Prioritätsstufe 8 ausgeführt wird. Einige Windows-Systemprozesse (wie der Sitzungs-Manager, der Dienststeuerungs-Manager und der lokale Sicherheitsauthentifizierungsprozess haben jedoch eine leicht höhere Basisprozesspriorität als Normal, weshalb auch die darin enthaltenen Threads mit einer höheren Priorität als dem Standardwert 8 starten. Threadplanung Kapitel 4 243
Echtzeitprioritäten In jeder Anwendung können Sie die Threadprioritäten im dynamischen Bereich erhöhen oder senken. Um in den Echtzeitbereich vorzudringen, müssen Sie allerdings das Recht für die Planungspriorität erhöhen (SetIncreaseBasePriorityPrivilege). Beachten Sie aber, dass im Echtzeitprioritätsbereich viele wichtige Kernelmodus-Systemthreads von Windows laufen. Wenn Ihre Threads erheblich viel Zeit in diesem Bereich verbringen, können sie dadurch wichtige Systemfunktionen blockieren (etwa den Speicher-Manager, den Cache-Manager oder einige Gerätetreiber). Wenn ein Prozess mit den Standard-Windows-APIs in den Echtzeitbereich verlegt wurde, müssen alle seine Threads (selbst Leerlaufthreads) auf einer der Echtzeitprioritätsstufen laufen. Mit den Standardschnittstellen ist es daher nicht möglich, in einem Prozess Echtzeit- und dynamische Threads zu kombinieren. Das liegt daran, dass die API SetThreadPriority die native API NtSetInformationThread mit der Informationsklasse ThreadBasePriority aufruft, bei der Prioritäten im selben Bereich bleiben müssen. Außerdem dürfen Prioritätsänderungen bei der Verwendung dieser Informationsklasse nur die von Windows-APIs verstandenen Abstände von -2 bis 2 (oder zeitkritisch/Leerlauf) umfassen, es sei denn, die Anforderung kommt von Csrss oder einem anderen Echtzeitprozess. Anders ausgedrückt, ein Echtzeitprozess kann beliebige Threadprioritäten zwischen 16 und 31 auswählen, auch wenn die relativen Threadprioritäten der Windows-Standard-API die Auswahl auf die Werte aus der zuvor gezeigten Tabelle einzuschränken scheinen. Wie bereits erwähnt, führt ein Aufruf von SetThreadPriority mit einigen besonderen Werten dazu, dass NtSetInformationThread mit der Informationsklasse ThreadActualBasePriority aufgerufen wird. Die Kernelbasispriorität für den Thread kann dann direkt festgelegt werden, wobei es auch möglich ist, die Priorität eines Threads in einem Echtzeitprozess im dynamischen Bereich einzustellen. Die Bezeichnung Echtzeit bedeutet nicht, dass es sich bei Windows um ein Echtzeitbetriebssystem im üblichen Sinne des Wortes handelt. Schließlich bietet es keine Echtzeitfunktionen wie garantierte Interruptlatenz oder eine garantierte Ausführungszeit für Threads an. Echtzeit bedeutet hier nur »mit einer höheren Priorität als alle anderen«. Werkzeuge für den Umgang mit Prioritäten Die Basisprozesspriorität können Sie im Task-Manager und Process Explorer einsehen und ändern. In Process Manager können Sie auch einzelne Threads in einem Prozess beenden (wobei Sie natürlich äußerste Vorsicht walten lassen müssen). Die Prioritäten einzelner Prozesse können Sie in der Leistungsüberwachung, in Process Explorer und WinDbg einsehen. Es kann durchaus sinnvoll sein, die Priorität eines Prozesses zu erhöhen oder zu verringern, aber die Einstellung der Priorität einzelner Threads in einem Prozess ist nicht sinnvoll, weil nur jemand, der das Programm in allen Einzelheiten kennt (also der Entwickler), die relative Wichtigkeit der Threads innerhalb des Prozesses überblicken kann. 244 Kapitel 4 Threads
Die einzige Möglichkeit, um die Anfangsprioritätsklasse für einen Prozess anzugeben, bietet der Befehl start in der Windows-Eingabeaufforderung. Wenn Sie ein Programm immer mit einer bestimmten Priorität starten lassen wollen, können Sie einen Kurzbefehl definieren, um immer den Befehl start zu verwenden. Dieser Kurzbefehl muss mit cmd /c beginnen, um die Eingabeaufforderung zu starten. Anschließend führt er den angegebenen Befehl aus und beendet die Eingabeaufforderung. Um beispielsweise den Editor mit der Leerlauf-Prozesspriorität auszuführen, verwenden Sie den Befehl cmd /c start /low Notepad.exe. Experiment: Prozess- und Threadprioritäten untersuchen und festlegen Um Prozess- und Threadprioritäten zu untersuchen und festzulegen, gehen Sie wie folgt vor: 1. Führen Sie Notepad.exe ganz normal aus, etwa indem Sie an der Eingabeaufforderung notepad eingeben. 2. Öffnen Sie den Task-Manager und klicken Sie auf die Registerkarte Details. 3. Fügen Sie die Spalte Basispriorität hinzu. Das ist die Bezeichnung der Prioritätsklasse im Task-Manager. 4. Suchen Sie in der Liste nach Notepad: 5. Wie Sie sehen, läuft der Notepad-Prozess mit der Prioritätsklasse Normal (8). Die Prioritätsklasse Leerlauf wird im Task-Manager als Niedrig angezeigt. 6. Öffnen Sie Process Explorer. 7. Doppelklicken Sie auf den Prozess Notepad, um sein Eigenschaftendialogfeld aufzurufen, und klicken Sie auf die Registerkarte Threads. → Threadplanung Kapitel 4 245
8. Markieren Sie den ersten Thread (falls es mehrere gibt). Sie sollten jetzt folgende Anzeige sehen: 9. Achten Sie auf die Prioritäten des Threads: Seine Basispriorität beträgt nach wie vor 8, seine aktuelle (dynamische) Priorität dagegen 10. Den Grund dafür erfahren Sie im Abschnitt »Prioritätserhöhung« weiter hinten. 10. Wenn Sie wollen, können Sie den Thread unterbrechen und beenden (wobei Sie jedoch in beiden Fällen sehr vorsichtig sein müssen). 11. Rechtsklicken Sie im Task-Manager auf den Prozess Notepad, wählen Sie Priorität festlegen und dann Hoch: → 246 Kapitel 4 Threads
12. Bestätigen Sie den Vorgang in dem daraufhin eingeblendeten Dialogfeld. In Process Explorer können Sie jetzt erkennen, dass die Priorität des Threads in die Klasse Hoch (13) heraufgesetzt wurde. Die dynamische Priorität hat die gleiche relative Steigerung erfahren: 13. Ändern Sie die Prioritätsklasse im Task-Manager auf Echtzeit. Dazu müssen Sie ein Administrator auf dem Computer sein. Diese Änderung können Sie auch in Process Manager vornehmen. 14. In Process Manager betragen die Basis- und die dynamische Priorität des Threads jetzt beide 24. Wie Sie wissen, wendet der Kernel auf Threads im Echtzeitbereich niemals Prioritätserhöhungen an. Threadplanung Kapitel 4 247
Der Windows-Systemressourcen-Manager Windows Server 2012 R2 Standard Edition und höhere SKUs enthalten den optional in stallierbaren Windows-Systemressourcen-Manager (WSRM). Damit können Administratoren Richtlinien aufstellen, um Affinitätseinstellungen und Grenzwerte für die CPU- und Speichernutzung (sowohl physisch als auch virtuell) durch Prozesse festzulegen. Darüber hinaus kann WSRM Ressourcennutzungsberichte erstellen, die sich zur Buchführung und als Nachweis für Dienstleistungsverträge mit Benutzern nutzen lassen. Die Richtlinien können auf bestimmte Anwendungen angewandt werden (durch Vergleich des Abbildnamens mit oder ohne Befehlszeilenargumente), aber auch auf Benutzer oder Gruppen. Sie lassen sich auch zeitlich planen, sodass sie nur zu bestimmten Zeiten in Kraft treten oder ständig gelten. Nachdem Sie eine Ressourcenzuweisungsrichtlinie aufgestellt haben, überwacht der WSRM-Dienst die CPU-Nutzung der darüber verwalteten Prozesse und passt deren Basisprioritäten an, wenn sie ihre Sollwerte nicht erfüllen. Bei den Grenzwerten für den physischen Arbeitsspeicher wird mit der Funktion SetProcessWorkingSetSizeEx ein harter Höchstwert für die Größe des Arbeitssatzes festgelegt. Beim Grenzwert für den virtuellen Arbeitsspeicher prüft der Dienst, wie viel privaten virtuellen Arbeitsspeicher die Prozesse verbrauchen (siehe Kapitel 5). Wird der Grenzwert überschritten, kann WSRM je nach Einstellung entweder den betreffenden Prozess beenden oder einen Eintrag in das Ereignisprotokoll schreiben. Damit lässt sich ein Prozess mit einem Speicherleck aufspüren, bevor er den gesamten verfügbaren, zugesicherten Speicher auf dem System verbraucht. Die WSRM-Speichergrenzwerte gelten jedoch nicht für AWE-Speicher (Address Windowing Extensions), Arbeitsspeicher mit großen Seiten und den Kernelspeicher (nicht ausgelagerter und ausgelagerter Pool). Mehr darüber erfahren Sie in Kapitel 5. Threadstatus Bevor wir uns mit den Threadplanungsalgorithmen beschäftigen, müssen wir zunächst die verschiedenen Ausführungsstatus ansehen, in denen sich ein Thread befinden kann: QQ Bereit (Ready) Ein Thread in diesem Status wartet darauf, dass er ausgeführt oder nach Abschluss eines Wartevorgangs wieder eingewechselt wird. Bei der Suche nach auszuführenden Threads berücksichtigt der Dispatcher nur solche, die bereit sind. QQ Verzögert bereit (Deferred Ready) Dieser Status wird für Threads verwendet, die auf einem bestimmten Prozessor ausgewählt wurden, aber noch nicht begonnen haben, dort zu laufen. Dieser Status ist dazu da, dass der Kernel die Zeit verkürzen kann, die eine prozessorweise Sperre für die Planungsdatenbank aufrechterhalten wird. QQ Standby Ein Thread in diesem Zustand wurde dazu ausgewählt, als nächster auf einem bestimmten Prozessor zu laufen. Wenn die erforderlichen Bedingungen hergestellt sind, führt der Dispatcher einen Kontextwechsel zu diesem Thread durch. Auf jedem Prozessor des Systems kann sich immer nur ein Thread auf einmal im Standby-Zustand befinden. 248 Kapitel 4 Threads
Es ist übrigens möglich, dass ein Thread noch im Standby-Status unterbrochen wird, also bevor er mit der Ausführung begonnen hat (etwa wenn ein Thread mit höherer Priorität inzwischen lauffähig wurde). QQ Wird ausgeführt (Running) Nachdem der Dispatcher einen Kontextwechsel zu einem Thread durchgeführt hat, geht dieser Thread in den Zustand Wird ausgeführt über. Der Thread wird so lange ausgeführt, bis sein Quantum abläuft (und ein anderer Thread derselben Priorität bereit zur Ausführung ist), er durch einen Thread höherer Priorität unterbrochen wird, er beendet wird, er die Ausführung abgibt oder er freiwillig in einen Wartezustand eintritt. QQ Wartend (Waiting) Ein Thread kann auf verschiedene Weisen in den Wartezustand übergehen: Er kann freiwillig auf ein Objekt warten, um die Ausführung zu synchronisieren; das Betriebssystem kann für den Thread warten (etwa um eine Auslagerungs-E/A durchzuführen); und ein Umgebungsteilsystem kann den Thread anweisen, sich selbst anzuhalten. Wenn die Wartezeit des Threads endet, wird er je nach seiner Priorität entweder sofort weiter ausgeführt oder in den Status Bereit versetzt. QQ Übergang (Transition) Ein Thread tritt in den Übergangsstatus ein, wenn er zur Ausführung bereit ist, sein Kernelstack aber aus dem Arbeitsspeicher ausgelagert wurde. Nachdem der Kernelstack wieder in den Arbeitsspeicher zurückgebracht wurde, nimmt der Thread den Status Bereit an. (Threadstacks werden in Kapitel 5 erklärt.) QQ Abgebrochen (Terminated) Wenn ein Thread die Ausführung beendet, tritt er in diesen Status ein. Ob nach seiner Beendigung die Zuweisung des Exekutivthreadobjekts (der Datenstruktur im Systemarbeitsspeicher, die den Thread beschreibt) aufgehoben wird oder nicht, bestimmt der Objekt-Manager. Beispielsweise bleibt das Objekt bestehen, wenn es noch offene Handles zu dem Thread gibt. Ein Thread kann auch in den Status Abgebrochen übergehen, wenn er ausdrücklich von einem anderen Thread beendet wird, etwa durch den Aufruf der Windows-API TerminateThread. QQ Initialisiert (Initialized) verwendet. Dieser Status wird intern während der Erstellung des Threads Abb. 4–8 zeigt die wichtigsten Statusübergänge für Threads. Die Zahlen stehen für die internen Werte der einzelnen Zustände, die in Werkzeugen wie der Leistungsüberwachung angezeigt werden. Bereit und Verzögert bereit werden als ein Status dargestellt, da Verzögert bereit lediglich als vorübergehender Platzhalter für Planungsroutinen dient. Das gilt auch für den Status Standby. Diese Zustände sind fast immer sehr kurzlebig. Threads in diesem Status gehen immer schnell in Bereit, Wird ausgeführt oder Wartend über. Threadplanung Kapitel 4 249
Initialisierung (0) Vorzeitige Unterbrechung, Ablauf des Quantums Vorzeitige Unterbrechung Bereit (1), verzögert bereit (7) Einwechseln des Kernelstacks Wird ausgeführt (2) Standby (3) Freiwilliger Wechsel Übergang (6) Wartend (5) Abgebrochen (4) Auslagerung des Kernelstacks Abbildung 4–8 Threadstatus und Übergänge Experiment: Statuswechsel bei der Threadplanung Die Statuswechsel bei der Threadplanung können Sie mit der Windows-Leistungsüberwachung beobachten. Dieses Werkzeug ist sehr praktisch, wenn Sie eine Multithreadanwendung debuggen wollen und sich über den Status der Threads in den Prozessen nicht im Klaren sind. Zur Beobachtung der Statuswechsel gehen Sie wie folgt vor: 1. Laden Sie das CPU Stress Tool aus den Begleitmaterialien zu diesem Buch herunter. 2. Starten Sie CPUStres.exe. Thread 1 sollte jetzt aktiv sein. 3. Aktivieren Sie Thread 2, indem Sie ihn in der Liste markieren und auf Activate klicken oder indem Sie auf ihn rechtsklicken und im Kontextmenü Activate auswählen. Sie sollten jetzt eine ähnliche Anzeige wie die folgende sehen: → 250 Kapitel 4 Threads
4. Klicken Sie auf die Startschaltfläche und geben Sie perfmon ein, um die Leistungsüberwachung zu starten. 5. Wählen Sie ggf. die Diagrammansicht aus und entfernen Sie den vorhandenen CPU-Indikator. 6. Rechtsklicken Sie auf das Diagramm und wählen Sie Eigenschaften. 7. Klicken Sie auf die Registerkarte Grafik und ändern Sie das Maximum für die vertikale Skalierung auf 7 (da die einzelnen Status mit den Zahlen 0 bis 7 bezeichnet werden). Klicken Sie auf OK. 8. Klicken Sie in der Symbolleiste auf Hinzufügen, um das Dialogfeld Leistungsindikatoren hinzufügen zu öffnen. 9. Wählen Sie das Leistungsobjekt Thread und darunter den Indikator Threadstatus aus. 10. Aktivieren Sie das Kontrollkästchen Beschreibung anzeigen, um eine Definition der Werte einzublenden: 11. Wählen Sie <Alle Instanzen> im Feld Instanzen. Geben Sie dann cpustres ein und klicken Sie auf Suchen. 12. Wählen Sie die ersten drei Threads von cpustres aus (cpustres/0, cpustres/1 und cpu stres/2), klicken Sie auf Hinzufügen >> und dann auf OK. Thread 0 befindet sich im Status 5 (Wartend), da dies der GUI-Thread ist, der auf Benutzereingaben wartet. Die Threads 1 und 2 wechseln ständig zwischen den Status 2 und 5 (Wird ausgeführt und Wartend). Es kann sein, dass Thread 2 hinter Thread 1 verborgen ist, da beide mit derselben Priorität und derselben Aktivität laufen. → Threadplanung Kapitel 4 251
13. Rechtsklicken Sie in CPU Stress auf Thread 2 und wählen Sie im Kontextmenü unter Activity den Punkt Busy. Jetzt wird Thread 2 häufiger in Status 2 (Wird ausgeführt) angezeigt als Thread 1: → 252 Kapitel 4 Threads
14. Rechtsklicken Sie auf Thread 1 und wählen Sie den Aktivitätsgrad Maximum. Wiederholen Sie diesen Schritt für Thread 2. Jetzt sind beide Threads ständig in Status 2, da sie praktisch in einer Endlosschleife ausgeführt werden: Auf einem Einzelprozessorsystem sehen Sie etwas anderes. Da es nur einen Prozessor gibt, kann immer nur ein Thread auf einmal ausgeführt werden, sodass die beiden Threads zwischen Status 1 (Bereit) und 2 (Wird ausgeführt) wechseln: → Threadplanung Kapitel 4 253
15. Wenn Sie auf einem Mehrprozessorsystem arbeiten (was wahrscheinlicher ist), können Sie den gleichen Effekt dadurch erzielen, dass Sie im Task-Manager auf den Prozess CPUSTRES rechtsklicken, Zugehörigkeit festlegen wählen und dann nur einen einzigen Prozessor auswählen, egal welchen. (Das können Sie auch in CPU Stress selbst tun, indem Sie das Menü Process öffnen und Affinity auswählen.) 16. Es gibt noch eine weitere Sache, die Sie ausprobieren können. Nachdem Sie die Einstellung aus Punkt 15 vorgenommen haben, rechtsklicken Sie in CPU Stress auf Thread 1 und wählen die Priorität Above normal. Jetzt wird Thread 1 kontinuierlich ausgeführt (Status 2), während sich Thread 2 immer im Bereitschaftsstatus (1) befindet. Das liegt daran, dass es nur einen Prozessor gibt, weshalb im Allgemeinen der Thread mit höherer Priorität den Zuschlag erhält. Von Zeit zu Zeit werden Sie jedoch sehen, dass Thread 1 in den Bereitschaftsstatus übergeht, weil der abgehängte Thread ca. alle vier Sekunden eine Prioritätserhöhung erhält, sodass er kurzzeitig laufen kann. Allerdings wird diese Statusänderung in dem Diagramm oft nicht angezeigt, da die Leistungsüberwachung nur eine Auflösung von einer Sekunde hat, was zu grob ist. Eine ausführlichere Beschreibung dieses Vorgangs finden Sie weiter hinten in diesem Kapitel im Abschnitt »Prioritätserhöhung«. 254 Kapitel 4 Threads
Die Dispatcherdatenbank Um Threadplanungsentscheidungen treffen zu können, unterhält der Kernel eine Reihe von Datenstrukturen, die zusammengenommen als Dispatcherdatenbank bezeichnet werden. Darin bewahrt der Kernel den Überblick darüber, welche Threads auf Ausführung waren und auf welchen Prozessoren welche Threads laufen. Um die Skalierbarkeit zu verbessern und eine parallele Threaddisponierung zu ermöglichen, gibt es in einem Windows-Mehrprozessorsystem Bereitschaftswarteschlangen für die einzelnen Prozessoren und gemeinsam genutzte Bereitschaftswarteschlangen für Prozessorgruppen (siehe Abb. 4–9). Dadurch kann jede CPU in der für sie zuständigen Bereitschaftswarteschlange nach dem nächsten auszuführenden Thread suchen, ohne die systemweiten Bereitschaftswarteschlangen sperren zu müssen. Gruppe 1 (CPU 0, 1, 2) Bereitschaftswarteschlangen Gruppe 2 (CPU 3, 4, 5) Bereitschaftswarteschlangen Bereitschaftsübersicht Bereitschaftsübersicht Abbildung 4–9 Dispatcherdatenbank in einem Windows-Mehrprozessorsystem. In diesem Beispiel werden sechs Prozessoren verwendet. P steht für Prozesse, T für Threads. In Windows-Versionen vor Windows 8 und Windows Server 2012 hatte jeder Prozessor seine eigene Bereitschaftswarteschlange und seine eigene Bereitschaftsübersicht. Sie waren im Prozessorsteuerungsblock (PRCB) gespeichert. (Die Felder des PRCB können Sie mit dem Befehl dt nt!_kprcb im Kerneldebugger einsehen.) Beginnend mit Windows 8 und Windows Server 2012 werden gemeinsam genutzte Bereitschaftswarteschlangen und Bereitschaftsübersichten jeweils für eine ganze Prozessorgruppe verwendet. Dadurch kann das System besser entscheiden, welcher Prozessor der Gruppe als nächster verwendet werden soll. Die prozessoreigenen Bereitschaftswarteschlangen sind jedoch nach wie vor vorhanden und werden für Threads mit Affinitätseinschränkungen verwendet. Threadplanung Kapitel 4 255
Die gemeinsamen Datenstrukturen müssen (durch ein Spinlock) geschützt werden. Damit die Konkurrenz um die Warteschlangen auf einem unerheblichen Niveau bleibt, dürfen die Gruppen daher nicht zu groß werden. In der aktuellen Implementierung beträgt die maximale Gruppengröße vier Prozessoren. Gibt es mehr als vier logische Prozessoren, so werden weitere Gruppen gebildet und die verfügbaren Prozessoren gleichmäßig darauf verteilt. Beispielsweise werden auf einem Sechs-Prozessor-System zwei Gruppen zu je drei Prozessoren angelegt. Die Bereitschaftswarteschlangen, die Bereitschaftsübersichten und einige andere Informationen werden in einer Kernelstruktur namens KSHARED_READY_QUEUE gespeichert, die sich wiederum im PRCB befindet. Es gibt sie zwar für jeden Prozessor, doch es wird immer nur die Struktur auf dem jeweils ersten Prozessor einer Gruppe von allen Prozessoren der Gruppe gemeinsam verwendet. Die Bereitschaftswarteschlangen (ReadListHead in KSHARED_READY_QUEUE) enthalten die Threads, die sich im Bereitschaftszustand befinden und darauf warten, dass sie zur Ausführung eingeplant werden. Es gibt eine Warteschlange für jede der 32 Prioritätsstufen. Um die Auswahl der auszuführenden oder zu unterbrechenden Threads zu beschleunigen, unterhält Windows eine 32-Bit-Maske, die als Bereitschaftsübersicht (ReadySummary) bezeichnet wird. Jedes gesetzte Bit darin steht für einen oder mehr Threads in der Bereitschaftswarteschlange für die jeweilige Prioritätsstufe (Bit 0 steht für Priorität 0, Bit 1 für Priorität 1 usw.). Anstatt die einzelnen Bereitschaftslisten darauf zu untersuchen, ob sie leer sind oder nicht (was die Planungsentscheidungen von der Anzahl an Threads mit unterschiedlicher Priorität abhängig machen würde), wird mit einem nativen Prozessorbefehl ein einziger Bitscan durchgeführt, um das höchste gesetzte Bit zu finden. Dieser Vorgang dauert unabhängig von der Anzahl der Threads in der Bereitschaftswarteschlange immer gleich lang. Die Dispatcherwarteschlange wird durch Erhöhung der IRQL auf DISPATCH_LEVEL (2) synchronisiert. (Eine Beschreibung der Interrupt-Prioritätsstufen oder IRQLs finden Sie in Kapitel 6.) Dadurch wird verhindert, dass andere Threads die Threaddisponierung auf dem Prozessor unterbrechen, da Threads normalerweise auf IRQL 0 oder 1 laufen. Es ist jedoch noch mehr erforderlich, als nur die IRQL hochzusetzen, denn es kann sein, dass andere Prozessoren gleichzeitig eine Erhöhung auf dieselbe IRQL vornehmen, um ihre eigene Dispatcherdatenbank zu bearbeiten. Wie Windows den Zugriff auf die Dispatcherdatenbank synchronisiert, wird weiter hinten in diesem Kapitel im Abschnitt »Mehrprozessorsysteme« erklärt. Experiment: Ausführungsbereite Threads anzeigen Mit dem Kerneldebuggerbefehl !ready können Sie sich eine Liste der Threads anzeigen lassen, die auf den einzelnen Prioritätsstufen zur Ausführung bereit sind.